debputy.lsp.languages.lsp_debian_watch

src/debputy/lsp/languages/lsp_debian_watch.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
import dataclasses
import importlib.resources
import re
from functools import lru_cache
from typing import (
    Union,
    Optional,
    TYPE_CHECKING,
    List,
    Self,
)
from collections.abc import Sequence, Mapping

from debputy.linting.lint_util import LintState, with_range_in_continuous_parts
from debputy.lsp.debputy_ls import DebputyLanguageServer
from debputy.lsp.lsp_debian_control_reference_data import (
    DebianWatch5FileMetadata,
    Deb822KnownField,
)

import debputy.lsp.data.deb822_data as deb822_ref_data_dir
from debputy.lsp.lsp_features import (
    lint_diagnostics,
    lsp_completer,
    lsp_hover,
    lsp_standard_handler,
    lsp_folding_ranges,
    lsp_semantic_tokens_full,
    lsp_will_save_wait_until,
    lsp_format_document,
    SecondaryLanguage,
    LanguageDispatchRule,
    lsp_cli_reformat_document,
)
from debputy.lsp.lsp_generic_deb822 import (
    deb822_completer,
    deb822_hover,
    deb822_folding_ranges,
    deb822_semantic_tokens_full,
    deb822_format_file,
    scan_for_syntax_errors_and_token_level_diagnostics,
)
from debputy.lsp.lsp_reference_keyword import LSP_DATA_DOMAIN
from debputy.lsp.ref_models.deb822_reference_parse_models import (
    GenericVariable,
    GENERIC_VARIABLE_REFERENCE_DATA_PARSER,
)
from debputy.lsp.text_util import markdown_urlify
from debputy.lsp.vendoring._deb822_repro import (
    Deb822ParagraphElement,
)
from debputy.lsprotocol.types import (
    CompletionItem,
    CompletionList,
    CompletionParams,
    HoverParams,
    Hover,
    TEXT_DOCUMENT_CODE_ACTION,
    SemanticTokens,
    SemanticTokensParams,
    FoldingRangeParams,
    FoldingRange,
    WillSaveTextDocumentParams,
    TextEdit,
    DocumentFormattingParams,
)
from debputy.manifest_parser.util import AttributePath
from debputy.util import _info
from debputy.yaml import MANIFEST_YAML

try:
    from debputy.lsp.vendoring._deb822_repro.locatable import (
        Position as TEPosition,
        Range as TERange,
    )

    from pygls.server import LanguageServer
    from pygls.workspace import TextDocument
except ImportError:
    pass


if TYPE_CHECKING:
    import lsprotocol.types as types
else:
    import debputy.lsprotocol.types as types


_CONTAINS_SPACE_OR_COLON = re.compile(r"[\s:]")

_DISPATCH_RULE = LanguageDispatchRule.new_rule(
    "debian/watch",
    None,
    "debian/watch",
    [
        # Presumably, emacs's name
        SecondaryLanguage("debian-watch"),
        # Presumably, vim's name
        SecondaryLanguage("debwatch"),
    ],
)

_DWATCH_FILE_METADATA = DebianWatch5FileMetadata()

lsp_standard_handler(_DISPATCH_RULE, TEXT_DOCUMENT_CODE_ACTION)


@dataclasses.dataclass(slots=True, frozen=True)
class VariableMetadata:
    name: str
    doc_uris: Sequence[str]
    synopsis: str
    description: str

    def render_metadata_fields(self) -> str:
        doc_uris = self.doc_uris
        parts = []
        if doc_uris:
            if len(doc_uris) == 1:
                parts.append(f"Documentation: {markdown_urlify(doc_uris[0])}")
            else:
                parts.append("Documentation:")
                parts.extend(f" - {markdown_urlify(uri)}" for uri in doc_uris)
        return "\n".join(parts)

    @classmethod
    def from_ref_data(cls, x: GenericVariable) -> "Self":
        doc = x.get("documentation", {})
        return cls(
            x["name"],
            doc.get("uris", []),
            doc.get("synopsis", ""),
            doc.get("long_description", ""),
        )


def dwatch_variables_metadata_basename() -> str:
    return "debian_watch_variables_data.yaml"


def _as_variables_metadata(
    args: list[VariableMetadata],
) -> Mapping[str, VariableMetadata]:
    r = {s.name: s for s in args}
    assert len(r) == len(args)
    return r


@lru_cache
def dwatch_variables_metadata() -> Mapping[str, VariableMetadata]:
    p = importlib.resources.files(deb822_ref_data_dir.__name__).joinpath(
        dwatch_variables_metadata_basename()
    )

    with p.open("r", encoding="utf-8") as fd:
        raw = MANIFEST_YAML.load(fd)

    attr_path = AttributePath.root_path(p)
    ref = GENERIC_VARIABLE_REFERENCE_DATA_PARSER.parse_input(raw, attr_path)
    return _as_variables_metadata(
        [VariableMetadata.from_ref_data(x) for x in ref["variables"]]
    )


def _custom_hover(
    ls: "DebputyLanguageServer",
    server_position: types.Position,
    _current_field: str | None,
    _word_at_position: str,
    _known_field: Deb822KnownField | None,
    in_value: bool,
    _doc: "TextDocument",
    lines: list[str],
) -> Hover | str | None:
    if not in_value:
        return None

    line_no = server_position.line
    line = lines[line_no]
    variable_search_ref = server_position.character
    variable = ""
    try:
        # Unlike ${} substvars where the start and end uses distinct characters, we cannot
        # know for certain whether we are at the start or end of a variable when we land
        # directly on a separator.
        try:
            variable_start = line.rindex("@", 0, variable_search_ref)
        except ValueError:
            if line[variable_search_ref] != "@":
                raise
            variable_start = variable_search_ref

        variable_end = line.index("@", variable_start + 1)
        if server_position.character <= variable_end:
            variable = line[variable_start : variable_end + 1]
    except (ValueError, IndexError):
        pass

    if variable != "" and variable != "@@":
        substvar_md = dwatch_variables_metadata().get(variable)

        if substvar_md is None:
            # In case of `@PACKAGE@-lin<CURSOR>ux-@ANY_VERSION@`
            return None
        doc = ls.translation(LSP_DATA_DOMAIN).pgettext(
            f"Variable:{substvar_md.name}",
            substvar_md.description,
        )
        md_fields = "\n" + substvar_md.render_metadata_fields()
        return f"# Variable `{variable}`\n\n{doc}{md_fields}"

    return None


@lsp_hover(_DISPATCH_RULE)
def _debian_watch_hover(
    ls: "DebputyLanguageServer",
    params: HoverParams,
) -> Hover | None:
    return deb822_hover(ls, params, _DWATCH_FILE_METADATA, custom_handler=_custom_hover)


@lsp_completer(_DISPATCH_RULE)
def _debian_watch_completions(
    ls: "DebputyLanguageServer",
    params: CompletionParams,
) -> CompletionList | Sequence[CompletionItem] | None:
    return deb822_completer(ls, params, _DWATCH_FILE_METADATA)


@lsp_folding_ranges(_DISPATCH_RULE)
def _debian_watch_folding_ranges(
    ls: "DebputyLanguageServer",
    params: FoldingRangeParams,
) -> Sequence[FoldingRange] | None:
    return deb822_folding_ranges(ls, params, _DWATCH_FILE_METADATA)


@lint_diagnostics(_DISPATCH_RULE)
async def _lint_debian_watch(lint_state: LintState) -> None:
    deb822_file = lint_state.parsed_deb822_file_content

    if not _DWATCH_FILE_METADATA.file_metadata_applies_to_file(deb822_file):
        return

    first_error = await scan_for_syntax_errors_and_token_level_diagnostics(
        deb822_file,
        lint_state,
    )
    header_stanza, source_stanza = _DWATCH_FILE_METADATA.stanza_types()
    stanza_no = 0

    async for stanza_range, stanza in lint_state.slow_iter(
        with_range_in_continuous_parts(deb822_file.iter_parts())
    ):
        if not isinstance(stanza, Deb822ParagraphElement):
            continue
        stanza_position = stanza_range.start_pos
        if stanza_position.line_position >= first_error:
            break
        stanza_no += 1
        is_source_stanza = stanza_no != 1
        if is_source_stanza:
            stanza_metadata = _DWATCH_FILE_METADATA.classify_stanza(
                stanza,
                stanza_no,
            )
            other_stanza_metadata = header_stanza
            other_stanza_name = "Header"
        elif "Version" in stanza:
            stanza_metadata = header_stanza
            other_stanza_metadata = source_stanza
            other_stanza_name = "Source"
        else:
            break

        await stanza_metadata.stanza_diagnostics(
            deb822_file,
            stanza,
            stanza_position,
            lint_state,
            confusable_with_stanza_name=other_stanza_name,
            confusable_with_stanza_metadata=other_stanza_metadata,
        )


@lsp_will_save_wait_until(_DISPATCH_RULE)
def _debian_watch_on_save_formatting(
    ls: "DebputyLanguageServer",
    params: WillSaveTextDocumentParams,
) -> Sequence[TextEdit] | None:
    doc = ls.workspace.get_text_document(params.text_document.uri)
    lint_state = ls.lint_state(doc)
    return deb822_format_file(lint_state, _DWATCH_FILE_METADATA)


@lsp_cli_reformat_document(_DISPATCH_RULE)
def _reformat_debian_watch(
    lint_state: LintState,
) -> Sequence[TextEdit] | None:
    return deb822_format_file(lint_state, _DWATCH_FILE_METADATA)


@lsp_format_document(_DISPATCH_RULE)
def _debian_watch_format_doc(
    ls: "DebputyLanguageServer",
    params: DocumentFormattingParams,
) -> Sequence[TextEdit] | None:
    doc = ls.workspace.get_text_document(params.text_document.uri)
    lint_state = ls.lint_state(doc)
    return deb822_format_file(lint_state, _DWATCH_FILE_METADATA)


@lsp_semantic_tokens_full(_DISPATCH_RULE)
async def _debian_watch_semantic_tokens_full(
    ls: "DebputyLanguageServer",
    request: SemanticTokensParams,
) -> SemanticTokens | None:
    return await deb822_semantic_tokens_full(
        ls,
        request,
        _DWATCH_FILE_METADATA,
    )