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

56 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-10-12 15:06 +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 

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: 

107 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext( 

108 self.translation_context, 

109 self.synopsis, 

110 ) 

111 

112 def long_description_translated( 

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

114 ) -> str: 

115 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext( 

116 self.translation_context, 

117 self.long_description, 

118 ) 

119 

120 def as_completion_item( 

121 self, 

122 lint_state: "LintState", 

123 stanza_parts: Sequence[Deb822ParagraphElement], 

124 value_being_completed: str, 

125 markdown_kind: types.MarkupKind, 

126 ) -> types.CompletionItem: 

127 return types.CompletionItem( 

128 self.value, 

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

130 sort_text=self.resolve_sort_text( 

131 lint_state, 

132 stanza_parts, 

133 value_being_completed, 

134 ), 

135 detail=format_comp_item_synopsis_doc( 

136 self.usage_hint, 

137 self.synopsis_translated(lint_state), 

138 self.is_deprecated, 

139 ), 

140 deprecated=self.is_deprecated, 

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

142 documentation=( 

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

144 if self.long_description 

145 else None 

146 ), 

147 ) 

148 

149 

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

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

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

153 # Simple bug check 

154 assert len(as_keywords) == len(as_mapping) 

155 return as_mapping 

156 

157 

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

159# the ones in the config file. 

160ALL_PUBLIC_NAMED_STYLES_AS_KEYWORDS = allowed_values( 

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

162 for s in ALL_PUBLIC_NAMED_STYLES 

163)