Coverage for src/debputy/lsp/lsp_reference_keyword.py: 98%

55 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-09-07 09:27 +0000

1import dataclasses 

2import textwrap 

3from typing import ( 

4 Optional, 

5 Union, 

6 Mapping, 

7 Sequence, 

8 Callable, 

9 Iterable, 

10 Any, 

11 Self, 

12 TYPE_CHECKING, 

13) 

14 

15from debputy.lsp.named_styles import ALL_PUBLIC_NAMED_STYLES 

16from debputy.lsp.ref_models.deb822_reference_parse_models import UsageHint 

17from debputy.lsp.vendoring._deb822_repro import Deb822ParagraphElement 

18 

19if TYPE_CHECKING: 

20 import lsprotocol.types as types 

21 from debputy.linting.lint_util import LintState 

22 from debputy.lsp.debputy_ls import DebputyLanguageServer 

23else: 

24 import debputy.lsprotocol.types as types 

25 

26LSP_DATA_DOMAIN = "debputy-lsp-data" 

27 

28 

29def format_comp_item_synopsis_doc( 

30 usage_hint: Optional[UsageHint], 

31 synopsis_doc: Optional[str], 

32 is_deprecated: bool, 

33) -> str: 

34 if is_deprecated: 

35 return ( 

36 f"[OBSOLETE]: {synopsis_doc}" 

37 if synopsis_doc is not None and not synopsis_doc.isspace() 

38 else f"[OBSOLETE]" 

39 ) 

40 if usage_hint is not None: 

41 return ( 

42 f"[{usage_hint.upper()}]: {synopsis_doc}" 

43 if synopsis_doc is not None and not synopsis_doc.isspace() 

44 else f"[{usage_hint.upper()}]" 

45 ) 

46 return synopsis_doc 

47 

48 

49@dataclasses.dataclass(slots=True, frozen=True) 

50class Keyword: 

51 value: str 

52 synopsis: Optional[str] = None 

53 long_description: Optional[str] = None 

54 translation_context: str = "" 

55 is_obsolete: bool = False 

56 replaced_by: Optional[str] = None 

57 is_exclusive: bool = False 

58 """For keywords in fields that allow multiple keywords, the `is_exclusive` can be 

59 used for keywords that cannot be used with other keywords. As an example, the `all` 

60 value in `Architecture` of `debian/control` cannot be used with any other architecture. 

61 """ 

62 is_alias_of: Optional[str] = None 

63 is_completion_suggestion: bool = True 

64 sort_text: Optional[ 

65 Union[ 

66 str, 

67 Callable[ 

68 ["Keyword", "LintState", Sequence[Deb822ParagraphElement], str], str 

69 ], 

70 ] 

71 ] = None 

72 usage_hint: Optional[UsageHint] = None 

73 can_complete_keyword_in_stanza: Optional[ 

74 Callable[[Iterable[Deb822ParagraphElement]], bool] 

75 ] = None 

76 

77 @property 

78 def is_deprecated(self) -> bool: 

79 return self.is_obsolete or self.replaced_by is not None 

80 

81 def resolve_sort_text( 

82 self, 

83 lint_state: "LintState", 

84 stanza_parts: Sequence[Deb822ParagraphElement], 

85 value_being_completed: str, 

86 ) -> Optional[str]: 

87 sort_text = self.sort_text 

88 if sort_text is None: 

89 return None 

90 if not isinstance(sort_text, str): 

91 return sort_text( 

92 self, 

93 lint_state, 

94 stanza_parts, 

95 value_being_completed, 

96 ) 

97 return sort_text 

98 

99 def is_keyword_valid_completion_in_stanza( 

100 self, 

101 stanza_parts: Sequence[Deb822ParagraphElement], 

102 ) -> bool: 

103 return ( 

104 self.can_complete_keyword_in_stanza is None 

105 or self.can_complete_keyword_in_stanza(stanza_parts) 

106 ) 

107 

108 def replace(self, **changes: Any) -> "Self": 

109 return dataclasses.replace(self, **changes) 

110 

111 def synopsis_translated( 

112 self, translation_provider: Union["DebputyLanguageServer", "LintState"] 

113 ) -> str: 

114 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext( 

115 self.translation_context, 

116 self.synopsis, 

117 ) 

118 

119 def long_description_translated( 

120 self, translation_provider: Union["DebputyLanguageServer", "LintState"] 

121 ) -> str: 

122 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext( 

123 self.translation_context, 

124 self.long_description, 

125 ) 

126 

127 def as_completion_item( 

128 self, 

129 lint_state: "LintState", 

130 stanza_parts: Sequence[Deb822ParagraphElement], 

131 value_being_completed: str, 

132 markdown_kind: types.MarkupKind, 

133 ) -> types.CompletionItem: 

134 return types.CompletionItem( 

135 self.value, 

136 insert_text=self.value if self.is_alias_of is None else self.is_alias_of, 

137 sort_text=self.resolve_sort_text( 

138 lint_state, 

139 stanza_parts, 

140 value_being_completed, 

141 ), 

142 detail=format_comp_item_synopsis_doc( 

143 self.usage_hint, 

144 self.synopsis_translated(lint_state), 

145 self.is_deprecated, 

146 ), 

147 deprecated=self.is_deprecated, 

148 tags=[types.CompletionItemTag.Deprecated] if self.is_deprecated else None, 

149 documentation=( 

150 types.MarkupContent(value=self.long_description, kind=markdown_kind) 

151 if self.long_description 

152 else None 

153 ), 

154 ) 

155 

156 

157def allowed_values(*values: Union[str, Keyword]) -> Mapping[str, Keyword]: 

158 as_keywords = [k if isinstance(k, Keyword) else Keyword(k) for k in values] 

159 as_mapping = {k.value: k for k in as_keywords if k.value} 

160 # Simple bug check 

161 assert len(as_keywords) == len(as_mapping) 

162 return as_mapping 

163 

164 

165# This is the set of styles that `debputy` explicitly supports, which is more narrow than 

166# the ones in the config file. 

167ALL_PUBLIC_NAMED_STYLES_AS_KEYWORDS = allowed_values( 

168 Keyword(s.name, long_description=s.long_description) 

169 for s in ALL_PUBLIC_NAMED_STYLES 

170)