Coverage for src/debputy/lsp/text_util.py: 85%

80 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-10-12 15:06 +0000

1from typing import List, Optional, Union, TYPE_CHECKING 

2from collections.abc import Sequence, Iterable 

3 

4from debputy.commands.debputy_cmd.output import MAN_URL_REWRITE 

5from debputy.lsprotocol.types import ( 

6 TextEdit, 

7 Position, 

8 Range, 

9 WillSaveTextDocumentParams, 

10 DocumentFormattingParams, 

11) 

12 

13from debputy.linting.lint_util import LinterPositionCodec 

14 

15try: 

16 from debputy.lsp.vendoring._deb822_repro.locatable import ( 

17 Position as TEPosition, 

18 Range as TERange, 

19 ) 

20 from debputy.lsp.debputy_ls import DebputyLanguageServer 

21except ImportError: 

22 pass 

23 

24try: 

25 from pygls.server import LanguageServer 

26 from pygls.workspace import TextDocument, PositionCodec 

27except ImportError: 

28 pass 

29 

30if TYPE_CHECKING: 

31 LintCapablePositionCodec = Union[LinterPositionCodec, PositionCodec] 

32else: 

33 LintCapablePositionCodec = LinterPositionCodec 

34 

35 

36def markdown_urlify(uri: str) -> str: 

37 if uri.startswith("man:"): 37 ↛ 45line 37 didn't jump to line 45 because the condition on line 37 was always true

38 m = MAN_URL_REWRITE.match(uri) 

39 if m: 39 ↛ 43line 39 didn't jump to line 43 because the condition on line 39 was always true

40 page, section = m.groups() 

41 link_url = f"https://manpages.debian.org/{page}.{section}" 

42 return f"[{uri}]({link_url})" 

43 return uri 

44 

45 return f"<{uri}>" 

46 

47 

48def normalize_dctrl_field_name(f: str) -> str: 

49 if not f or not f.startswith(("x", "X")): 

50 return f 

51 i = 0 

52 for i in range(1, len(f)): 52 ↛ 58line 52 didn't jump to line 58 because the loop on line 52 didn't complete

53 if f[i] == "-": 

54 i += 1 

55 break 

56 if f[i] not in ("b", "B", "s", "S", "c", "C"): 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true

57 return f 

58 assert i > 0 

59 return f[i:] 

60 

61 

62def on_save_trim_end_of_line_whitespace( 

63 ls: "LanguageServer", 

64 params: WillSaveTextDocumentParams | DocumentFormattingParams, 

65) -> Sequence[TextEdit] | None: 

66 doc = ls.workspace.get_text_document(params.text_document.uri) 

67 return trim_end_of_line_whitespace(doc.position_codec, doc.lines) 

68 

69 

70def trim_end_of_line_whitespace( 

71 position_codec: "LintCapablePositionCodec", 

72 lines: list[str], 

73 *, 

74 line_range: Iterable[int] | None = None, 

75 line_relative_line_no: int = 0, 

76) -> Sequence[TextEdit] | None: 

77 edits = [] 

78 if line_range is None: 

79 line_range = range(0, len(lines)) 

80 for line_no in line_range: 

81 orig_line = lines[line_no] 

82 orig_len = len(orig_line) 

83 if orig_line.endswith("\n"): 83 ↛ 85line 83 didn't jump to line 85 because the condition on line 83 was always true

84 orig_len -= 1 

85 stripped_len = len(orig_line.rstrip()) 

86 if stripped_len == orig_len: 86 ↛ 89line 86 didn't jump to line 89 because the condition on line 86 was always true

87 continue 

88 

89 stripped_len_client_off = position_codec.client_num_units( 

90 orig_line[:stripped_len] 

91 ) 

92 orig_len_client_off = position_codec.client_num_units(orig_line[:orig_len]) 

93 edit_range = position_codec.range_to_client_units( 

94 lines, 

95 Range( 

96 Position( 

97 line_no + line_relative_line_no, 

98 stripped_len_client_off, 

99 ), 

100 Position( 

101 line_no + line_relative_line_no, 

102 orig_len_client_off, 

103 ), 

104 ), 

105 ) 

106 edits.append( 

107 TextEdit( 

108 edit_range, 

109 "", 

110 ) 

111 ) 

112 

113 return edits 

114 

115 

116class SemanticTokensState: 

117 __slots__ = ("ls", "doc", "lines", "tokens", "_previous_line", "_previous_col") 

118 

119 def __init__( 

120 self, 

121 ls: "DebputyLanguageServer", 

122 doc: "TextDocument", 

123 lines: list[str], 

124 tokens: list[int], 

125 ) -> None: 

126 self.ls = ls 

127 self.doc = doc 

128 self.lines = lines 

129 self.tokens = tokens 

130 self._previous_line = 0 

131 self._previous_col = 0 

132 

133 def emit_token( 

134 self, 

135 start_pos: Position, 

136 len_client_units: int, 

137 token_code: int, 

138 *, 

139 token_modifiers: int = 0, 

140 ) -> None: 

141 line_delta = start_pos.line - self._previous_line 

142 self._previous_line = start_pos.line 

143 previous_col = self._previous_col 

144 

145 if line_delta: 

146 previous_col = 0 

147 

148 column_delta = start_pos.character - previous_col 

149 self._previous_col = start_pos.character 

150 

151 tokens = self.tokens 

152 tokens.append(line_delta) # Line delta 

153 tokens.append(column_delta) # Token column delta 

154 tokens.append(len_client_units) # Token length 

155 tokens.append(token_code) 

156 tokens.append(token_modifiers)