summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml73
-rw-r--r--debputy.pod15
-rw-r--r--src/debputy/commands/debputy_cmd/context.py2
-rw-r--r--src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py8
-rw-r--r--src/debputy/linting/lint_impl.py91
-rw-r--r--src/debputy/linting/lint_util.py18
-rw-r--r--src/debputy/lsp/debputy_ls.py208
-rw-r--r--src/debputy/lsp/lsp_debian_changelog.py10
-rw-r--r--src/debputy/lsp/lsp_debian_control.py52
-rw-r--r--src/debputy/lsp/lsp_debian_control_reference_data.py141
-rw-r--r--src/debputy/lsp/lsp_debian_copyright.py15
-rw-r--r--src/debputy/lsp/lsp_debian_rules.py2
-rw-r--r--src/debputy/lsp/lsp_debian_tests_control.py9
-rw-r--r--src/debputy/lsp/lsp_dispatch.py33
-rw-r--r--src/debputy/lsp/lsp_generic_deb822.py275
-rw-r--r--src/debputy/lsp/lsp_reference_keyword.py16
-rw-r--r--src/debputy/lsp/quickfixes.py2
-rw-r--r--src/debputy/lsp/style_prefs.py96
-rw-r--r--src/debputy/packages.py49
-rw-r--r--tests/lint_tests/lint_tutil.py2
-rw-r--r--tests/lsp_tests/test_lsp_dctrl.py162
-rw-r--r--tests/test_style.py122
22 files changed, 1095 insertions, 306 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b3d32b2..c0c5ccf 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -7,32 +7,43 @@ include:
stages:
- ci-test
+ - ci-os-support
- aggregate-coverage
- pages
+ - os-build-tests
- provisioning
- build
- publish
- test
tests-testing:
- stage: ci-test
- image: debian:testing
+ stage: os-build-tests
+ image: debian:testing-slim
script:
- apt-get update
- apt-get build-dep -y .
- dpkg-buildpackage -us -uc -tc
tests-unstable:
- stage: ci-test
- image: debian:unstable
+ stage: os-build-tests
+ image: debian:unstable-slim
script:
- apt-get update
- apt-get build-dep -Ppkg.debputy.ci -y .
- dpkg-buildpackage -Ppkg.debputy.ci -us -uc -tc
+tests-ubuntu-noble:
+ stage: os-build-tests
+ image: ubuntu:noble
+ script:
+ - apt-get update
+ - apt-get build-dep -Ppkg.debputy.ci -y .
+ - dpkg-buildpackage -Ppkg.debputy.ci -us -uc -tc
+
+
code-lint-mypy:
stage: ci-test
- image: debian:unstable
+ image: debian:unstable-slim
script:
- apt-get update
- apt-get build-dep -Ppkg.debputy.ci -y .
@@ -53,12 +64,11 @@ code-lint-mypy:
tests-unstable-coverage-without-optional-bd:
stage: ci-test
- image: debian:unstable
+ image: debian:unstable-slim
script:
- apt-get update
- apt-get build-dep -Ppkg.debputy.minimal-tests,pkg.debputy.test-coverage -y .
- py.test-3 -v --cov --cov-branch --doctest-modules --junit-xml=xunit-report.xml --cov-report xml:coverage.xml
- - dpkg-buildpackage -Ppkg.debputy.minimal-tests -us -uc -tc
after_script:
- mkdir -p coverage-results/tests-unstable-coverage-without-optional-bd
- cp .coverage coverage-results/tests-unstable-coverage-without-optional-bd/coverage
@@ -73,7 +83,7 @@ tests-unstable-coverage-without-optional-bd:
tests-unstable-coverage:
stage: ci-test
- image: debian:unstable
+ image: debian:unstable-slim
script:
- apt-get update
- apt-get build-dep -Ppkg.debputy.test-coverage -y .
@@ -109,9 +119,50 @@ tests-unstable-coverage-with-extra-bd:
coverage_format: cobertura
path: coverage.xml
+
+tests-ubuntu-noble-coverage-with-extra-bd:
+ stage: ci-os-support
+ image: ubuntu:noble
+ script:
+ - apt-get update
+ - apt-get build-dep -Ppkg.debputy.ci,pkg.debputy.test-coverage -y .
+ - py.test-3 -v --cov --cov-branch --doctest-modules --junit-xml=xunit-report.xml --cov-report xml:coverage.xml
+ after_script:
+ - mkdir -p coverage-results/tests-ubuntu-noble-coverage-with-extra-bd
+ - cp .coverage coverage-results/tests-ubuntu-noble-coverage-with-extra-bd/coverage
+ artifacts:
+ paths:
+ - coverage-results/tests-ubuntu-noble-coverage-with-extra-bd
+ reports:
+ junit: xunit-report.xml
+ coverage_report:
+ coverage_format: cobertura
+ path: coverage.xml
+
+
+tests-ubuntu-noble-coverage-without-optional-bd:
+ stage: ci-os-support
+ image: ubuntu:noble
+ script:
+ - apt-get update
+ - apt-get build-dep -Ppkg.debputy.minimal-tests,pkg.debputy.test-coverage -y .
+ - py.test-3 -v --cov --cov-branch --doctest-modules --junit-xml=xunit-report.xml --cov-report xml:coverage.xml
+ after_script:
+ - mkdir -p coverage-results/tests-ubuntu-noble-coverage-without-optional-bd
+ - cp .coverage coverage-results/tests-ubuntu-noble-coverage-without-optional-bd/coverage
+ artifacts:
+ paths:
+ - coverage-results/tests-ubuntu-noble-coverage-without-optional-bd
+ reports:
+ junit: xunit-report.xml
+ coverage_report:
+ coverage_format: cobertura
+ path: coverage.xml
+
+
aggregate-coverage:
stage: aggregate-coverage
- image: debian:unstable
+ image: debian:unstable-slim
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
script:
- apt-get update -y
@@ -126,6 +177,8 @@ aggregate-coverage:
- tests-unstable-coverage
- tests-unstable-coverage-without-optional-bd
- tests-unstable-coverage-with-extra-bd
+ - tests-ubuntu-noble-coverage-with-extra-bd
+ - tests-ubuntu-noble-coverage-without-optional-bd
pages:
stage: pages
@@ -149,7 +202,7 @@ variables:
debputy-reformat:
stage: ci-test
- image: debian:sid-slim
+ image: debian:unstable-slim
script:
- apt-get update -qq && apt-get -qq build-dep --no-install-recommends --yes .
- ./debputy.sh reformat --linter-exit-code --no-auto-fix
diff --git a/debputy.pod b/debputy.pod
index 0cee740..c53fa23 100644
--- a/debputy.pod
+++ b/debputy.pod
@@ -171,6 +171,21 @@ providing the results.
If you rely on the exit code, you are recommended to explicitly pass the relevant variant of the
flag even if the current default matches your wishes.
+=item B<--missing-style-is-ok>
+
+By default, B<debputy reformat> will exit with an error when it cannot determine which style to
+use. This is generally what you want for "per package" CI or other lint checks to inform you that
+the reformatting will not work.
+
+However, for "cross package" pipelines, like the default Debian Salsa CI pipeline, having
+B<debputy reformat> automatically work when a style is set and doing nothing otherwise is
+preferable, since the pipeline can then provide a B<debputy reformat> job for all consumers
+without worrying about breaking their pipelines.
+
+It can also be useful for scripts or automated mass-edits where you want B<debputy> to fixup
+the changes you did if there is a known style without being hampered by the packages that
+have no known style.
+
=back
=item lint
diff --git a/src/debputy/commands/debputy_cmd/context.py b/src/debputy/commands/debputy_cmd/context.py
index 4d28408..29bc573 100644
--- a/src/debputy/commands/debputy_cmd/context.py
+++ b/src/debputy/commands/debputy_cmd/context.py
@@ -262,7 +262,7 @@ class CommandContext:
os.path.join(self.debian_dir.fs_path, "control"),
)
with debian_control.open() as fd:
- source_package, binary_packages = (
+ _, source_package, binary_packages = (
self.dctrl_parser.parse_source_debian_control(
fd,
)
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 ea97665..a5e1142 100644
--- a/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py
+++ b/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py
@@ -313,6 +313,14 @@ def lint_cmd(context: CommandContext) -> None:
action=BooleanOptionalAction,
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",
+ default=True,
+ 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"',
+ ),
],
)
def reformat_cmd(context: CommandContext) -> None:
diff --git a/src/debputy/linting/lint_impl.py b/src/debputy/linting/lint_impl.py
index c4e9310..8ce78c1 100644
--- a/src/debputy/linting/lint_impl.py
+++ b/src/debputy/linting/lint_impl.py
@@ -3,8 +3,11 @@ import os
import stat
import subprocess
import sys
+import textwrap
from typing import Optional, List, Union, NoReturn, Mapping
+from debputy.lsp.vendoring._deb822_repro import Deb822FileElement
+from debputy.yaml import MANIFEST_YAML, YAMLError
from lsprotocol.types import (
CodeAction,
Command,
@@ -57,6 +60,7 @@ from debputy.lsp.text_edit import (
from debputy.packages import SourcePackage, BinaryPackage
from debputy.plugin.api.feature_set import PluginProvidedFeatureSet
from debputy.util import _warn, _error, _info
+from debputy.yaml.compat import CommentedMap, __all__
LINTER_FORMATS = {
"debian/changelog": _lint_debian_changelog,
@@ -79,9 +83,11 @@ REFORMAT_FORMATS = {
class LintContext:
plugin_feature_set: PluginProvidedFeatureSet
style_preference_table: StylePreferenceTable
+ parsed_deb822_file_content: Optional[Deb822FileElement] = None
source_package: Optional[SourcePackage] = None
binary_packages: Optional[Mapping[str, BinaryPackage]] = None
effective_preference: Optional[EffectivePreference] = None
+ salsa_ci: Optional[CommentedMap] = None
def state_for(self, path: str, content: str, lines: List[str]) -> LintStateImpl:
return LintStateImpl(
@@ -103,18 +109,33 @@ def gather_lint_info(context: CommandContext) -> LintContext:
)
try:
with open("debian/control") as fd:
- source_package, binary_packages = (
+ deb822_file, source_package, binary_packages = (
context.dctrl_parser.parse_source_debian_control(fd, ignore_errors=True)
)
+ except FileNotFoundError:
+ source_package = None
+ else:
+ lint_context.parsed_deb822_file_content = deb822_file
lint_context.source_package = source_package
lint_context.binary_packages = binary_packages
- if source_package is not None:
- lint_context.effective_preference = determine_effective_style(
- lint_context.style_preference_table,
- source_package,
- )
- except FileNotFoundError:
- pass
+ salsa_ci_map: Optional[CommentedMap] = None
+ for ci_file in ("debian/salsa-ci.yml", ".gitlab-ci.yml"):
+ try:
+ with open(ci_file) as fd:
+ salsa_ci_map = MANIFEST_YAML.load(fd)
+ if not isinstance(salsa_ci_map, CommentedMap):
+ salsa_ci_map = None
+ break
+ except FileNotFoundError:
+ pass
+ except YAMLError:
+ break
+ if source_package is not None or salsa_ci_map is not None:
+ lint_context.effective_preference = determine_effective_style(
+ lint_context.style_preference_table,
+ source_package,
+ salsa_ci_map,
+ )
return lint_context
@@ -186,29 +207,45 @@ def perform_reformat(
lint_context.effective_preference = style
if lint_context.effective_preference is None:
- _info(
- "You can set `X-Style: black` in the source stanza of `debian/control` to pick"
- )
- _info("`black` as the preferred style for this package.")
- _info("")
- _info(
- "You can also include your style in `debputy`. This has the advantage that `debputy`"
- )
- _info(
- "can sometimes automatically derive an agreed style between all maintainers and"
- )
- _info(
- "co-maintainers for informal teams (provided all agree to the same style), where"
- )
- _info(
- "no explicit synchronization has been done (that is, there is no `X-Style` field)"
+ 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.declared_style_required:
+ _error(
+ "Sorry; `debputy` does not know which style to use for this package."
+ )
_info("")
_info(
- "For packaging teams, you can also have a team style that is applied to all team"
+ "Doing nothing since no style could be identified as requested."
+ " See above how to set a style."
)
- _info("maintained packages by having it recorded in `debputy`.")
- _error("Sorry; `debputy` does not know which style to use for this package.")
+ sys.exit(0)
changes = False
auto_fix = context.parsed_args.auto_fix
diff --git a/src/debputy/linting/lint_util.py b/src/debputy/linting/lint_util.py
index e997097..02b8804 100644
--- a/src/debputy/linting/lint_util.py
+++ b/src/debputy/linting/lint_util.py
@@ -5,6 +5,7 @@ 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.lsp.vendoring._deb822_repro import Deb822FileElement, parse_deb822_file
from debputy.packages import SourcePackage, BinaryPackage
from debputy.plugin.api.feature_set import PluginProvidedFeatureSet
from debputy.util import _DEFAULT_LOGGER, _warn
@@ -48,6 +49,10 @@ class LintState:
raise NotImplementedError
@property
+ def parsed_deb822_file_content(self) -> Optional[Deb822FileElement]:
+ raise NotImplementedError
+
+ @property
def source_package(self) -> Optional[SourcePackage]:
raise NotImplementedError
@@ -74,6 +79,7 @@ class LintStateImpl(LintState):
source_package: Optional[SourcePackage] = None
binary_packages: Optional[Mapping[str, BinaryPackage]] = None
effective_preference: Optional["EffectivePreference"] = None
+ _parsed_cache: Optional[Deb822FileElement] = None
@property
def doc_uri(self) -> str:
@@ -85,6 +91,18 @@ class LintStateImpl(LintState):
def position_codec(self) -> "LintCapablePositionCodec":
return LINTER_POSITION_CODEC
+ @property
+ def parsed_deb822_file_content(self) -> Optional[Deb822FileElement]:
+ cache = self._parsed_cache
+ if cache is None:
+ cache = parse_deb822_file(
+ self.lines,
+ accept_files_with_error_tokens=True,
+ accept_files_with_duplicated_fields=True,
+ )
+ self._parsed_cache = cache
+ return cache
+
@dataclasses.dataclass(slots=True)
class LintReport:
diff --git a/src/debputy/lsp/debputy_ls.py b/src/debputy/lsp/debputy_ls.py
index e475ab4..c08fd18 100644
--- a/src/debputy/lsp/debputy_ls.py
+++ b/src/debputy/lsp/debputy_ls.py
@@ -22,6 +22,7 @@ from debputy.lsp.style_prefs import (
determine_effective_style,
)
from debputy.lsp.text_util import LintCapablePositionCodec
+from debputy.lsp.vendoring._deb822_repro import Deb822FileElement, parse_deb822_file
from debputy.packages import (
SourcePackage,
BinaryPackage,
@@ -29,6 +30,8 @@ from debputy.packages import (
)
from debputy.plugin.api.feature_set import PluginProvidedFeatureSet
from debputy.util import _info
+from debputy.yaml import MANIFEST_YAML, YAMLError
+from debputy.yaml.compat import CommentedMap
if TYPE_CHECKING:
from pygls.server import LanguageServer
@@ -46,18 +49,117 @@ else:
def __init__(self, *args, **kwargs) -> None:
"""Placeholder to work if pygls is not installed"""
# Should not be called
+ global e
raise e # pragma: no cover
@dataclasses.dataclass(slots=True)
-class DctrlCache:
+class FileCache:
doc_uri: str
path: str
- is_open_in_editor: Optional[bool]
- last_doc_version: Optional[int]
- last_mtime: Optional[float]
- source_package: Optional[SourcePackage]
- binary_packages: Optional[Mapping[str, BinaryPackage]]
+ is_open_in_editor: Optional[bool] = None
+ last_doc_version: Optional[int] = None
+ last_mtime: Optional[float] = None
+ is_valid: bool = False
+
+ def _update_cache(self, doc: "TextDocument", source: str) -> None:
+ raise NotImplementedError
+
+ def _clear_cache(self) -> None:
+ raise NotImplementedError
+
+ def resolve_cache(self, ls: "DebputyLanguageServer") -> bool:
+ doc = ls.workspace.text_documents.get(self.doc_uri)
+ if doc is None:
+ doc = ls.workspace.get_text_document(self.doc_uri)
+ is_open = False
+ else:
+ is_open = True
+ new_content: Optional[str] = None
+ if is_open:
+ last_doc_version = self.last_doc_version
+ dctrl_doc_version = doc.version
+ if (
+ not self.is_open_in_editor
+ or last_doc_version is None
+ or dctrl_doc_version is None
+ or last_doc_version < dctrl_doc_version
+ ):
+ new_content = doc.source
+
+ self.last_doc_version = doc.version
+ elif doc.uri.startswith("file://"):
+ try:
+ with open(self.path) as fd:
+ st = os.fstat(fd.fileno())
+ current_mtime = st.st_mtime
+ last_mtime = self.last_mtime or current_mtime - 1
+ if self.is_open_in_editor or current_mtime > last_mtime:
+ new_content = fd.read()
+ self.last_mtime = current_mtime
+ except FileNotFoundError:
+ self._clear_cache()
+ self.is_valid = False
+ return False
+ if new_content is not None:
+ self._update_cache(doc, new_content)
+ self.is_valid = True
+ return True
+
+
+@dataclasses.dataclass(slots=True)
+class Deb822FileCache(FileCache):
+ deb822_file: Optional[Deb822FileElement] = None
+
+ def _update_cache(self, doc: "TextDocument", source: str) -> None:
+ deb822_file = parse_deb822_file(
+ source.splitlines(keepends=True),
+ accept_files_with_error_tokens=True,
+ accept_files_with_duplicated_fields=True,
+ )
+ self.deb822_file = deb822_file
+
+ def _clear_cache(self) -> None:
+ self.deb822_file = None
+
+
+@dataclasses.dataclass(slots=True)
+class DctrlFileCache(Deb822FileCache):
+ dctrl_parser: Optional[DctrlParser] = None
+ source_package: Optional[SourcePackage] = None
+ binary_packages: Optional[Mapping[str, BinaryPackage]] = None
+
+ def _update_cache(self, doc: "TextDocument", source: str) -> None:
+ deb822_file, source_package, binary_packages = (
+ self.dctrl_parser.parse_source_debian_control(
+ source.splitlines(keepends=True),
+ ignore_errors=True,
+ )
+ )
+ self.deb822_file = deb822_file
+ self.source_package = source_package
+ self.binary_packages = binary_packages
+
+ def _clear_cache(self) -> None:
+ super()._clear_cache()
+ self.source_package = None
+ self.binary_packages = None
+
+
+@dataclasses.dataclass(slots=True)
+class SalsaCICache(FileCache):
+ parsed_content: Optional[CommentedMap] = None
+
+ def _update_cache(self, doc: "TextDocument", source: str) -> None:
+ try:
+ value = MANIFEST_YAML.load(source)
+ if isinstance(value, CommentedMap):
+ self.parsed_content = value
+ except YAMLError:
+ pass
+
+ def _clear_cache(self) -> None:
+ self.parsed_content = None
class LSProvidedLintState(LintState):
@@ -72,17 +174,27 @@ class LSProvidedLintState(LintState):
self._doc = doc
# Cache lines (doc.lines re-splits everytime)
self._lines = doc.lines
- self._dctrl_parser = dctrl_parser
dctrl_file = os.path.join(debian_dir_path, "control")
- self._dctrl_cache: DctrlCache = DctrlCache(
+ self._dctrl_cache: DctrlFileCache = DctrlFileCache(
from_fs_path(dctrl_file),
dctrl_file,
- is_open_in_editor=None, # Unresolved
- last_doc_version=None,
- last_mtime=None,
- source_package=None,
- binary_packages=None,
+ dctrl_parser=dctrl_parser,
)
+ if dctrl_file != doc.path:
+ self._deb822_file: Deb822FileCache = Deb822FileCache(
+ from_fs_path(dctrl_file),
+ dctrl_file,
+ )
+ else:
+ self._deb822_file = self._dctrl_cache
+
+ self._salsa_ci_caches = [
+ SalsaCICache(
+ from_fs_path(os.path.join(debian_dir_path, p)),
+ os.path.join(debian_dir_path, p),
+ )
+ for p in ("salsa-ci.yml", os.path.join("..", ".gitlab-ci.yml"))
+ ]
@property
def plugin_feature_set(self) -> PluginProvidedFeatureSet:
@@ -108,67 +220,51 @@ class LSProvidedLintState(LintState):
def position_codec(self) -> LintCapablePositionCodec:
return self._doc.position_codec
- def _resolve_dctrl(self) -> Optional[DctrlCache]:
+ def _resolve_dctrl(self) -> Optional[DctrlFileCache]:
dctrl_cache = self._dctrl_cache
- doc = self._ls.workspace.text_documents.get(dctrl_cache.doc_uri)
- is_open = doc is not None
- dctrl_doc = self._ls.workspace.get_text_document(dctrl_cache.doc_uri)
- re_parse_lines: Optional[List[str]] = None
- if is_open:
- last_doc_version = dctrl_cache.last_doc_version
- dctrl_doc_version = dctrl_doc.version
- if (
- not dctrl_cache.is_open_in_editor
- or last_doc_version is None
- or dctrl_doc_version is None
- or last_doc_version < dctrl_doc_version
- ):
- re_parse_lines = doc.lines
-
- dctrl_cache.last_doc_version = dctrl_doc.version
- elif self._doc.uri.startswith("file://"):
- try:
- with open(dctrl_cache.path) as fd:
- st = os.fstat(fd.fileno())
- current_mtime = st.st_mtime
- last_mtime = dctrl_cache.last_mtime or current_mtime - 1
- if dctrl_cache.is_open_in_editor or current_mtime > last_mtime:
- re_parse_lines = list(fd)
- dctrl_cache.last_mtime = current_mtime
- except FileNotFoundError:
- return None
- if re_parse_lines is not None:
- source_package, binary_packages = (
- self._dctrl_parser.parse_source_debian_control(
- re_parse_lines,
- ignore_errors=True,
- )
- )
- dctrl_cache.source_package = source_package
- dctrl_cache.binary_packages = binary_packages
+ dctrl_cache.resolve_cache(self._ls)
return dctrl_cache
@property
+ def parsed_deb822_file_content(self) -> Optional[Deb822FileElement]:
+ cache = self._deb822_file
+ cache.resolve_cache(self._ls)
+ return cache.deb822_file
+
+ @property
def source_package(self) -> Optional[SourcePackage]:
- dctrl = self._resolve_dctrl()
- return dctrl.source_package if dctrl is not None else None
+ return self._resolve_dctrl().source_package
@property
def binary_packages(self) -> Optional[Mapping[str, BinaryPackage]]:
- dctrl = self._resolve_dctrl()
- return dctrl.binary_packages if dctrl is not None else None
+ return self._resolve_dctrl().binary_packages
+
+ def _resolve_salsa_ci(self) -> Optional[CommentedMap]:
+ for salsa_ci_cache in self._salsa_ci_caches:
+ if salsa_ci_cache.resolve_cache(self._ls):
+ return salsa_ci_cache.parsed_content
+ return None
@property
def effective_preference(self) -> Optional[MaintainerPreference]:
source_package = self.source_package
- if source_package is None:
+ salsa_ci = self._resolve_salsa_ci()
+ if source_package is None and salsa_ci is None:
return None
- return determine_effective_style(self.style_preference_table, source_package)
+ return determine_effective_style(
+ self.style_preference_table,
+ source_package,
+ salsa_ci,
+ )
@property
def style_preference_table(self) -> StylePreferenceTable:
return self._ls.style_preferences
+ @property
+ def salsa_ci(self) -> Optional[CommentedMap]:
+ return None
+
def _preference(
client_preference: Optional[List[MarkupKind]],
diff --git a/src/debputy/lsp/lsp_debian_changelog.py b/src/debputy/lsp/lsp_debian_changelog.py
index c6166c5..2deff97 100644
--- a/src/debputy/lsp/lsp_debian_changelog.py
+++ b/src/debputy/lsp/lsp_debian_changelog.py
@@ -4,7 +4,6 @@ from email.utils import parsedate_to_datetime
from typing import (
Union,
List,
- Dict,
Iterator,
Optional,
Iterable,
@@ -16,7 +15,6 @@ from lsprotocol.types import (
DidChangeTextDocumentParams,
TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL,
TEXT_DOCUMENT_CODE_ACTION,
- DidCloseTextDocumentParams,
Range,
Position,
DiagnosticSeverity,
@@ -29,12 +27,12 @@ from debputy.lsp.quickfixes import (
propose_correct_text_quick_fix,
)
from debputy.lsp.spellchecking import spellcheck_line
-from debputy.lsp.text_util import (
- LintCapablePositionCodec,
-)
try:
- from debian._deb822_repro.locatable import Position as TEPosition, Ranage as TERange
+ from debputy.lsp.vendoring._deb822_repro.locatable import (
+ Position as TEPosition,
+ Range as TERange,
+ )
from pygls.server import LanguageServer
from pygls.workspace import TextDocument
diff --git a/src/debputy/lsp/lsp_debian_control.py b/src/debputy/lsp/lsp_debian_control.py
index 3a4107b..699193c 100644
--- a/src/debputy/lsp/lsp_debian_control.py
+++ b/src/debputy/lsp/lsp_debian_control.py
@@ -1,7 +1,6 @@
import dataclasses
import re
import textwrap
-from functools import lru_cache
from typing import (
Union,
Sequence,
@@ -12,6 +11,28 @@ from typing import (
Dict,
)
+from lsprotocol.types import (
+ DiagnosticSeverity,
+ Range,
+ Diagnostic,
+ Position,
+ FoldingRange,
+ FoldingRangeParams,
+ CompletionItem,
+ CompletionList,
+ CompletionParams,
+ DiagnosticRelatedInformation,
+ Location,
+ HoverParams,
+ Hover,
+ TEXT_DOCUMENT_CODE_ACTION,
+ SemanticTokens,
+ SemanticTokensParams,
+ WillSaveTextDocumentParams,
+ TextEdit,
+ DocumentFormattingParams,
+)
+
from debputy.linting.lint_util import LintState
from debputy.lsp.debputy_ls import DebputyLanguageServer
from debputy.lsp.diagnostics import DiagnosticData
@@ -55,7 +76,6 @@ from debputy.lsp.text_util import (
te_range_to_lsp,
)
from debputy.lsp.vendoring._deb822_repro import (
- parse_deb822_file,
Deb822FileElement,
Deb822ParagraphElement,
)
@@ -63,28 +83,6 @@ from debputy.lsp.vendoring._deb822_repro.parsing import (
Deb822KeyValuePairElement,
LIST_SPACE_SEPARATED_INTERPRETATION,
)
-from lsprotocol.types import (
- DiagnosticSeverity,
- Range,
- Diagnostic,
- Position,
- FoldingRange,
- FoldingRangeParams,
- CompletionItem,
- CompletionList,
- CompletionParams,
- TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL,
- DiagnosticRelatedInformation,
- Location,
- HoverParams,
- Hover,
- TEXT_DOCUMENT_CODE_ACTION,
- SemanticTokens,
- SemanticTokensParams,
- WillSaveTextDocumentParams,
- TextEdit,
- DocumentFormattingParams,
-)
try:
from debputy.lsp.vendoring._deb822_repro.locatable import (
@@ -846,11 +844,7 @@ def _lint_debian_control(
position_codec = lint_state.position_codec
doc_reference = lint_state.doc_uri
diagnostics = []
- deb822_file = parse_deb822_file(
- lines,
- accept_files_with_duplicated_fields=True,
- accept_files_with_error_tokens=True,
- )
+ deb822_file = lint_state.parsed_deb822_file_content
first_error = _scan_for_syntax_errors_and_token_level_diagnostics(
deb822_file,
diff --git a/src/debputy/lsp/lsp_debian_control_reference_data.py b/src/debputy/lsp/lsp_debian_control_reference_data.py
index 60e47d7..1d4628c 100644
--- a/src/debputy/lsp/lsp_debian_control_reference_data.py
+++ b/src/debputy/lsp/lsp_debian_control_reference_data.py
@@ -22,9 +22,10 @@ from typing import (
Any,
Set,
TYPE_CHECKING,
+ Sequence,
)
-from debian._deb822_repro.types import TE
+from debputy.lsp.vendoring._deb822_repro.types import TE
from debian.debian_support import DpkgArchTable
from lsprotocol.types import (
DiagnosticSeverity,
@@ -33,10 +34,13 @@ from lsprotocol.types import (
Range,
TextEdit,
Position,
+ CompletionItem,
+ MarkupContent,
+ CompletionItemTag,
+ MarkupKind,
)
from debputy.lsp.diagnostics import DiagnosticData
-
from debputy.lsp.lsp_reference_keyword import (
ALL_PUBLIC_NAMED_STYLES,
Keyword,
@@ -205,9 +209,11 @@ ALL_SECTIONS_WITHOUT_COMPONENT = frozenset(
"gnome",
"gnu-r",
"gnustep",
+ "golang",
"graphics",
"hamradio",
"haskell",
+ "httpd",
"interpreters",
"introspection",
"java",
@@ -455,6 +461,29 @@ def _udeb_only_field_validation(
)
+def _complete_only_in_arch_dep_pkgs(
+ stanza_parts: Iterable[Deb822ParagraphElement],
+) -> bool:
+ for stanza in stanza_parts:
+ arch = stanza.get("Architecture")
+ if arch is None:
+ continue
+ archs = arch.split()
+ return "all" not in archs
+ return False
+
+
+def _complete_only_for_udeb_pkgs(
+ stanza_parts: Iterable[Deb822ParagraphElement],
+) -> bool:
+ for stanza in stanza_parts:
+ for option in ("Package-Type", "XC-Package-Type"):
+ pkg_type = stanza.get(option)
+ if pkg_type is not None:
+ return pkg_type == "udeb"
+ return False
+
+
def _arch_not_all_only_field_validation(
known_field: "F",
_kvpair: Deb822KeyValuePairElement,
@@ -800,6 +829,92 @@ class Deb822KnownField:
is_stanza_name: bool = False
is_single_value_field: bool = True
custom_field_check: Optional[CustomFieldCheck] = None
+ can_complete_field_in_stanza: Optional[
+ Callable[[Iterable[Deb822ParagraphElement]], bool]
+ ] = None
+
+ def _can_complete_field_in_stanza(
+ self,
+ stanza_parts: Sequence[Deb822ParagraphElement],
+ ) -> bool:
+ return (
+ self.can_complete_field_in_stanza is None
+ or self.can_complete_field_in_stanza(stanza_parts)
+ )
+
+ def complete_field(
+ self,
+ stanza_parts: Sequence[Deb822ParagraphElement],
+ markdown_kind: MarkupKind,
+ ) -> Optional[CompletionItem]:
+ if not self._can_complete_field_in_stanza(stanza_parts):
+ return None
+ name = self.name
+ complete_as = name + ": "
+ options = self.value_options_for_completer(
+ stanza_parts,
+ is_completion_for_field=True,
+ )
+ if options is not None and len(options) == 1:
+ value = options[0].insert_text
+ if value is not None:
+ complete_as += value
+ tags = []
+ is_deprecated = False
+ if self.replaced_by or self.deprecated_with_no_replacement:
+ is_deprecated = True
+ tags.append(CompletionItemTag.Deprecated)
+
+ doc = self.hover_text
+ if doc:
+ doc = MarkupContent(
+ value=doc,
+ kind=markdown_kind,
+ )
+ else:
+ doc = None
+
+ return CompletionItem(
+ name,
+ insert_text=complete_as,
+ deprecated=is_deprecated,
+ tags=tags,
+ detail=self.synopsis_doc,
+ documentation=doc,
+ )
+
+ def value_options_for_completer(
+ self,
+ stanza_parts: Sequence[Deb822ParagraphElement],
+ *,
+ is_completion_for_field: bool = False,
+ ) -> Optional[Sequence[CompletionItem]]:
+ known_values = self.known_values
+ if known_values is None:
+ return None
+ if is_completion_for_field and (
+ len(known_values) == 1
+ or (
+ len(known_values) == 2
+ and self.warn_if_default
+ and self.default_value is not None
+ )
+ ):
+ value = next(
+ iter(v for v in self.known_values if v != self.default_value),
+ None,
+ )
+ if value is None:
+ return None
+ return [CompletionItem(value, insert_text=value)]
+ return [
+ CompletionItem(
+ keyword.value,
+ insert_text=keyword.value,
+ )
+ for keyword in known_values.values()
+ if keyword.is_keyword_valid_completion_in_stanza(stanza_parts)
+ ]
def field_diagnostics(
self,
@@ -1723,6 +1838,7 @@ SOURCE_FIELDS = _fields(
FieldValueClass.SINGLE_VALUE,
known_values=allowed_values("yes"),
synopsis_doc="Whether this non-free is auto-buildable on buildds",
+ # TODO: Needs logic for reading source section: can_complete_field_in_stanza=_complete_only_for_non_free_pkgs,
hover_text=textwrap.dedent(
"""\
Used for non-free packages to denote that they may be auto-build on the Debian build infrastructure
@@ -2364,6 +2480,7 @@ BINARY_FIELDS = _fields(
),
Keyword(
"same",
+ can_complete_keyword_in_stanza=_complete_only_in_arch_dep_pkgs,
hover_text=textwrap.dedent(
"""\
The same version of the package can be co-installed for multiple architecture. However,
@@ -2381,8 +2498,14 @@ BINARY_FIELDS = _fields(
"allowed",
hover_text=textwrap.dedent(
"""\
- **Advanced value**. The package is *not* co-installable with itself but can satisfy Multi-Arch
- foreign and Multi-Arch same relations at the same. This is useful for implementations of
+ **Advanced and very rare value**. This value is exceedingly rare to the point that less
+ than 0.40% of all packages in Debian used in it (May 2024). It is even rarer for for
+ `Architecture: all` packages, where the number an order of magnitude smaller. Unless
+ a Multi-Arch expert or the Multi-Arch hinter suggested that you use this value, for
+ this package, then probably it is not the right choice.
+
+ The value means that the package is *not* co-installable with itself but can satisfy Multi-Arch
+ `foreign` and Multi-Arch `same` relations at the same time. This is useful for implementations of
scripting languages (such as Perl or Python). Here the interpreter contextually need to
satisfy some relations as `Multi-Arch: foreign` and others as `Multi-Arch: same`.
@@ -2504,6 +2627,7 @@ BINARY_FIELDS = _fields(
DctrlKnownField(
"XB-Installer-Menu-Item",
FieldValueClass.SINGLE_VALUE,
+ can_complete_field_in_stanza=_complete_only_for_udeb_pkgs,
custom_field_check=_combined_custom_field_check(
_udeb_only_field_validation,
_each_value_match_regex_validation(re.compile(r"^[1-9]\d{3,4}$")),
@@ -2531,6 +2655,7 @@ BINARY_FIELDS = _fields(
"X-DH-Build-For-Type",
FieldValueClass.SINGLE_VALUE,
custom_field_check=_arch_not_all_only_field_validation,
+ can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs,
default_value="host",
known_values=allowed_values(
Keyword(
@@ -2572,7 +2697,11 @@ BINARY_FIELDS = _fields(
DctrlKnownField(
"X-Time64-Compat",
FieldValueClass.SINGLE_VALUE,
- custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX),
+ can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs,
+ custom_field_check=_combined_custom_field_check(
+ _each_value_match_regex_validation(PKGNAME_REGEX),
+ _arch_not_all_only_field_validation,
+ ),
synopsis_doc="(Special purpose) Compat name for time64_t transition",
hover_text=textwrap.dedent(
"""\
@@ -2581,6 +2710,8 @@ BINARY_FIELDS = _fields(
It is used to inform packaging helpers what the original (non-transitioned) package name
was when the auto-detection is inadequate. The non-transitioned package name is then
conditionally provided in the `${t64:Provides}` substitution variable.
+
+ The field only works for architecture dependent packages.
"""
),
),
diff --git a/src/debputy/lsp/lsp_debian_copyright.py b/src/debputy/lsp/lsp_debian_copyright.py
index abd5488..843627e 100644
--- a/src/debputy/lsp/lsp_debian_copyright.py
+++ b/src/debputy/lsp/lsp_debian_copyright.py
@@ -3,15 +3,12 @@ from typing import (
Union,
Sequence,
Tuple,
- Iterator,
Optional,
- Iterable,
Mapping,
List,
Dict,
)
-from debputy.lsp.debputy_ls import DebputyLanguageServer
from lsprotocol.types import (
DiagnosticSeverity,
Range,
@@ -20,7 +17,6 @@ from lsprotocol.types import (
CompletionItem,
CompletionList,
CompletionParams,
- TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL,
DiagnosticRelatedInformation,
Location,
HoverParams,
@@ -36,6 +32,7 @@ from lsprotocol.types import (
)
from debputy.linting.lint_util import LintState
+from debputy.lsp.debputy_ls import DebputyLanguageServer
from debputy.lsp.diagnostics import DiagnosticData
from debputy.lsp.lsp_debian_control_reference_data import (
_DEP5_HEADER_FIELDS,
@@ -73,7 +70,6 @@ from debputy.lsp.text_util import (
te_range_to_lsp,
)
from debputy.lsp.vendoring._deb822_repro import (
- parse_deb822_file,
Deb822FileElement,
Deb822ParagraphElement,
)
@@ -81,9 +77,6 @@ from debputy.lsp.vendoring._deb822_repro.parsing import (
Deb822KeyValuePairElement,
LIST_SPACE_SEPARATED_INTERPRETATION,
)
-from debputy.lsp.vendoring._deb822_repro.tokens import (
- Deb822Token,
-)
try:
from debputy.lsp.vendoring._deb822_repro.locatable import (
@@ -442,11 +435,7 @@ def _lint_debian_copyright(
position_codec = lint_state.position_codec
doc_reference = lint_state.doc_uri
diagnostics: List[Diagnostic] = []
- deb822_file = parse_deb822_file(
- lines,
- accept_files_with_duplicated_fields=True,
- accept_files_with_error_tokens=True,
- )
+ deb822_file = lint_state.parsed_deb822_file_content
first_error = _scan_for_syntax_errors_and_token_level_diagnostics(
deb822_file,
diff --git a/src/debputy/lsp/lsp_debian_rules.py b/src/debputy/lsp/lsp_debian_rules.py
index ec8d9d6..390ddfa 100644
--- a/src/debputy/lsp/lsp_debian_rules.py
+++ b/src/debputy/lsp/lsp_debian_rules.py
@@ -43,7 +43,7 @@ from debputy.lsp.text_util import (
from debputy.util import _warn
try:
- from debian._deb822_repro.locatable import (
+ from debputy.lsp.vendoring._deb822_repro.locatable import (
Position as TEPosition,
Range as TERange,
START_POSITION,
diff --git a/src/debputy/lsp/lsp_debian_tests_control.py b/src/debputy/lsp/lsp_debian_tests_control.py
index 001dbe2..20a198c 100644
--- a/src/debputy/lsp/lsp_debian_tests_control.py
+++ b/src/debputy/lsp/lsp_debian_tests_control.py
@@ -68,7 +68,6 @@ from debputy.lsp.text_util import (
te_range_to_lsp,
)
from debputy.lsp.vendoring._deb822_repro import (
- parse_deb822_file,
Deb822FileElement,
Deb822ParagraphElement,
)
@@ -259,6 +258,8 @@ def _diagnostics_for_paragraph(
)
)
continue
+ if known_field is None:
+ continue
diagnostics.extend(
known_field.field_diagnostics(
kvpair,
@@ -433,11 +434,7 @@ def _lint_debian_tests_control(
position_codec = lint_state.position_codec
doc_reference = lint_state.doc_uri
diagnostics: List[Diagnostic] = []
- deb822_file = parse_deb822_file(
- lines,
- accept_files_with_duplicated_fields=True,
- accept_files_with_error_tokens=True,
- )
+ deb822_file = lint_state.parsed_deb822_file_content
first_error = _scan_for_syntax_errors_and_token_level_diagnostics(
deb822_file,
diff --git a/src/debputy/lsp/lsp_dispatch.py b/src/debputy/lsp/lsp_dispatch.py
index 99479dc..9a54c2b 100644
--- a/src/debputy/lsp/lsp_dispatch.py
+++ b/src/debputy/lsp/lsp_dispatch.py
@@ -8,6 +8,7 @@ from typing import (
Callable,
Mapping,
List,
+ TYPE_CHECKING,
)
from lsprotocol.types import (
@@ -53,20 +54,30 @@ from debputy.util import _info
_DOCUMENT_VERSION_TABLE: Dict[str, int] = {}
-try:
- from pygls.server import LanguageServer
- from pygls.workspace import TextDocument
+
+if TYPE_CHECKING:
+ try:
+ from pygls.server import LanguageServer
+ except ImportError:
+ pass
+
from debputy.lsp.debputy_ls import DebputyLanguageServer
DEBPUTY_LANGUAGE_SERVER = DebputyLanguageServer("debputy", f"v{__version__}")
-except ImportError as e:
+else:
+ try:
+ from pygls.server import LanguageServer
+ from debputy.lsp.debputy_ls import DebputyLanguageServer
+
+ DEBPUTY_LANGUAGE_SERVER = DebputyLanguageServer("debputy", f"v{__version__}")
+ except ImportError:
- class Mock:
+ class Mock:
- def feature(self, *args, **kwargs):
- return lambda x: x
+ def feature(self, *args, **kwargs):
+ return lambda x: x
- DEBPUTY_LANGUAGE_SERVER = Mock()
+ DEBPUTY_LANGUAGE_SERVER = Mock()
P = TypeVar("P")
@@ -225,7 +236,7 @@ def _will_save_wait_until(
@DEBPUTY_LANGUAGE_SERVER.feature(TEXT_DOCUMENT_FORMATTING)
-def _will_save_wait_until(
+def _format_document(
ls: "DebputyLanguageServer",
params: WillSaveTextDocumentParams,
) -> Optional[Sequence[TextEdit]]:
@@ -244,7 +255,7 @@ def _dispatch_standard_handler(
params: P,
handler_table: Mapping[str, Callable[[L, P], R]],
request_type: str,
-) -> R:
+) -> Optional[R]:
doc = ls.workspace.get_text_document(doc_uri)
id_source, language_id = ls.determine_language_id(doc)
@@ -253,7 +264,7 @@ def _dispatch_standard_handler(
_info(
f"{request_type} for document: {doc.path} ({language_id}, {id_source}) - no handler"
)
- return
+ return None
_info(
f"{request_type} for document: {doc.path} ({language_id}, {id_source}) - delegating to handler"
)
diff --git a/src/debputy/lsp/lsp_generic_deb822.py b/src/debputy/lsp/lsp_generic_deb822.py
index 409b27e..6d44d1a 100644
--- a/src/debputy/lsp/lsp_generic_deb822.py
+++ b/src/debputy/lsp/lsp_generic_deb822.py
@@ -1,5 +1,6 @@
import dataclasses
import re
+from itertools import chain
from typing import (
Optional,
Union,
@@ -12,6 +13,7 @@ from typing import (
Iterable,
Iterator,
Callable,
+ cast,
)
from lsprotocol.types import (
@@ -29,9 +31,8 @@ from lsprotocol.types import (
FoldingRangeKind,
SemanticTokensParams,
SemanticTokens,
- WillSaveTextDocumentParams,
TextEdit,
- DocumentFormattingParams,
+ MessageType,
)
from debputy.linting.lint_util import LintState
@@ -45,17 +46,21 @@ from debputy.lsp.lsp_debian_control_reference_data import (
)
from debputy.lsp.lsp_features import SEMANTIC_TOKEN_TYPES_IDS
from debputy.lsp.text_util import (
- normalize_dctrl_field_name,
te_position_to_lsp,
trim_end_of_line_whitespace,
)
-from debputy.lsp.vendoring._deb822_repro import parse_deb822_file
+from debputy.lsp.vendoring._deb822_repro.locatable import (
+ START_POSITION,
+ Range as TERange,
+)
from debputy.lsp.vendoring._deb822_repro.parsing import (
Deb822KeyValuePairElement,
Deb822ParagraphElement,
+ Deb822FileElement,
)
from debputy.lsp.vendoring._deb822_repro.tokens import tokenize_deb822_file, Deb822Token
-from debputy.util import _info
+from debputy.lsp.vendoring._deb822_repro.types import TokenOrElement
+from debputy.util import _info, _warn
try:
from pygls.server import LanguageServer
@@ -67,61 +72,113 @@ except ImportError:
_CONTAINS_SPACE_OR_COLON = re.compile(r"[\s:]")
+def in_range(te_range: TERange, cursor_position: Position) -> bool:
+ 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
+ return (
+ cursor_position.line == end_pos.line_position
+ and cursor_position.character < end_pos.cursor_position
+ )
+
+
+def _field_at_position(
+ stanza: Deb822ParagraphElement,
+ stanza_range: TERange,
+ position: Position,
+) -> Tuple[Optional[Deb822KeyValuePairElement], 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)
+ if not in_range(te_range, position):
+ continue
+ if isinstance(token_or_element, Deb822KeyValuePairElement):
+ value_range = token_or_element.value_element.range_in_parent().relative_to(
+ te_range.start_pos
+ )
+ in_value = in_range(value_range, position)
+ return token_or_element, in_value
+ return None, False
+
+
+def _allow_stanza_continuation(
+ token_or_element: TokenOrElement,
+ is_completion: bool,
+) -> bool:
+ if not is_completion:
+ return False
+ if token_or_element.is_error or token_or_element.is_comment:
+ return True
+ return (
+ token_or_element.is_whitespace
+ and token_or_element.convert_to_text().count("\n") < 2
+ )
+
+
def _at_cursor(
+ deb822_file: Deb822FileElement,
doc: "TextDocument",
lines: List[str],
client_position: Position,
-) -> Tuple[Position, Optional[str], str, bool, int, Set[str]]:
- paragraph_no = -1
- paragraph_started = False
- seen_fields: Set[str] = set()
- last_field_seen: Optional[str] = None
- current_field: Optional[str] = None
+ is_completion: bool = False,
+) -> Tuple[Position, Optional[str], str, bool, int, Iterable[Deb822ParagraphElement]]:
server_position = doc.position_codec.position_from_client_units(
lines,
client_position,
)
- position_line_no = server_position.line
+ te_range = TERange(
+ START_POSITION,
+ START_POSITION,
+ )
+ paragraph_no = -1
+ previous_stanza: Optional[Deb822ParagraphElement] = None
+ next_stanza: Optional[Deb822ParagraphElement] = None
+ current_word = doc.word_at_position(client_position)
+ in_value: bool = False
+ file_iter = iter(deb822_file.iter_parts())
+ matched_token: Optional[TokenOrElement] = None
+ matched_field: Optional[str] = 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):
+ previous_stanza = token_or_element
+ paragraph_no += 1
+ elif not _allow_stanza_continuation(token_or_element, is_completion):
+ previous_stanza = None
+ if not in_range(te_range, server_position):
+ 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
+ )
+ if kvpair is not None:
+ matched_field = kvpair.field_name
+ break
- line_at_position = lines[position_line_no]
- line_start = ""
- if server_position.character:
- line_start = line_at_position[0 : server_position.character]
+ if matched_token is not None and _allow_stanza_continuation(
+ matched_token,
+ is_completion,
+ ):
+ next_te = next(file_iter, None)
+ if isinstance(next_te, Deb822ParagraphElement):
+ next_stanza = next_te
+
+ stanza_parts = (p for p in (previous_stanza, next_stanza) if p is not None)
- for line_no, line in enumerate(lines):
- if not line or line.isspace():
- if line_no == position_line_no:
- current_field = last_field_seen
- continue
- last_field_seen = None
- if line_no > position_line_no:
- break
- paragraph_started = False
- elif line and line[0] == "#":
- continue
- elif line and not line[0].isspace() and ":" in line:
- if not paragraph_started:
- paragraph_started = True
- seen_fields = set()
- paragraph_no += 1
- key, _ = line.split(":", 1)
- key_lc = key.lower()
- last_field_seen = key_lc
- if line_no == position_line_no:
- current_field = key_lc
- seen_fields.add(key_lc)
-
- in_value = bool(_CONTAINS_SPACE_OR_COLON.search(line_start))
- current_word = doc.word_at_position(client_position)
- if current_field is not None:
- current_field = normalize_dctrl_field_name(current_field)
return (
server_position,
- current_field,
+ matched_field,
current_word,
in_value,
paragraph_no,
- seen_fields,
+ stanza_parts,
)
@@ -132,32 +189,44 @@ 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
+ if deb822_file is None:
+ _warn("The deb822 result missing failed!?")
+ ls.show_message_log(
+ "Internal error; could not get deb822 content!?", MessageType.Warning
+ )
+ return None
- _a, current_field, _b, in_value, paragraph_no, seen_fields = _at_cursor(
+ _a, current_field, _b, in_value, paragraph_no, matched_stanzas = _at_cursor(
+ deb822_file,
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 = stanza_metadata.get(current_field)
+ known_field: Deb822KnownField = stanza_metadata.get(current_field)
if known_field is None:
return None
- items = _complete_field_value(known_field)
+ items = known_field.value_options_for_completer(list(matched_stanzas))
else:
_info("Completing field name")
items = _complete_field_name(
ls,
stanza_metadata,
- seen_fields,
+ matched_stanzas,
)
- _info(f"Completion candidates: {items}")
+ _info(
+ f"Completion candidates: {[i.label for i in items] if items is not None else 'None'}"
+ )
return items
@@ -184,8 +253,19 @@ def deb822_hover(
) -> Optional[Hover]:
doc = ls.workspace.get_text_document(params.text_document.uri)
lines = doc.lines
+ deb822_file = ls.lint_state(doc).parsed_deb822_file_content
+ if deb822_file is None:
+ _warn("The deb822 result missing failed!?")
+ ls.show_message_log(
+ "Internal error; could not get deb822 content!?", MessageType.Warning
+ )
+ return None
+
server_pos, current_field, word_at_position, in_value, paragraph_no, _ = _at_cursor(
- doc, lines, params.position
+ deb822_file,
+ doc,
+ lines,
+ params.position,
)
stanza_metadata = file_metadata.guess_stanza_classification_by_idx(paragraph_no)
@@ -428,11 +508,11 @@ def deb822_format_file(
return trim_end_of_line_whitespace(lint_state.position_codec, lint_state.lines)
formatter = effective_preference.deb822_formatter()
lines = lint_state.lines
- deb822_file = parse_deb822_file(
- lines,
- accept_files_with_duplicated_fields=True,
- accept_files_with_error_tokens=True,
- )
+ deb822_file = lint_state.parsed_deb822_file_content
+ if deb822_file is None:
+ _warn("The deb822 result missing failed!?")
+ return None
+
return list(
file_metadata.reformat(
effective_preference,
@@ -453,11 +533,14 @@ def deb822_semantic_tokens_full(
doc = ls.workspace.get_text_document(request.text_document.uri)
position_codec = doc.position_codec
lines = doc.lines
- deb822_file = parse_deb822_file(
- lines,
- accept_files_with_duplicated_fields=True,
- accept_files_with_error_tokens=True,
- )
+ deb822_file = ls.lint_state(doc).parsed_deb822_file_content
+ if deb822_file is None:
+ _warn("The deb822 result missing failed!?")
+ ls.show_message_log(
+ "Internal error; could not get deb822 content!?", MessageType.Warning
+ )
+ return None
+
tokens: List[int] = []
comment_token_code = SEMANTIC_TOKEN_TYPES_IDS["comment"]
sem_token_state = SemanticTokenState(
@@ -494,65 +577,33 @@ def deb822_semantic_tokens_full(
return SemanticTokens(tokens)
-def _should_complete_field_with_value(cand: Deb822KnownField) -> bool:
- return cand.known_values is not None and (
- len(cand.known_values) == 1
- or (
- len(cand.known_values) == 2
- and cand.warn_if_default
- and cand.default_value is not None
- )
- )
-
-
def _complete_field_name(
ls: "DebputyLanguageServer",
fields: StanzaMetadata[Any],
- seen_fields: Container[str],
-) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]:
+ matched_stanzas: Iterable[Deb822ParagraphElement],
+) -> Sequence[CompletionItem]:
items = []
markdown_kind = ls.completion_item_document_markup(
MarkupKind.Markdown, MarkupKind.PlainText
)
+ matched_stanzas = list(matched_stanzas)
+ # TODO: Normalize fields according to file rules (X[BCS]- should be stripped in some files)
+ seen_fields = set(
+ f.lower()
+ for f in chain.from_iterable(
+ # The typing from python3-debian is not entirely optimal here. The iter always return a
+ # `str`, but the provided type is `ParagraphKey` (because `__getitem__` supports those)
+ # and that is not exclusively a `str`.
+ #
+ # So, this cast for now
+ cast("Iterable[str]", s)
+ for s in matched_stanzas
+ )
+ )
for cand_key, cand in fields.items():
if cand_key.lower() in seen_fields:
continue
- name = cand.name
- complete_as = name + ": "
- if _should_complete_field_with_value(cand):
- value = next(iter(v for v in cand.known_values if v != cand.default_value))
- complete_as += value
- tags = []
- is_deprecated = False
- if cand.replaced_by or cand.deprecated_with_no_replacement:
- is_deprecated = True
- tags.append(CompletionItemTag.Deprecated)
-
- doc = cand.hover_text
- if doc:
- doc = MarkupContent(
- value=doc,
- kind=markdown_kind,
- )
- else:
- doc = None
-
- items.append(
- CompletionItem(
- name,
- insert_text=complete_as,
- deprecated=is_deprecated,
- tags=tags,
- detail=cand.synopsis_doc,
- documentation=doc,
- )
- )
+ item = cand.complete_field(matched_stanzas, markdown_kind)
+ if item is not None:
+ items.append(item)
return items
-
-
-def _complete_field_value(
- field: Deb822KnownField,
-) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]:
- if field.known_values is None:
- return None
- return [CompletionItem(v) for v in field.known_values]
diff --git a/src/debputy/lsp/lsp_reference_keyword.py b/src/debputy/lsp/lsp_reference_keyword.py
index 44f43fd..c71352a 100644
--- a/src/debputy/lsp/lsp_reference_keyword.py
+++ b/src/debputy/lsp/lsp_reference_keyword.py
@@ -1,6 +1,8 @@
import dataclasses
import textwrap
-from typing import Optional, Union, Mapping
+from typing import Optional, Union, Mapping, Sequence, Callable, Iterable
+
+from debputy.lsp.vendoring._deb822_repro import Deb822ParagraphElement
@dataclasses.dataclass(slots=True, frozen=True)
@@ -10,11 +12,23 @@ class Keyword:
is_obsolete: bool = False
replaced_by: Optional[str] = None
is_exclusive: bool = False
+ can_complete_keyword_in_stanza: Optional[
+ Callable[[Iterable[Deb822ParagraphElement]], bool]
+ ] = None
"""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.
"""
+ 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 allowed_values(*values: Union[str, Keyword]) -> Mapping[str, Keyword]:
as_keywords = [k if isinstance(k, Keyword) else Keyword(k) for k in values]
diff --git a/src/debputy/lsp/quickfixes.py b/src/debputy/lsp/quickfixes.py
index 7a8a904..a9fcf7b 100644
--- a/src/debputy/lsp/quickfixes.py
+++ b/src/debputy/lsp/quickfixes.py
@@ -137,7 +137,7 @@ def _correct_value_code_action(
@_code_handler_for("insert-text-on-line-after-diagnostic")
-def _correct_value_code_action(
+def _insert_text_on_line_after_diagnostic_code_action(
code_action_data: InsertTextOnLineAfterDiagnosticCodeAction,
code_action_params: CodeActionParams,
diagnostic: Diagnostic,
diff --git a/src/debputy/lsp/style_prefs.py b/src/debputy/lsp/style_prefs.py
index 5dfdac2..40d850b 100644
--- a/src/debputy/lsp/style_prefs.py
+++ b/src/debputy/lsp/style_prefs.py
@@ -14,13 +14,15 @@ from typing import (
Mapping,
Self,
Dict,
+ Iterable,
+ Any,
)
from debputy.lsp.lsp_reference_keyword import ALL_PUBLIC_NAMED_STYLES
from debputy.lsp.vendoring._deb822_repro.types import FormatterCallback
from debputy.lsp.vendoring.wrap_and_sort import wrap_and_sort_formatter
from debputy.packages import SourcePackage
-from debputy.util import _error
+from debputy.util import _error, _info
from debputy.yaml import MANIFEST_YAML
from debputy.yaml.compat import CommentedMap
@@ -32,6 +34,29 @@ BUILTIN_STYLES = os.path.join(os.path.dirname(__file__), "style-preferences.yaml
_NORMALISE_FIELD_CONTENT_KEY = ["formatting", "deb822", "normalize-field-content"]
_UPLOADER_SPLIT_RE = re.compile(r"(?<=>)\s*,")
+_WAS_OPTIONS = {
+ "-a": ("formatting_deb822_always_wrap", True),
+ "--always-wrap": ("formatting_deb822_always_wrap", True),
+ "-s": ("formatting_deb822_short_indent", True),
+ "--short-indent": ("formatting_deb822_short_indent", True),
+ "-t": ("formatting_deb822_trailing_separator", True),
+ "--trailing-separator": ("formatting_deb822_trailing_separator", True),
+ # Noise option for us; we do not accept `--no-keep-first` though
+ "-k": (None, True),
+ "--keep-first": (None, True),
+ "--no-keep-first": ("DISABLE_NORMALIZE_STANZA_ORDER", True),
+ "-b": ("formatting_deb822_normalize_stanza_order", True),
+ "--sort-binary-packages": ("formatting_deb822_normalize_stanza_order", True),
+}
+
+_WAS_DEFAULTS = {
+ "formatting_deb822_always_wrap": False,
+ "formatting_deb822_short_indent": False,
+ "formatting_deb822_trailing_separator": False,
+ "formatting_deb822_normalize_stanza_order": False,
+ "formatting_deb822_normalize_field_content": True,
+}
+
@dataclasses.dataclass(slots=True, frozen=True, kw_only=True)
class PreferenceOption(Generic[PT]):
@@ -400,7 +425,6 @@ class EffectivePreference:
a_value = getattr(a, attr_name)
b_value = getattr(b, attr_name)
if a_value != b_value:
- print(f"{attr_name} was misaligned")
return None
return a
@@ -415,6 +439,9 @@ class EffectivePreference:
),
)
+ def replace(self, /, **changes: Any) -> Self:
+ return dataclasses.replace(self, **changes)
+
@dataclasses.dataclass(slots=True, frozen=True)
class MaintainerPreference(EffectivePreference):
@@ -539,14 +566,45 @@ def extract_maint_email(maint: str) -> str:
def determine_effective_style(
style_preference_table: StylePreferenceTable,
- source_package: SourcePackage,
+ source_package: Optional[SourcePackage],
+ salsa_ci: Optional[CommentedMap],
) -> Optional[EffectivePreference]:
- style = source_package.fields.get("X-Style")
+ 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)
+ if salsa_ci:
+ disable_wrap_and_sort = salsa_ci.mlget(
+ ["variables", "SALSA_CI_DISABLE_WRAP_AND_SORT"],
+ list_ok=True,
+ default=True,
+ )
+
+ if isinstance(disable_wrap_and_sort, str):
+ disable_wrap_and_sort = disable_wrap_and_sort in ("yes", "1", "true")
+ elif not isinstance(disable_wrap_and_sort, (int, bool)):
+ disable_wrap_and_sort = True
+ else:
+ disable_wrap_and_sort = (
+ disable_wrap_and_sort is True or disable_wrap_and_sort == 1
+ )
+ if not disable_wrap_and_sort:
+ wrap_and_sort_options = salsa_ci.mlget(
+ ["variables", "SALSA_CI_WRAP_AND_SORT_ARGS"],
+ list_ok=True,
+ default=None,
+ )
+ 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)
+
+ if source_package is None:
+ return None
+
maint = source_package.fields.get("Maintainer")
if maint is None:
return None
@@ -580,3 +638,33 @@ def determine_effective_style(
if isinstance(r, MaintainerPreference):
return r.as_effective_pref()
return r
+
+
+def _split_options(args: Iterable[str]) -> Iterable[str]:
+ for arg in args:
+ if arg.startswith("--"):
+ yield arg
+ continue
+ if not arg.startswith("-") or len(arg) < 2:
+ yield arg
+ continue
+ for sarg in arg[1:]:
+ yield f"-{sarg}"
+
+
+@functools.lru_cache
+def parse_salsa_ci_wrap_and_sort_args(args: str) -> Optional[EffectivePreference]:
+ options = dict(_WAS_DEFAULTS)
+ for arg in _split_options(args.split()):
+ v = _WAS_OPTIONS.get(arg)
+ if v is None:
+ return None
+ varname, value = v
+ if varname is None:
+ continue
+ options[varname] = value
+ if "DISABLE_NORMALIZE_STANZA_ORDER" in options:
+ del options["DISABLE_NORMALIZE_STANZA_ORDER"]
+ options["formatting_deb822_normalize_stanza_order"] = False
+
+ return EffectivePreference(**options) # type: ignore
diff --git a/src/debputy/packages.py b/src/debputy/packages.py
index 4dfdd49..3a6ee16 100644
--- a/src/debputy/packages.py
+++ b/src/debputy/packages.py
@@ -11,6 +11,7 @@ from typing import (
overload,
)
+from debian.deb822 import Deb822
from debian.debian_support import DpkgArchTable
from ._deb_options_profiles import DebBuildOptionsAndProfiles
@@ -18,7 +19,11 @@ from .architecture_support import (
DpkgArchitectureBuildProcessValuesTable,
dpkg_architecture_table,
)
-from .lsp.vendoring._deb822_repro import parse_deb822_file, Deb822ParagraphElement
+from .lsp.vendoring._deb822_repro import (
+ parse_deb822_file,
+ Deb822ParagraphElement,
+ Deb822FileElement,
+)
from .util import DEFAULT_PACKAGE_TYPE, UDEB_PACKAGE_TYPE, _error, active_profiles_match
_MANDATORY_BINARY_PACKAGE_FIELD = [
@@ -68,7 +73,7 @@ class DctrlParser:
def parse_source_debian_control(
self,
debian_control_lines: Iterable[str],
- ) -> Tuple["SourcePackage", Dict[str, "BinaryPackage"]]: ...
+ ) -> Tuple[Deb822FileElement, "SourcePackage", Dict[str, "BinaryPackage"]]: ...
@overload
def parse_source_debian_control(
@@ -76,21 +81,28 @@ class DctrlParser:
debian_control_lines: Iterable[str],
*,
ignore_errors: bool = False,
- ) -> Tuple[Optional["SourcePackage"], Optional[Dict[str, "BinaryPackage"]]]: ...
+ ) -> Tuple[
+ Deb822FileElement,
+ Optional["SourcePackage"],
+ Optional[Dict[str, "BinaryPackage"]],
+ ]: ...
def parse_source_debian_control(
self,
debian_control_lines: Iterable[str],
*,
ignore_errors: bool = False,
- ) -> Tuple[Optional["SourcePackage"], Optional[Dict[str, "BinaryPackage"]]]:
- dctrl_paragraphs = list(
- parse_deb822_file(
- debian_control_lines,
- accept_files_with_error_tokens=ignore_errors,
- accept_files_with_duplicated_fields=ignore_errors,
- )
+ ) -> Tuple[
+ Optional[Deb822FileElement],
+ Optional["SourcePackage"],
+ Optional[Dict[str, "BinaryPackage"]],
+ ]:
+ deb822_file = parse_deb822_file(
+ debian_control_lines,
+ accept_files_with_error_tokens=ignore_errors,
+ accept_files_with_duplicated_fields=ignore_errors,
)
+ dctrl_paragraphs = list(deb822_file)
if len(dctrl_paragraphs) < 2:
if not ignore_errors:
_error(
@@ -99,7 +111,7 @@ class DctrlParser:
source_package = (
SourcePackage(dctrl_paragraphs[0]) if dctrl_paragraphs else None
)
- return source_package, None
+ return deb822_file, source_package, None
source_package = SourcePackage(dctrl_paragraphs[0])
bin_pkgs = []
@@ -107,9 +119,16 @@ class DctrlParser:
if ignore_errors:
if "Package" not in p:
continue
- for f in _MANDATORY_BINARY_PACKAGE_FIELD:
- if f not in p:
- p[f] = "unknown"
+ missing_field = any(f not in p for f in _MANDATORY_BINARY_PACKAGE_FIELD)
+ if missing_field:
+ # In the LSP context, it is problematic if we "add" fields as it ranges and provides invalid
+ # results. However, `debputy` also needs the mandatory fields to be there, so we clone the
+ # stanzas that `debputy` (build) will see to add missing fields.
+ copy = Deb822(p)
+ for f in _MANDATORY_BINARY_PACKAGE_FIELD:
+ if f not in p:
+ copy[f] = "unknown"
+ p = copy
bin_pkgs.append(
_create_binary_package(
p,
@@ -137,7 +156,7 @@ class DctrlParser:
f"The following *excluded* packages (-N) are not listed in debian/control: {sorted(unknown)}"
)
- return source_package, bin_pkgs_table
+ return deb822_file, source_package, bin_pkgs_table
def _check_package_sets(
diff --git a/tests/lint_tests/lint_tutil.py b/tests/lint_tests/lint_tutil.py
index 8290712..08120ee 100644
--- a/tests/lint_tests/lint_tutil.py
+++ b/tests/lint_tests/lint_tutil.py
@@ -52,7 +52,7 @@ class LintWrapper:
binary_packages = None
dctrl_lines = self.dctrl_lines
if dctrl_lines is not None:
- source_package, binary_packages = (
+ _, source_package, binary_packages = (
self._dctrl_parser.parse_source_debian_control(
dctrl_lines, ignore_errors=True
)
diff --git a/tests/lsp_tests/test_lsp_dctrl.py b/tests/lsp_tests/test_lsp_dctrl.py
index 2bc90ba..e93ba17 100644
--- a/tests/lsp_tests/test_lsp_dctrl.py
+++ b/tests/lsp_tests/test_lsp_dctrl.py
@@ -1,4 +1,7 @@
import textwrap
+from typing import Optional
+
+import pytest
from debputy.lsp.debputy_ls import DebputyLanguageServer
@@ -56,6 +59,165 @@ def test_dctrl_complete_field(ls: "DebputyLanguageServer") -> None:
assert "Package" not in keywords
assert "Source" not in keywords
+ cursor_pos = put_doc_with_cursor(
+ ls,
+ dctrl_uri,
+ "debian/control",
+ textwrap.dedent(
+ """\
+ Source: foo
+
+ Package: foo
+ <CURSOR>
+ Architecture: any
+"""
+ ),
+ )
+
+ matches = _debian_control_completions(
+ ls,
+ CompletionParams(TextDocumentIdentifier(dctrl_uri), cursor_pos),
+ )
+ assert isinstance(matches, list)
+ keywords = {m.label for m in matches}
+ assert "Multi-Arch" in keywords
+ # Should be considered present even though it is parsed as two stanzas with a space
+ assert "Architecture" not in keywords
+ # Already present or wrong section
+ assert "Package" not in keywords
+ assert "Source" not in keywords
+
+ cursor_pos = put_doc_with_cursor(
+ ls,
+ dctrl_uri,
+ "debian/control",
+ textwrap.dedent(
+ """\
+ Source: foo
+
+ Package: foo
+ Sec<CURSOR>
+ Architecture: any
+"""
+ ),
+ )
+
+ matches = _debian_control_completions(
+ ls,
+ CompletionParams(TextDocumentIdentifier(dctrl_uri), cursor_pos),
+ )
+ assert isinstance(matches, list)
+ keywords = {m.label for m in matches}
+ # Included since we rely on client filtering (some clients let "RRR" match "R(ules-)R(equires-)R(oot), etc).
+ assert "Multi-Arch" in keywords
+ # Should be considered present even though it is parsed as two stanzas with an error
+ assert "Architecture" not in keywords
+ # Already present or wrong section
+ assert "Package" not in keywords
+ assert "Source" not in keywords
+
+
+@pytest.mark.parametrize(
+ "case,is_arch_all",
+ [
+ ("Architecture: any\n<CURSOR>", False),
+ ("Architecture: any\nM-A<CURSOR>", False),
+ ("<CURSOR>\nArchitecture: any", False),
+ ("M-A<CURSOR>\nArchitecture: any", False),
+ ("Architecture: all\n<CURSOR>", True),
+ ("Architecture: all\nM-A<CURSOR>", True),
+ ("<CURSOR>\nArchitecture: all", True),
+ ("M-A<CURSOR>\nArchitecture: all", True),
+ # Does not have architecture
+ ("M-A<CURSOR>", None),
+ ],
+)
+def test_dctrl_complete_field_context(
+ ls: "DebputyLanguageServer",
+ case: str,
+ is_arch_all: Optional[bool],
+) -> None:
+ dctrl_uri = "file:///nowhere/debian/control"
+
+ content = textwrap.dedent(
+ """\
+ Source: foo
+
+ Package: foo
+ {CASE}
+"""
+ ).format(CASE=case)
+ cursor_pos = put_doc_with_cursor(
+ ls,
+ dctrl_uri,
+ "debian/control",
+ content,
+ )
+
+ matches = _debian_control_completions(
+ ls,
+ CompletionParams(TextDocumentIdentifier(dctrl_uri), cursor_pos),
+ )
+ assert isinstance(matches, list)
+ keywords = {m.label for m in matches}
+ # Missing Architecture counts as "arch:all" by the completion logic
+ if is_arch_all is False:
+ assert "X-DH-Build-For-Type" in keywords
+ else:
+ assert "X-DH-Build-For-Type" not in keywords
+
+
+def test_dctrl_complete_field_value_context(ls: "DebputyLanguageServer") -> None:
+ dctrl_uri = "file:///nowhere/debian/control"
+
+ content = textwrap.dedent(
+ """\
+ Source: foo
+
+ Package: foo
+ Architecture: any
+ Multi-Arch: <CURSOR>
+"""
+ )
+ cursor_pos = put_doc_with_cursor(
+ ls,
+ dctrl_uri,
+ "debian/control",
+ content,
+ )
+
+ matches = _debian_control_completions(
+ ls,
+ CompletionParams(TextDocumentIdentifier(dctrl_uri), cursor_pos),
+ )
+ assert isinstance(matches, list)
+ keywords = {m.label for m in matches}
+ assert keywords == {"no", "same", "foreign", "allowed"}
+
+ content = textwrap.dedent(
+ """\
+ Source: foo
+
+ Package: foo
+ Architecture: all
+ Multi-Arch: <CURSOR>
+"""
+ )
+ cursor_pos = put_doc_with_cursor(
+ ls,
+ dctrl_uri,
+ "debian/control",
+ content,
+ )
+
+ matches = _debian_control_completions(
+ ls,
+ CompletionParams(TextDocumentIdentifier(dctrl_uri), cursor_pos),
+ )
+ assert isinstance(matches, list)
+ keywords = {m.label for m in matches}
+ assert keywords == {"no", "foreign", "allowed"}
+
def test_dctrl_hover_doc_field(ls: "DebputyLanguageServer") -> None:
dctrl_uri = "file:///nowhere/debian/control"
diff --git a/tests/test_style.py b/tests/test_style.py
index 9ce83bd..cae4510 100644
--- a/tests/test_style.py
+++ b/tests/test_style.py
@@ -1,7 +1,15 @@
+from typing import Mapping, Any, Optional
+
import pytest
from debian.deb822 import Deb822
-
-from debputy.lsp.style_prefs import StylePreferenceTable, determine_effective_style
+from debputy.yaml.compat import CommentedMap
+
+from debputy.lsp.style_prefs import (
+ StylePreferenceTable,
+ determine_effective_style,
+ EffectivePreference,
+ _WAS_DEFAULTS,
+)
from debputy.packages import SourcePackage
@@ -63,7 +71,7 @@ def test_compat_styles() -> None:
)
src = SourcePackage(fields)
- effective_style = determine_effective_style(styles, src)
+ effective_style = determine_effective_style(styles, src, None)
assert effective_style == nt_pref
fields["Uploaders"] = (
@@ -71,7 +79,7 @@ def test_compat_styles() -> None:
)
src = SourcePackage(fields)
- effective_style = determine_effective_style(styles, src)
+ effective_style = determine_effective_style(styles, src, None)
assert effective_style == nt_pref
assert effective_style == zeha_pref
@@ -80,7 +88,7 @@ def test_compat_styles() -> None:
)
src = SourcePackage(fields)
- effective_style = determine_effective_style(styles, src)
+ effective_style = determine_effective_style(styles, src, None)
assert effective_style is None
@@ -100,7 +108,7 @@ def test_compat_styles_team_maint() -> None:
assert "random@example.org" not in styles.maintainer_preferences
team_style = styles.maintainer_preferences["team@lists.debian.org"]
assert team_style.is_packaging_team
- effective_style = determine_effective_style(styles, src)
+ effective_style = determine_effective_style(styles, src, None)
assert effective_style == team_style.as_effective_pref()
@@ -117,5 +125,105 @@ def test_x_style() -> None:
assert "random@example.org" not in styles.maintainer_preferences
assert "black" in styles.named_styles
black_style = styles.named_styles["black"]
- effective_style = determine_effective_style(styles, src)
+ effective_style = determine_effective_style(styles, src, None)
assert effective_style == black_style
+
+
+def test_was_from_salsa_ci_style() -> None:
+ styles = StylePreferenceTable.load_styles()
+ fields = Deb822(
+ {
+ "Package": "foo",
+ "Maintainer": "Random Developer <random@example.org>",
+ },
+ )
+ src = SourcePackage(fields)
+ assert "random@example.org" not in styles.maintainer_preferences
+ effective_style = determine_effective_style(styles, src, None)
+ assert effective_style is None
+ salsa_ci = CommentedMap(
+ {"variables": CommentedMap({"SALSA_CI_DISABLE_WRAP_AND_SORT": "yes"})}
+ )
+ effective_style = determine_effective_style(styles, src, salsa_ci)
+ assert effective_style is None
+
+ salsa_ci = CommentedMap(
+ {"variables": CommentedMap({"SALSA_CI_DISABLE_WRAP_AND_SORT": "no"})}
+ )
+ effective_style = determine_effective_style(styles, src, salsa_ci)
+ was_style = EffectivePreference(**_WAS_DEFAULTS)
+ assert effective_style == was_style
+
+
+@pytest.mark.parametrize(
+ "was_args,style_delta",
+ [
+ (
+ "-a",
+ {
+ "formatting_deb822_always_wrap": True,
+ },
+ ),
+ (
+ "-sa",
+ {
+ "formatting_deb822_always_wrap": True,
+ "formatting_deb822_short_indent": True,
+ },
+ ),
+ (
+ "-sa --keep-first",
+ {
+ "formatting_deb822_always_wrap": True,
+ "formatting_deb822_short_indent": True,
+ },
+ ),
+ (
+ "-sab --keep-first",
+ {
+ "formatting_deb822_always_wrap": True,
+ "formatting_deb822_short_indent": True,
+ "formatting_deb822_normalize_stanza_order": True,
+ },
+ ),
+ (
+ "-sab --no-keep-first",
+ {
+ "formatting_deb822_always_wrap": True,
+ "formatting_deb822_short_indent": True,
+ "formatting_deb822_normalize_stanza_order": False,
+ },
+ ),
+ ],
+)
+def test_was_from_salsa_ci_style_args(
+ was_args: str, style_delta: Optional[Mapping[str, Any]]
+) -> None:
+ styles = StylePreferenceTable.load_styles()
+ fields = Deb822(
+ {
+ "Package": "foo",
+ "Maintainer": "Random Developer <random@example.org>",
+ },
+ )
+ src = SourcePackage(fields)
+ assert "random@example.org" not in styles.maintainer_preferences
+ salsa_ci = CommentedMap(
+ {
+ "variables": CommentedMap(
+ {
+ "SALSA_CI_DISABLE_WRAP_AND_SORT": "no",
+ "SALSA_CI_WRAP_AND_SORT_ARGS": was_args,
+ }
+ )
+ }
+ )
+ effective_style = determine_effective_style(styles, src, salsa_ci)
+ if style_delta is None:
+ assert effective_style is None
+ else:
+ was_style = EffectivePreference(**_WAS_DEFAULTS).replace(
+ **style_delta,
+ )
+
+ assert effective_style == was_style