diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py | 20 | ||||
-rw-r--r-- | src/debputy/filesystem_scan.py | 3 | ||||
-rw-r--r-- | src/debputy/linting/lint_impl.py | 94 | ||||
-rw-r--r-- | src/debputy/linting/lint_util.py | 12 | ||||
-rw-r--r-- | src/debputy/lsp/debputy_ls.py | 41 | ||||
-rw-r--r-- | src/debputy/lsp/lsp_debian_control_reference_data.py | 123 | ||||
-rw-r--r-- | src/debputy/lsp/lsp_generic_deb822.py | 141 | ||||
-rw-r--r-- | src/debputy/lsp/style_prefs.py | 33 |
8 files changed, 370 insertions, 97 deletions
diff --git a/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py b/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py index a5e1142..8189535 100644 --- a/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py +++ b/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py @@ -314,12 +314,24 @@ def lint_cmd(context: CommandContext) -> None: help='Enable or disable the "linter" convention of exiting with an error if issues were found', ), add_arg( - "--missing-style-is-ok", - dest="declared_style_required", + "--supported-style-is-required", + dest="supported_style_required", default=True, + action="store_true", + help="Fail with an error if a supported style cannot be identified.", + ), + add_arg( + "--unknown-or-unsupported-style-is-ok", + dest="supported_style_required", + action="store_false", + help="Do not exit with an error if no supported style can be identified. Useful for general" + ' pipelines to implement "reformat if possible"', + ), + add_arg( + "--missing-style-is-ok", + dest="supported_style_required", action="store_false", - help="Do not exit with an error if no style can be identified. Useful for general pipelines to implement" - ' "reformat if possible"', + help="[Deprecated] Use --unknown-or-unsupported-style-is-ok instead", ), ], ) diff --git a/src/debputy/filesystem_scan.py b/src/debputy/filesystem_scan.py index 0a18899..7b20040 100644 --- a/src/debputy/filesystem_scan.py +++ b/src/debputy/filesystem_scan.py @@ -1565,7 +1565,8 @@ class FSROOverlay(VirtualPathBase): parent: Optional["FSROOverlay"], ) -> None: self._path: str = path - self._fs_path: str = _normalize_path(fs_path, with_prefix=False) + prefix = "/" if fs_path.startswith("/") else "" + self._fs_path: str = prefix + _normalize_path(fs_path, with_prefix=False) self._parent: Optional[ReferenceType[FSROOverlay]] = ( ref(parent) if parent is not None else None ) diff --git a/src/debputy/linting/lint_impl.py b/src/debputy/linting/lint_impl.py index 8ce78c1..81ce0e9 100644 --- a/src/debputy/linting/lint_impl.py +++ b/src/debputy/linting/lint_impl.py @@ -6,7 +6,9 @@ import sys import textwrap from typing import Optional, List, Union, NoReturn, Mapping +from debputy.filesystem_scan import FSROOverlay from debputy.lsp.vendoring._deb822_repro import Deb822FileElement +from debputy.plugin.api import VirtualPath from debputy.yaml import MANIFEST_YAML, YAMLError from lsprotocol.types import ( CodeAction, @@ -83,16 +85,21 @@ REFORMAT_FORMATS = { class LintContext: plugin_feature_set: PluginProvidedFeatureSet style_preference_table: StylePreferenceTable + source_root: Optional[VirtualPath] + debian_dir: Optional[VirtualPath] parsed_deb822_file_content: Optional[Deb822FileElement] = None source_package: Optional[SourcePackage] = None binary_packages: Optional[Mapping[str, BinaryPackage]] = None effective_preference: Optional[EffectivePreference] = None + unsupported_preference_reason: Optional[str] = None salsa_ci: Optional[CommentedMap] = None def state_for(self, path: str, content: str, lines: List[str]) -> LintStateImpl: return LintStateImpl( self.plugin_feature_set, self.style_preference_table, + self.source_root, + self.debian_dir, path, content, lines, @@ -103,9 +110,15 @@ class LintContext: def gather_lint_info(context: CommandContext) -> LintContext: + source_root = FSROOverlay.create_root_dir(".", ".") + debian_dir = source_root.get("debian") + if debian_dir is not None and not debian_dir.is_dir: + debian_dir = None lint_context = LintContext( context.load_plugins(), StylePreferenceTable.load_styles(), + source_root, + debian_dir, ) try: with open("debian/control") as fd: @@ -131,11 +144,13 @@ def gather_lint_info(context: CommandContext) -> LintContext: except YAMLError: break if source_package is not None or salsa_ci_map is not None: - lint_context.effective_preference = determine_effective_style( + pref, pref_reason = determine_effective_style( lint_context.style_preference_table, source_package, salsa_ci_map, ) + lint_context.effective_preference = pref + lint_context.unsupported_preference_reason = pref_reason return lint_context @@ -207,44 +222,57 @@ def perform_reformat( lint_context.effective_preference = style if lint_context.effective_preference is None: - print( - textwrap.dedent( - """\ - You can enable set a style by doing either of: - - * You can set `X-Style: black` in the source stanza of `debian/control` to pick - `black` as the preferred style for this package. - - Note: `black` is an opinionated style that follows the spirit of the `black` code formatter - for Python. - - If you use `pre-commit`, then there is a formatting hook at - https://salsa.debian.org/debian/debputy-pre-commit-hooks - - * If you use the Debian Salsa CI pipeline, then you can set SALSA_CI_DISABLE_WRAP_AND_SORT - to a truth value and `debputy` will pick up the configuration from there. - - Note: The option must be in `.gitlab-ci.yml` or `debian/salsa-ci.yml` to work. The Salsa CI - pipeline will use `wrap-and-sort` while `debputy` uses its own emulation of `wrap-and-sort` - (`debputy` also needs to apply the style via `debputy lsp server`). - - * The `debputy` code also comes with a built-in style database. This may be interesting for - packaging teams, so set a default team style that applies to all packages maintained by - that packaging team. - - Individuals can also add their style, which can useful for ad-hoc packaging teams, where - `debputy` will automatically apply a style if *all* co-maintainers agree to it. - - Note the above list is an ordered list of how `debputy` determines which style to use in case - multiple options are available. - """ + if lint_context.unsupported_preference_reason is not None: + _warn( + "While `debputy` could identify a formatting for this package, it does not support it." ) - ) - if parsed_args.declared_style_required: - _error( - "Sorry; `debputy` does not know which style to use for this package." + _warn(f"{lint_context.unsupported_preference_reason}") + if parsed_args.supported_style_required: + _error( + "Sorry; `debputy` does not support the style. Use --unknown-or-unsupported-style-is-ok to make" + " this a non-error." + ) + else: + print( + textwrap.dedent( + """\ + You can enable set a style by doing either of: + + * You can set `X-Style: black` in the source stanza of `debian/control` to pick + `black` as the preferred style for this package. + - Note: `black` is an opinionated style that follows the spirit of the `black` code formatter + for Python. + - If you use `pre-commit`, then there is a formatting hook at + https://salsa.debian.org/debian/debputy-pre-commit-hooks + + * If you use the Debian Salsa CI pipeline, then you can set SALSA_CI_DISABLE_WRAP_AND_SORT + to a truth value and `debputy` will pick up the configuration from there. + - Note: The option must be in `.gitlab-ci.yml` or `debian/salsa-ci.yml` to work. The Salsa CI + pipeline will use `wrap-and-sort` while `debputy` uses its own emulation of `wrap-and-sort` + (`debputy` also needs to apply the style via `debputy lsp server`). + + * The `debputy` code also comes with a built-in style database. This may be interesting for + packaging teams, so set a default team style that applies to all packages maintained by + that packaging team. + - Individuals can also add their style, which can useful for ad-hoc packaging teams, where + `debputy` will automatically apply a style if *all* co-maintainers agree to it. + + Note the above list is an ordered list of how `debputy` determines which style to use in case + multiple options are available. + """ + ) ) + if parsed_args.supported_style_required: + _error( + "Sorry; `debputy` does not know which style to use for this package. Please either set a" + "style or use --unknown-or-unsupported-style-is-ok to make this a non-error" + ) _info("") _info( - "Doing nothing since no style could be identified as requested." + "Doing nothing since no supported style could be identified as requested." " See above how to set a style." ) + _info("Use --supported-style-is-required if this should be an error instead.") sys.exit(0) changes = False diff --git a/src/debputy/linting/lint_util.py b/src/debputy/linting/lint_util.py index 02b8804..78e9f9a 100644 --- a/src/debputy/linting/lint_util.py +++ b/src/debputy/linting/lint_util.py @@ -5,8 +5,10 @@ from typing import List, Optional, Callable, Counter, TYPE_CHECKING, Mapping, Se from lsprotocol.types import Position, Range, Diagnostic, DiagnosticSeverity, TextEdit from debputy.commands.debputy_cmd.output import OutputStylingBase +from debputy.filesystem_scan import VirtualPathBase from debputy.lsp.vendoring._deb822_repro import Deb822FileElement, parse_deb822_file from debputy.packages import SourcePackage, BinaryPackage +from debputy.plugin.api import VirtualPath from debputy.plugin.api.feature_set import PluginProvidedFeatureSet from debputy.util import _DEFAULT_LOGGER, _warn @@ -33,6 +35,14 @@ class LintState: raise NotImplementedError @property + def source_root(self) -> Optional[VirtualPathBase]: + raise NotImplementedError + + @property + def debian_dir(self) -> Optional[VirtualPathBase]: + raise NotImplementedError + + @property def path(self) -> str: raise NotImplementedError @@ -73,6 +83,8 @@ class LintState: class LintStateImpl(LintState): plugin_feature_set: PluginProvidedFeatureSet = dataclasses.field(repr=False) style_preference_table: "StylePreferenceTable" = dataclasses.field(repr=False) + source_root: Optional[VirtualPathBase] + debian_dir: Optional[VirtualPathBase] path: str content: str lines: List[str] diff --git a/src/debputy/lsp/debputy_ls.py b/src/debputy/lsp/debputy_ls.py index c08fd18..7365491 100644 --- a/src/debputy/lsp/debputy_ls.py +++ b/src/debputy/lsp/debputy_ls.py @@ -13,6 +13,7 @@ from typing import ( from lsprotocol.types import MarkupKind +from debputy.filesystem_scan import FSROOverlay, VirtualPathBase from debputy.linting.lint_util import ( LintState, ) @@ -28,6 +29,7 @@ from debputy.packages import ( BinaryPackage, DctrlParser, ) +from debputy.plugin.api import VirtualPath from debputy.plugin.api.feature_set import PluginProvidedFeatureSet from debputy.util import _info from debputy.yaml import MANIFEST_YAML, YAMLError @@ -167,6 +169,7 @@ class LSProvidedLintState(LintState): self, ls: "DebputyLanguageServer", doc: "TextDocument", + source_root: str, debian_dir_path: str, dctrl_parser: DctrlParser, ) -> None: @@ -174,18 +177,29 @@ class LSProvidedLintState(LintState): self._doc = doc # Cache lines (doc.lines re-splits everytime) self._lines = doc.lines + self._source_root = FSROOverlay.create_root_dir(".", source_root) + debian_dir = self._source_root.get("debian") + if debian_dir is not None and not debian_dir.is_dir: + debian_dir = None + self._debian_dir = debian_dir dctrl_file = os.path.join(debian_dir_path, "control") - self._dctrl_cache: DctrlFileCache = DctrlFileCache( - from_fs_path(dctrl_file), - dctrl_file, - dctrl_parser=dctrl_parser, - ) + if dctrl_file != doc.path: - self._deb822_file: Deb822FileCache = Deb822FileCache( + self._dctrl_cache: DctrlFileCache = DctrlFileCache( from_fs_path(dctrl_file), dctrl_file, + dctrl_parser=dctrl_parser, + ) + self._deb822_file: Deb822FileCache = Deb822FileCache( + doc.uri, + doc.path, ) else: + self._dctrl_cache: DctrlFileCache = DctrlFileCache( + doc.uri, + doc.path, + dctrl_parser=dctrl_parser, + ) self._deb822_file = self._dctrl_cache self._salsa_ci_caches = [ @@ -205,6 +219,14 @@ class LSProvidedLintState(LintState): return self._doc.uri @property + def source_root(self) -> Optional[VirtualPathBase]: + return self._source_root + + @property + def debian_dir(self) -> Optional[VirtualPathBase]: + return self._debian_dir + + @property def path(self) -> str: return self._doc.path @@ -251,11 +273,12 @@ class LSProvidedLintState(LintState): salsa_ci = self._resolve_salsa_ci() if source_package is None and salsa_ci is None: return None - return determine_effective_style( + style, _ = determine_effective_style( self.style_preference_table, source_package, salsa_ci, ) + return style @property def style_preference_table(self) -> StylePreferenceTable: @@ -343,7 +366,9 @@ class DebputyLanguageServer(LanguageServer): while dir_path and dir_path != "/" and os.path.basename(dir_path) != "debian": dir_path = os.path.dirname(dir_path) - return LSProvidedLintState(self, doc, dir_path, self.dctrl_parser) + source_root = os.path.dirname(dir_path) + + return LSProvidedLintState(self, doc, source_root, dir_path, self.dctrl_parser) @property def _client_hover_markup_formats(self) -> Optional[List[MarkupKind]]: diff --git a/src/debputy/lsp/lsp_debian_control_reference_data.py b/src/debputy/lsp/lsp_debian_control_reference_data.py index 1d4628c..bd2a43d 100644 --- a/src/debputy/lsp/lsp_debian_control_reference_data.py +++ b/src/debputy/lsp/lsp_debian_control_reference_data.py @@ -25,6 +25,8 @@ from typing import ( Sequence, ) +from debputy.filesystem_scan import VirtualPathBase +from debputy.linting.lint_util import LintState from debputy.lsp.vendoring._deb822_repro.types import TE from debian.debian_support import DpkgArchTable from lsprotocol.types import ( @@ -38,6 +40,7 @@ from lsprotocol.types import ( MarkupContent, CompletionItemTag, MarkupKind, + CompletionItemKind, ) from debputy.lsp.diagnostics import DiagnosticData @@ -71,6 +74,7 @@ from debputy.lsp.vendoring._deb822_repro.parsing import ( LIST_UPLOADERS_INTERPRETATION, _parse_whitespace_list_value, parse_deb822_file, + Deb822ParsedTokenList, ) from debputy.lsp.vendoring._deb822_repro.tokens import ( Deb822FieldNameToken, @@ -82,7 +86,8 @@ from debputy.lsp.vendoring._deb822_repro.tokens import ( ) from debputy.lsp.vendoring._deb822_repro.types import FormatterCallback from debputy.lsp.vendoring.wrap_and_sort import _sort_packages_key -from debputy.util import PKGNAME_REGEX +from debputy.plugin.api import VirtualPath +from debputy.util import PKGNAME_REGEX, _info try: from debputy.lsp.vendoring._deb822_repro.locatable import ( @@ -759,9 +764,9 @@ class FieldValueClass(Enum): COMMA_SEPARATED_EMAIL_LIST = auto(), LIST_UPLOADERS_INTERPRETATION COMMA_OR_SPACE_SEPARATED_LIST = auto(), LIST_COMMA_OR_SPACE_SEPARATED_INTERPRETATION FREE_TEXT_FIELD = auto(), None - DEP5_FILE_LIST = auto(), None # TODO + DEP5_FILE_LIST = auto(), LIST_SPACE_SEPARATED_INTERPRETATION - def interpreter(self) -> Optional[Interpretation[Any]]: + def interpreter(self) -> Optional[Interpretation[Deb822ParsedTokenList[Any, Any]]]: return self.value[1] @@ -809,6 +814,36 @@ def _unknown_value_check( return known_value, message, severity, fix_data +def _dep5_escape_path(path: str) -> str: + return path.replace(" ", "?") + + +def _noop_escape_path(path: str) -> str: + return path + + +def _should_ignore_dir( + path: VirtualPath, + *, + supports_dir_match: bool = False, + match_non_persistent_paths: bool = False, +) -> bool: + if not supports_dir_match and not any(path.iterdir): + return True + cachedir_tag = path.get("CACHEDIR.TAG") + if ( + not match_non_persistent_paths + and cachedir_tag is not None + and cachedir_tag.is_file + ): + # https://bford.info/cachedir/ + with cachedir_tag.open(byte_io=True, buffering=64) as fd: + start = fd.read(43) + if start == b"Signature: 8a477f597d28d172789f06886806bc55": + return True + return False + + @dataclasses.dataclass(slots=True, frozen=True) class Deb822KnownField: name: str @@ -844,6 +879,7 @@ class Deb822KnownField: def complete_field( self, + lint_state: LintState, stanza_parts: Sequence[Deb822ParagraphElement], markdown_kind: MarkupKind, ) -> Optional[CompletionItem]: @@ -852,7 +888,9 @@ class Deb822KnownField: name = self.name complete_as = name + ": " options = self.value_options_for_completer( + lint_state, stanza_parts, + "", is_completion_for_field=True, ) if options is not None and len(options) == 1: @@ -883,13 +921,92 @@ class Deb822KnownField: documentation=doc, ) + def _complete_files( + self, + base_dir: Optional[VirtualPathBase], + value_being_completed: str, + *, + is_dep5_file_list: bool = False, + supports_dir_match: bool = False, + supports_spaces_in_filename: bool = False, + match_non_persistent_paths: bool = False, + ) -> Optional[Sequence[CompletionItem]]: + _info(f"_complete_files: {base_dir.fs_path} - {value_being_completed!r}") + if base_dir is None or not base_dir.is_dir: + return None + + if is_dep5_file_list: + supports_spaces_in_filename = True + supports_dir_match = False + match_non_persistent_paths = False + + if value_being_completed == "": + current_dir = base_dir + unmatched_parts: Sequence[str] = tuple() + else: + current_dir, unmatched_parts = base_dir.attempt_lookup( + value_being_completed + ) + + if len(unmatched_parts) > 1: + # Unknown directory part / glob, and we currently do not deal with that. + return None + if len(unmatched_parts) == 1 and unmatched_parts[0] == "*": + # Avoid convincing the client to remove the star (seen with emacs) + return None + items = [] + + path_escaper = _dep5_escape_path if is_dep5_file_list else _noop_escape_path + + for child in current_dir.iterdir: + if child.is_symlink and is_dep5_file_list: + continue + if not supports_spaces_in_filename and ( + " " in child.name or "\t" in child.name + ): + continue + if child.is_dir: + if _should_ignore_dir( + child, + supports_dir_match=supports_dir_match, + match_non_persistent_paths=match_non_persistent_paths, + ): + continue + items.append( + CompletionItem( + f"{child.path}/", + insert_text=path_escaper(f"{child.path}/"), + kind=CompletionItemKind.Folder, + ) + ) + else: + items.append( + CompletionItem( + child.path, + insert_text=path_escaper(child.path), + kind=CompletionItemKind.File, + ) + ) + return items + def value_options_for_completer( self, + lint_state: LintState, stanza_parts: Sequence[Deb822ParagraphElement], + value_being_completed: str, *, is_completion_for_field: bool = False, ) -> Optional[Sequence[CompletionItem]]: known_values = self.known_values + if self.field_value_class == FieldValueClass.DEP5_FILE_LIST: + if is_completion_for_field: + return None + return self._complete_files( + lint_state.source_root, + value_being_completed, + is_dep5_file_list=True, + ) + if known_values is None: return None if is_completion_for_field and ( diff --git a/src/debputy/lsp/lsp_generic_deb822.py b/src/debputy/lsp/lsp_generic_deb822.py index 6d44d1a..5b1a22a 100644 --- a/src/debputy/lsp/lsp_generic_deb822.py +++ b/src/debputy/lsp/lsp_generic_deb822.py @@ -6,7 +6,6 @@ from typing import ( Union, Sequence, Tuple, - Set, Any, Container, List, @@ -21,7 +20,6 @@ from lsprotocol.types import ( CompletionList, CompletionItem, Position, - CompletionItemTag, MarkupContent, Hover, MarkupKind, @@ -72,27 +70,41 @@ except ImportError: _CONTAINS_SPACE_OR_COLON = re.compile(r"[\s:]") -def in_range(te_range: TERange, cursor_position: Position) -> bool: +def in_range( + te_range: TERange, + cursor_position: Position, + *, + inclusive_end: bool = False, +) -> bool: + cursor_line = cursor_position.line start_pos = te_range.start_pos end_pos = te_range.end_pos - if start_pos.line_position < cursor_position.line < end_pos.line_position: - return True - if ( - cursor_position.line == start_pos.line_position - and cursor_position.character >= start_pos.cursor_position - ): - return True + if cursor_line < start_pos.line_position or cursor_line > end_pos.line_position: + return False + + if start_pos.line_position == end_pos.line_position: + start_col = start_pos.cursor_position + cursor_col = cursor_position.character + end_col = end_pos.cursor_position + if inclusive_end: + return start_col <= cursor_col <= end_col + return start_col <= cursor_col < end_col + + if cursor_line == end_pos.line_position: + return cursor_position.character < end_pos.cursor_position + return ( - cursor_position.line == end_pos.line_position - and cursor_position.character < end_pos.cursor_position + cursor_line > start_pos.line_position + or start_pos.cursor_position <= cursor_position.character ) def _field_at_position( stanza: Deb822ParagraphElement, + stanza_metadata: S, stanza_range: TERange, position: Position, -) -> Tuple[Optional[Deb822KeyValuePairElement], bool]: +) -> Tuple[Optional[Deb822KeyValuePairElement], Optional[F], str, bool]: te_range = TERange(stanza_range.start_pos, stanza_range.start_pos) for token_or_element in stanza.iter_parts(): te_range = token_or_element.size().relative_to(te_range.end_pos) @@ -102,9 +114,27 @@ def _field_at_position( value_range = token_or_element.value_element.range_in_parent().relative_to( te_range.start_pos ) + known_field = stanza_metadata.get(token_or_element.field_name) in_value = in_range(value_range, position) - return token_or_element, in_value - return None, False + interpreter = ( + known_field.field_value_class.interpreter() + if known_field is not None + else None + ) + matched_value = "" + if in_value and interpreter is not None: + interpreted = token_or_element.interpret_as(interpreter) + for value_ref in interpreted.iter_value_references(): + value_token_range = ( + value_ref.locatable.range_in_parent().relative_to( + value_range.start_pos + ) + ) + if in_range(value_token_range, position, inclusive_end=True): + matched_value = value_ref.value + break + return token_or_element, known_field, matched_value, in_value + return None, None, "", False def _allow_stanza_continuation( @@ -123,11 +153,20 @@ def _allow_stanza_continuation( def _at_cursor( deb822_file: Deb822FileElement, + file_metadata: Deb822FileMetadata[S], doc: "TextDocument", lines: List[str], client_position: Position, is_completion: bool = False, -) -> Tuple[Position, Optional[str], str, bool, int, Iterable[Deb822ParagraphElement]]: +) -> Tuple[ + Position, + Optional[str], + str, + bool, + Optional[S], + Optional[F], + Iterable[Deb822ParagraphElement], +]: server_position = doc.position_codec.position_from_client_units( lines, client_position, @@ -144,6 +183,9 @@ def _at_cursor( file_iter = iter(deb822_file.iter_parts()) matched_token: Optional[TokenOrElement] = None matched_field: Optional[str] = None + stanza_metadata: Optional[S] = None + known_field: Optional[F] = None + for token_or_element in file_iter: te_range = token_or_element.size().relative_to(te_range.end_pos) if isinstance(token_or_element, Deb822ParagraphElement): @@ -155,8 +197,14 @@ def _at_cursor( continue matched_token = token_or_element if isinstance(token_or_element, Deb822ParagraphElement): - kvpair, in_value = _field_at_position( - token_or_element, te_range, server_position + stanza_metadata = file_metadata.guess_stanza_classification_by_idx( + paragraph_no + ) + kvpair, known_field, current_word, in_value = _field_at_position( + token_or_element, + stanza_metadata, + te_range, + server_position, ) if kvpair is not None: matched_field = kvpair.field_name @@ -172,12 +220,18 @@ def _at_cursor( stanza_parts = (p for p in (previous_stanza, next_stanza) if p is not None) + if stanza_metadata is None and is_completion: + if paragraph_no < 0: + paragraph_no = 0 + stanza_metadata = file_metadata.guess_stanza_classification_by_idx(paragraph_no) + return ( server_position, matched_field, current_word, in_value, - paragraph_no, + stanza_metadata, + known_field, stanza_parts, ) @@ -189,7 +243,8 @@ def deb822_completer( ) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]: doc = ls.workspace.get_text_document(params.text_document.uri) lines = doc.lines - deb822_file = ls.lint_state(doc).parsed_deb822_file_content + lint_state = ls.lint_state(doc) + deb822_file = lint_state.parsed_deb822_file_content if deb822_file is None: _warn("The deb822 result missing failed!?") ls.show_message_log( @@ -197,29 +252,40 @@ def deb822_completer( ) return None - _a, current_field, _b, in_value, paragraph_no, matched_stanzas = _at_cursor( + ( + _a, + current_field, + word_at_position, + in_value, + stanza_metadata, + known_field, + matched_stanzas, + ) = _at_cursor( deb822_file, + file_metadata, doc, lines, params.position, is_completion=True, ) - stanza_metadata = file_metadata.guess_stanza_classification_by_idx(paragraph_no) - items: Optional[Sequence[CompletionItem]] if in_value: - _info(f"Completion for field value {current_field}") - if current_field is None: - return None - known_field: Deb822KnownField = stanza_metadata.get(current_field) + _info(f"Completion for field value {current_field} -- {word_at_position}") if known_field is None: return None - items = known_field.value_options_for_completer(list(matched_stanzas)) + value_being_completed = word_at_position + items = known_field.value_options_for_completer( + lint_state, + list(matched_stanzas), + value_being_completed, + ) else: _info("Completing field name") + assert stanza_metadata is not None items = _complete_field_name( ls, + lint_state, stanza_metadata, matched_stanzas, ) @@ -261,17 +327,21 @@ def deb822_hover( ) return None - server_pos, current_field, word_at_position, in_value, paragraph_no, _ = _at_cursor( + ( + server_pos, + current_field, + word_at_position, + in_value, + _, + known_field, + _, + ) = _at_cursor( deb822_file, + file_metadata, doc, lines, params.position, ) - stanza_metadata = file_metadata.guess_stanza_classification_by_idx(paragraph_no) - - known_field = ( - stanza_metadata.get(current_field) if current_field is not None else None - ) hover_text = None if custom_handler is not None: res = custom_handler( @@ -579,6 +649,7 @@ def deb822_semantic_tokens_full( def _complete_field_name( ls: "DebputyLanguageServer", + lint_state: LintState, fields: StanzaMetadata[Any], matched_stanzas: Iterable[Deb822ParagraphElement], ) -> Sequence[CompletionItem]: @@ -603,7 +674,7 @@ def _complete_field_name( for cand_key, cand in fields.items(): if cand_key.lower() in seen_fields: continue - item = cand.complete_field(matched_stanzas, markdown_kind) + item = cand.complete_field(lint_state, matched_stanzas, markdown_kind) if item is not None: items.append(item) return items diff --git a/src/debputy/lsp/style_prefs.py b/src/debputy/lsp/style_prefs.py index 40d850b..755e67c 100644 --- a/src/debputy/lsp/style_prefs.py +++ b/src/debputy/lsp/style_prefs.py @@ -16,6 +16,7 @@ from typing import ( Dict, Iterable, Any, + Tuple, ) from debputy.lsp.lsp_reference_keyword import ALL_PUBLIC_NAMED_STYLES @@ -568,12 +569,12 @@ def determine_effective_style( style_preference_table: StylePreferenceTable, source_package: Optional[SourcePackage], salsa_ci: Optional[CommentedMap], -) -> Optional[EffectivePreference]: +) -> Tuple[Optional[EffectivePreference], Optional[str]]: style = source_package.fields.get("X-Style") if source_package is not None else None if style is not None: if style not in ALL_PUBLIC_NAMED_STYLES: - return None - return style_preference_table.named_styles.get(style) + return None, "X-Style contained an unknown/unsupported style" + return style_preference_table.named_styles.get(style), None if salsa_ci: disable_wrap_and_sort = salsa_ci.mlget( @@ -599,29 +600,35 @@ def determine_effective_style( if wrap_and_sort_options is None: wrap_and_sort_options = "" elif not isinstance(wrap_and_sort_options, str): - return None - return parse_salsa_ci_wrap_and_sort_args(wrap_and_sort_options) + return None, "The salsa-ci had a non-string option for wrap-and-sort" + detected_style = parse_salsa_ci_wrap_and_sort_args(wrap_and_sort_options) + if detected_style is None: + msg = "One or more of the wrap-and-sort options in the salsa-ci file was not supported" + else: + msg = None + return detected_style, msg if source_package is None: - return None + return None, None maint = source_package.fields.get("Maintainer") if maint is None: - return None + return None, None maint_email = extract_maint_email(maint) maint_style = style_preference_table.maintainer_preferences.get(maint_email) # Special-case "@packages.debian.org" when missing, since they are likely to be "ad-hoc" # teams that will not be registered. In that case, we fall back to looking at the uploader # preferences as-if the maintainer had not been listed at all. if maint_style is None and not maint_email.endswith("@packages.debian.org"): - return None + return None, None if maint_style is not None and maint_style.is_packaging_team: # When the maintainer is registered as a packaging team, then we assume the packaging # team's style applies unconditionally. - return maint_style.as_effective_pref() + return maint_style.as_effective_pref(), None uploaders = source_package.fields.get("Uploaders") if uploaders is None: - return maint_style.as_effective_pref() if maint_style is not None else None + detected_style = maint_style.as_effective_pref() if maint_style is not None else None + return detected_style, None all_styles: List[Optional[EffectivePreference]] = [] if maint_style is not None: all_styles.append(maint_style) @@ -633,11 +640,11 @@ def determine_effective_style( all_styles.append(uploader_style) if not all_styles: - return None + return None, None r = functools.reduce(EffectivePreference.aligned_preference, all_styles) if isinstance(r, MaintainerPreference): - return r.as_effective_pref() - return r + return r.as_effective_pref(), None + return r, None def _split_options(args: Iterable[str]) -> Iterable[str]: |