debputy.lsp.lsp_reference_keyword

src/debputy/lsp/lsp_reference_keyword.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
import dataclasses
import textwrap
from typing import (
    Optional,
    Union,
    Any,
    Self,
    TYPE_CHECKING,
)
from collections.abc import Mapping, Sequence, Callable, Iterable

from debputy.lsp.named_styles import ALL_PUBLIC_NAMED_STYLES
from debputy.lsp.ref_models.deb822_reference_parse_models import UsageHint
from debputy.lsp.vendoring._deb822_repro import Deb822ParagraphElement

if TYPE_CHECKING:
    import lsprotocol.types as types
    from debputy.linting.lint_util import LintState
    from debputy.lsp.debputy_ls import DebputyLanguageServer
else:
    import debputy.lsprotocol.types as types

LSP_DATA_DOMAIN = "debputy-lsp-data"


def format_comp_item_synopsis_doc(
    usage_hint: UsageHint | None,
    synopsis_doc: str | None,
    is_deprecated: bool,
) -> str:
    if is_deprecated:
        return (
            f"[OBSOLETE]: {synopsis_doc}"
            if synopsis_doc is not None and not synopsis_doc.isspace()
            else f"[OBSOLETE]"
        )
    if usage_hint is not None:
        return (
            f"[{usage_hint.upper()}]: {synopsis_doc}"
            if synopsis_doc is not None and not synopsis_doc.isspace()
            else f"[{usage_hint.upper()}]"
        )
    return synopsis_doc


@dataclasses.dataclass(slots=True, frozen=True)
class Keyword:
    value: str
    synopsis: str | None = None
    long_description: str | None = None
    translation_context: str = ""
    is_obsolete: bool = False
    replaced_by: str | None = None
    is_exclusive: bool = False
    """For keywords in fields that allow multiple keywords, the `is_exclusive` can be
    used for keywords that cannot be used with other keywords. As an example, the `all`
    value in `Architecture` of `debian/control` cannot be used with any other architecture.
    """
    is_alias_of: str | None = None
    is_completion_suggestion: bool = True
    sort_text: None | (
        str
        | Callable[["Keyword", "LintState", Sequence[Deb822ParagraphElement], str], str]
    ) = None
    usage_hint: UsageHint | None = None
    can_complete_keyword_in_stanza: None | (
        Callable[[Iterable[Deb822ParagraphElement]], bool]
    ) = None

    @property
    def is_deprecated(self) -> bool:
        return self.is_obsolete or self.replaced_by is not None

    def resolve_sort_text(
        self,
        lint_state: "LintState",
        stanza_parts: Sequence[Deb822ParagraphElement],
        value_being_completed: str,
    ) -> str | None:
        sort_text = self.sort_text
        if sort_text is None:
            return None
        if not isinstance(sort_text, str):
            return sort_text(
                self,
                lint_state,
                stanza_parts,
                value_being_completed,
            )
        return sort_text

    def is_keyword_valid_completion_in_stanza(
        self,
        stanza_parts: Sequence[Deb822ParagraphElement],
    ) -> bool:
        return (
            self.can_complete_keyword_in_stanza is None
            or self.can_complete_keyword_in_stanza(stanza_parts)
        )

    def replace(self, **changes: Any) -> "Self":
        return dataclasses.replace(self, **changes)

    def synopsis_translated(
        self, translation_provider: Union["DebputyLanguageServer", "LintState"]
    ) -> str:
        return translation_provider.translation(LSP_DATA_DOMAIN).pgettext(
            self.translation_context,
            self.synopsis,
        )

    def long_description_translated(
        self, translation_provider: Union["DebputyLanguageServer", "LintState"]
    ) -> str:
        return translation_provider.translation(LSP_DATA_DOMAIN).pgettext(
            self.translation_context,
            self.long_description,
        )

    def as_completion_item(
        self,
        lint_state: "LintState",
        stanza_parts: Sequence[Deb822ParagraphElement],
        value_being_completed: str,
        markdown_kind: types.MarkupKind,
    ) -> types.CompletionItem:
        return types.CompletionItem(
            self.value,
            insert_text=self.value if self.is_alias_of is None else self.is_alias_of,
            sort_text=self.resolve_sort_text(
                lint_state,
                stanza_parts,
                value_being_completed,
            ),
            detail=format_comp_item_synopsis_doc(
                self.usage_hint,
                self.synopsis_translated(lint_state),
                self.is_deprecated,
            ),
            deprecated=self.is_deprecated,
            tags=[types.CompletionItemTag.Deprecated] if self.is_deprecated else None,
            documentation=(
                types.MarkupContent(value=self.long_description, kind=markdown_kind)
                if self.long_description
                else None
            ),
        )


def allowed_values(*values: str | Keyword) -> Mapping[str, Keyword]:
    as_keywords = [k if isinstance(k, Keyword) else Keyword(k) for k in values]
    as_mapping = {k.value: k for k in as_keywords if k.value}
    # Simple bug check
    assert len(as_keywords) == len(as_mapping)
    return as_mapping


# This is the set of styles that `debputy` explicitly supports, which is more narrow than
# the ones in the config file.
ALL_PUBLIC_NAMED_STYLES_AS_KEYWORDS = allowed_values(
    Keyword(s.name, long_description=s.long_description)
    for s in ALL_PUBLIC_NAMED_STYLES
)