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

60 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-01-26 19:30 +0000

1import dataclasses 

2import textwrap 

3from typing import ( 

4 Optional, 

5 Union, 

6 Any, 

7 Self, 

8 TYPE_CHECKING, 

9) 

10from collections.abc import Mapping, Sequence, Callable, Iterable 

11 

12from debputy.lsp.named_styles import ALL_PUBLIC_NAMED_STYLES 

13from debputy.lsp.ref_models.deb822_reference_parse_models import UsageHint 

14from debputy.lsp.vendoring._deb822_repro import Deb822ParagraphElement 

15 

16if TYPE_CHECKING: 

17 import lsprotocol.types as types 

18 from debputy.linting.lint_util import LintState 

19 from debputy.lsp.debputy_ls import DebputyLanguageServer 

20else: 

21 import debputy.lsprotocol.types as types 

22 

23LSP_DATA_DOMAIN = "debputy-lsp-data" 

24 

25 

26def format_comp_item_synopsis_doc( 

27 usage_hint: UsageHint | None, 

28 synopsis_doc: str | None, 

29 is_deprecated: bool, 

30) -> str: 

31 if is_deprecated: 

32 return ( 

33 f"[OBSOLETE]: {synopsis_doc}" 

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

35 else f"[OBSOLETE]" 

36 ) 

37 if usage_hint is not None: 

38 return ( 

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

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

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

42 ) 

43 return synopsis_doc or "" 

44 

45 

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

47class Keyword: 

48 value: str 

49 synopsis: str | None = None 

50 long_description: str | None = None 

51 translation_context: str = "" 

52 is_obsolete: bool = False 

53 replaced_by: str | None = None 

54 is_exclusive: bool = False 

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

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

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

58 """ 

59 is_alias_of: str | None = None 

60 is_completion_suggestion: bool = True 

61 sort_text: None | ( 

62 str 

63 | Callable[["Keyword", "LintState", Sequence[Deb822ParagraphElement], str], str] 

64 ) = None 

65 usage_hint: UsageHint | None = None 

66 can_complete_keyword_in_stanza: None | ( 

67 Callable[[Iterable[Deb822ParagraphElement]], bool] 

68 ) = None 

69 

70 @property 

71 def is_deprecated(self) -> bool: 

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

73 

74 def resolve_sort_text( 

75 self, 

76 lint_state: "LintState", 

77 stanza_parts: Sequence[Deb822ParagraphElement], 

78 value_being_completed: str, 

79 ) -> str | None: 

80 sort_text = self.sort_text 

81 if sort_text is None: 

82 return None 

83 if not isinstance(sort_text, str): 

84 return sort_text( 

85 self, 

86 lint_state, 

87 stanza_parts, 

88 value_being_completed, 

89 ) 

90 return sort_text 

91 

92 def is_keyword_valid_completion_in_stanza( 

93 self, 

94 stanza_parts: Sequence[Deb822ParagraphElement], 

95 ) -> bool: 

96 return ( 

97 self.can_complete_keyword_in_stanza is None 

98 or self.can_complete_keyword_in_stanza(stanza_parts) 

99 ) 

100 

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

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

103 

104 def synopsis_translated( 

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

106 ) -> str | None: 

107 if self.synopsis is None: 

108 return None 

109 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext( 

110 self.translation_context, 

111 self.synopsis, 

112 ) 

113 

114 def long_description_translated( 

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

116 ) -> str | None: 

117 if self.long_description is None: 

118 return None 

119 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext( 

120 self.translation_context, 

121 self.long_description, 

122 ) 

123 

124 def as_completion_item( 

125 self, 

126 lint_state: "LintState", 

127 stanza_parts: Sequence[Deb822ParagraphElement], 

128 value_being_completed: str, 

129 markdown_kind: types.MarkupKind, 

130 ) -> types.CompletionItem: 

131 return types.CompletionItem( 

132 self.value, 

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

134 sort_text=self.resolve_sort_text( 

135 lint_state, 

136 stanza_parts, 

137 value_being_completed, 

138 ), 

139 detail=format_comp_item_synopsis_doc( 

140 self.usage_hint, 

141 self.synopsis_translated(lint_state), 

142 self.is_deprecated, 

143 ), 

144 deprecated=self.is_deprecated, 

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

146 documentation=( 

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

148 if self.long_description 

149 else None 

150 ), 

151 ) 

152 

153 

154def allowed_values(*values: str | Keyword) -> Mapping[str, Keyword]: 

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

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

157 # Simple bug check 

158 assert len(as_keywords) == len(as_mapping) 

159 return as_mapping 

160 

161 

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

163# the ones in the config file. 

164ALL_PUBLIC_NAMED_STYLES_AS_KEYWORDS = allowed_values( 

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

166 for s in ALL_PUBLIC_NAMED_STYLES 

167)