summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py20
-rw-r--r--src/debputy/filesystem_scan.py3
-rw-r--r--src/debputy/linting/lint_impl.py94
-rw-r--r--src/debputy/linting/lint_util.py12
-rw-r--r--src/debputy/lsp/debputy_ls.py41
-rw-r--r--src/debputy/lsp/lsp_debian_control_reference_data.py123
-rw-r--r--src/debputy/lsp/lsp_generic_deb822.py141
-rw-r--r--src/debputy/lsp/style_prefs.py33
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]: