add updates libs

This commit is contained in:
Christoph Brandau
2026-06-18 17:21:58 +02:00
parent 91ce762570
commit 41f331d4c4
150 changed files with 8249 additions and 2550 deletions
+149 -61
View File
@@ -1,22 +1,27 @@
import argparse
import dataclasses
import logging
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import uuid
from pathlib import Path
from typing import Optional
from lsprotocol import types as lsp
from pygls.server import LanguageServer
from pygls.workspace import TextDocument
from pygls.uris import to_fs_path
from pygls.workspace import TextDocument
from tclint.cli import tclint
from tclint.config import get_config, DEFAULT_CONFIGS, RunConfig, Config, ConfigError
from tclint.cli import tclint, utils
from tclint.commands.plugins import PluginManager
from tclint.config import (
DEFAULT_CONFIGS,
Config,
ConfigError,
SpacesInBraces,
load_config_at,
)
from tclint.format import Formatter, FormatterOpts
from tclint.lexer import TclSyntaxError
from tclint.parser import Parser
from tclint.cli import utils
try:
from tclint._version import __version__ # type: ignore
@@ -25,13 +30,14 @@ except ModuleNotFoundError:
DIAGNOSTIC_SOURCE = "tclint"
_DEFAULT_CONFIG = Config()
def lint(source, config, path):
def lint(source, config, plugin_manager, path):
diagnostics = []
try:
violations = tclint.lint(source, config, path)
violations = tclint.lint(source, config, plugin_manager, path)
except TclSyntaxError as e:
return [
lsp.Diagnostic(
@@ -62,7 +68,7 @@ def lint(source, config, path):
start=start,
end=end,
),
code=violation.id,
code=str(violation.id),
source=DIAGNOSTIC_SOURCE,
)
)
@@ -83,15 +89,26 @@ class TclspServer(LanguageServer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.diagnostics = {}
self.global_config: RunConfig = None
# Maps workspace roots to configs.
self.configs: Dict[Path, RunConfig] = {}
# There are many config caches!!
# Caches loaded config files specified in LSP settings.
self.workspace_configs: dict[Path, Config] = {}
# Caches loaded config files present in open workspaces.
self.config_files: dict[Path, Config] = {}
# Caches which config is used by each open file.
self.source_configs: dict[Path, Config] = {}
# Tracks which invalid configs we've already displayed an error for, to avoid
# spam.
self.invalid_configs: set[Path] = set()
self.client_supports_refresh = False
self.global_settings = ExtensionSettings()
self.workspace_settings: Dict[Path, ExtensionSettings] = {}
self.workspace_settings: dict[Path, ExtensionSettings] = {}
def get_roots(self) -> List[Path]:
self.plugin_manager = PluginManager()
def get_roots(self) -> list[Path]:
"""Returns root folders currently open in the workspace."""
roots = []
for uri in self.workspace.folders.keys():
@@ -108,10 +125,8 @@ class TclspServer(LanguageServer):
return roots
def get_root(self, path: Path) -> Optional[Path]:
"""Returns workspace root folder that's closest to path.
Returns None if path is not in a workspace folder or if there are no workspace
folders.
"""Returns workspace root if path is in a workspace folder. Otherwise, returns
None.
"""
roots = self.get_roots()
closest_root = None
@@ -124,55 +139,98 @@ class TclspServer(LanguageServer):
if len(relpath.parts) < distance:
distance = len(relpath.parts)
closest_root = root
return closest_root
def get_config_file(self, workspace_root: Path) -> Optional[Path]:
if workspace_root in self.workspace_settings:
settings = self.workspace_settings[workspace_root]
def get_config_file(self, root: Path) -> Optional[Path]:
if root in self.workspace_settings:
settings = self.workspace_settings[root]
return settings.config_file
return self.global_settings.config_file
def load_configs(self):
self.configs = {}
def show_config_error(self, msg: str, path: Path):
if path not in self.invalid_configs:
self.show_message(f"Error loading config file: {msg}")
self.invalid_configs.add(path)
def load_config(self, path: Path, root: Path) -> Optional[Config]:
try:
return Config.from_path(path, root)
except FileNotFoundError:
self.show_config_error(f"{path} doesn't exist", path)
return None
except ConfigError as e:
self.show_config_error(str(e), path)
return None
def load_workspace_setting_configs(self):
"""These may be used a lot if specified, so cache specially."""
for root in self.get_roots():
try:
path = self.get_config_file(root)
config = get_config(path, root)
path = self.get_config_file(root)
if path is None:
continue
config = self.load_config(path, root)
if config is None:
continue
self.workspace_configs[root] = config
def _get_config(self, path: Path) -> Config:
workspace_root = self.get_root(path)
# First, check for configs specified in the LSP settings.
# If not in a workspace, our only shot is to use a global config. Otherwise, we
# bail (no searching, since the LSP only searches up to the workspace root).
if workspace_root is None:
global_file = self.global_settings.config_file
if global_file is not None:
config = self.load_config(global_file, path.parent)
if config is not None:
self.configs[root] = config
except ConfigError as e:
self.show_message(f"Error loading config file: {e}")
return config
return _DEFAULT_CONFIG
# If a global config file exists, we apply it to any file not under a workspace
# folder.
global_path = self.global_settings.config_file
if global_path is not None:
# If file is in a workspace, and we've got a workspace config configured, use
# that (this logic also handles global configs, since these are still
# instantiated once per workspace to resolve relative paths).
if workspace_root is not None and workspace_root in self.workspace_configs:
return self.workspace_configs[workspace_root]
# Otherwise, walk upwards until root.
# path is a file, which is a sneaky trick to guarantee we always run the first
# iteration. It becomes a directory after the first statement in the loop.
while path != workspace_root:
path = path.parent
try:
config = get_config(global_path, global_path.parent)
self.global_config = config
config = load_config_at(path)
except ConfigError as e:
self.show_message(f"Error loading config file: {e}")
self.show_config_error(str(e), path)
return _DEFAULT_CONFIG
def get_config(self, path: Path, root: Optional[Path]) -> Config:
if root in self.configs:
return self.configs[root].get_for_path(path)
if self.global_config is not None:
return self.global_config.get_for_path(path)
return Config()
if config is not None:
return config
def _compute_diagnostics(self, document: TextDocument) -> List[lsp.Diagnostic]:
return _DEFAULT_CONFIG
def get_config(self, path: Path) -> Config:
"""Return config object for a given path.
If no config has already been loaded for root (either by calling this function
or load_configs), this function will search for and load a config file if found.
"""
if path in self.source_configs:
return self.source_configs[path]
config = self._get_config(path)
self.source_configs[path] = config
return config
def _compute_diagnostics(self, document: TextDocument) -> list[lsp.Diagnostic]:
path = Path(document.path)
root = self.get_root(path)
config = self.get_config(path, root)
if root is None:
root = path.parent
config = self.get_config(path)
is_excluded = utils.make_exclude_filter(config.exclude)
if is_excluded(path, root):
if is_excluded(path):
return []
return lint(document.source, config, path)
return lint(document.source, config, self.plugin_manager, path)
def compute_diagnostics(self, document: TextDocument):
# `None` sentinel ensures that `diagnostics` gets updated if the URI is not
@@ -189,11 +247,10 @@ class TclspServer(LanguageServer):
self,
document: TextDocument,
options: lsp.FormattingOptions,
range: Optional[Tuple[int, int]] = None,
range: Optional[tuple[int, int]] = None,
):
path = Path(document.path)
root = self.get_root(path)
config = self.get_config(path, root)
config = self.get_config(path)
parser = Parser()
@@ -205,9 +262,19 @@ class TclspServer(LanguageServer):
formatter = Formatter(
FormatterOpts(
indent=indent,
spaces_in_braces=config.style_spaces_in_braces,
indent_mixed_tab_size=config.get_indent_mixed_tab_size(),
spaces_in_braces=(
config.style_spaces_in_braces == SpacesInBraces.ALWAYS
or config.style_spaces_in_braces == SpacesInBraces.BALANCED_YES
),
balanced_spaces_in_braces=(
config.style_spaces_in_braces == SpacesInBraces.BALANCED_NO
or config.style_spaces_in_braces == SpacesInBraces.BALANCED_YES
),
max_blank_lines=config.style_max_blank_lines,
indent_namespace_eval=config.style_indent_namespace_eval,
emacs=False,
debug_whitespace=False,
)
)
@@ -230,13 +297,30 @@ def did_open(ls: TclspServer, params: lsp.DidOpenTextDocumentParams):
@server.feature(lsp.TEXT_DOCUMENT_DID_CHANGE)
def did_change(ls: TclspServer, params: lsp.DidOpenTextDocumentParams):
def did_change(ls: TclspServer, params: lsp.DidChangeTextDocumentParams):
"""Parse each document when it is changed"""
logging.debug("Received %s: %s", lsp.TEXT_DOCUMENT_DID_CHANGE, params)
doc = ls.workspace.get_text_document(params.text_document.uri)
ls.compute_diagnostics(doc)
@server.feature(lsp.TEXT_DOCUMENT_DID_CLOSE)
def did_close(ls: TclspServer, params: lsp.DidCloseTextDocumentParams):
"""Free up resources when a document is closed."""
logging.debug("Received %s: %s", lsp.TEXT_DOCUMENT_DID_CLOSE, params)
doc = ls.workspace.get_text_document(params.text_document.uri)
try:
del ls.diagnostics[doc.uri]
except KeyError:
pass
try:
del ls.source_configs[Path(doc.path)]
except KeyError:
pass
@server.feature(
lsp.TEXT_DOCUMENT_DIAGNOSTIC,
lsp.DiagnosticOptions(
@@ -275,7 +359,11 @@ def change_watched_files(ls: TclspServer, params: lsp.DidChangeWatchedFilesParam
# Clear diagnostics cache so they get recalculated when requested
ls.diagnostics = {}
ls.load_configs()
# Config files changed, clear the many caches!
ls.config_files = {}
ls.source_configs = {}
ls.invalid_configs = set()
if ls.client_supports_refresh:
ls.lsp.send_request(lsp.WORKSPACE_DIAGNOSTIC_REFRESH, None)
@@ -369,7 +457,7 @@ def init(ls: TclspServer, params: lsp.InitializeParams):
capabilities = ls.client_capabilities.workspace
try:
ls.client_supports_refresh = (
ls.client_supports_refresh = bool(
capabilities.diagnostics.refresh_support # type: ignore[union-attr]
)
except AttributeError:
@@ -391,7 +479,7 @@ def init(ls: TclspServer, params: lsp.InitializeParams):
for settings in (ls.global_settings, *ls.workspace_settings.values()):
if settings.config_file is not None:
watchers.append(
lsp.FileSystemWatcher(glob_pattern=settings.config_file)
lsp.FileSystemWatcher(glob_pattern=str(settings.config_file))
)
ls.register_capability(
@@ -408,7 +496,7 @@ def init(ls: TclspServer, params: lsp.InitializeParams):
)
)
ls.load_configs()
ls.load_workspace_setting_configs()
def main():