# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. """Implementation of tool support over LSP.""" from __future__ import annotations import json import operator import os import pathlib import re import sys import threading from collections import ChainMap from functools import reduce from typing import Any, Optional # ********************************************************** # Update sys.path before importing any bundled libraries. # ********************************************************** def update_sys_path(path_to_add: str, strategy: str) -> None: """Add given path to `sys.path`.""" if path_to_add not in sys.path and os.path.isdir(path_to_add): if strategy == "useBundled": sys.path.insert(0, path_to_add) elif strategy == "fromEnvironment": sys.path.append(path_to_add) # Ensure that we can import LSP libraries, and other bundled libraries. update_sys_path( os.fspath(pathlib.Path(__file__).parent.parent / "libs"), os.getenv("LS_IMPORT_STRATEGY", "useBundled"), ) # ********************************************************** # Imports needed for the language server goes below this. # ********************************************************** # pylint: disable=wrong-import-position,import-error import lsp_jsonrpc as jsonrpc import lsprotocol.types as lsp from common.load_data import standard_items from lsp_tclserver import TclLanguageServer from pygls import uris, workspace from pygls.workspace.text_document import TextDocument from tools.folding_ranges import build_folding_ranges from tools.inlay_hint import ( InlayHintGenerator, build_builtin_inlay_signatures, ) from tools.navigation import ( SymbolIdentity, definition_identities, matching_occurrences, symbol_at_position, workspace_symbols, ) from tools.semantic_tokens import ( TOKEN_TYPE_INDEX, TOKEN_TYPES, TokenModifier, _Highlighter, ) from tools.signature_help import build_signature_help WORKSPACE_SETTINGS = {} GLOBAL_SETTINGS = {} MAX_WORKERS = 5 LSP_SERVER = TclLanguageServer( name="NX Postprocessor Support", version="0.0.1", max_workers=MAX_WORKERS ) BUILTIN_PROC_NAMES = { item.label for item in standard_items.tcl_keyword_list + standard_items.nx_procs } BUILTIN_VARIABLE_NAMES = {item.label for item in standard_items.nx_variables} STATIC_COMPLETION_ITEMS = tuple( standard_items.tcl_keyword_list + standard_items.nx_procs + standard_items.nx_variables ) STATIC_COMPLETION_KEYS = frozenset( (item.label, getattr(item, "kind", None)) for item in STATIC_COMPLETION_ITEMS ) BUILTIN_INLAY_SIGNATURES = build_builtin_inlay_signatures( standard_items.json_data.get("MOM_procs", []) ) BUILTIN_HOVER_ITEMS = {} for _hover_item in ( standard_items.json_data.get("MOM_procs", []) + standard_items.json_data.get("mom_variables", []) ): BUILTIN_HOVER_ITEMS.setdefault(_hover_item.get("label"), _hover_item) # ********************************************************** # Tool specific code goes below this. # ********************************************************** # Delete "Linting features" section if your tool is NOT a linter. # ********************************************************** # Linting features start here # ********************************************************** # See `pylint` implementation for a full featured linter extension: # Pylint: https://github.com/microsoft/vscode-pylint/blob/main/bundled/tool @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_OPEN) def did_open(params: lsp.DidOpenTextDocumentParams) -> None: """LSP handler for textDocument/didOpen request.""" document = LSP_SERVER.workspace.get_text_document(params.text_document.uri) LSP_SERVER.clear_cache_for_uri(document.uri) LSP_SERVER.analyze_document_now(document) @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE) def did_save(params: lsp.DidSaveTextDocumentParams) -> None: """LSP handler for textDocument/didSave request.""" document = LSP_SERVER.workspace.get_text_document(params.text_document.uri) LSP_SERVER.analyze_document_now(document) @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_CLOSE) def did_close(params: lsp.DidCloseTextDocumentParams) -> None: """LSP handler for textDocument/didClose request.""" uri = params.text_document.uri LSP_SERVER.remove_file_state(uri) _index_tcl_file_from_disk(uri) @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_CHANGE) def did_change(params: lsp.DidChangeTextDocumentParams) -> None: """LSP handler for textDocument/didChange request""" document = LSP_SERVER.workspace.get_text_document(params.text_document.uri) LSP_SERVER.clear_cache_for_uri(document.uri) LSP_SERVER.schedule_document_analysis(document) FILE_OPERATION_OPTIONS = lsp.FileOperationRegistrationOptions( filters=[ lsp.FileOperationFilter( scheme="file", pattern=lsp.FileOperationPattern(glob="**/*"), ) ] ) def _index_tcl_file_from_disk(uri: str) -> None: if not uri.startswith("file:"): return path = pathlib.Path(uris.to_fs_path(uri)) if path.suffix.lower() != ".tcl" or not path.is_file(): return try: document = TextDocument(uri=uri, language_id="tcl") LSP_SERVER.update_poco_completion_for_file( document, cache_tree=False, require_file_exists=True, ) except (OSError, UnicodeError) as error: log_warning(f"Could not re-index {path}: {error}") @LSP_SERVER.feature(lsp.WORKSPACE_DID_DELETE_FILES, FILE_OPERATION_OPTIONS) def did_delete_files(params: lsp.DeleteFilesParams) -> None: for deleted_file in params.files: LSP_SERVER.remove_file_state(deleted_file.uri) @LSP_SERVER.feature(lsp.WORKSPACE_DID_RENAME_FILES, FILE_OPERATION_OPTIONS) def did_rename_files(params: lsp.RenameFilesParams) -> None: for renamed_file in params.files: old_path = pathlib.Path(uris.to_fs_path(renamed_file.old_uri)) new_path = pathlib.Path(uris.to_fs_path(renamed_file.new_uri)) indexed_paths = LSP_SERVER.indexed_paths_under_uri(renamed_file.old_uri) new_index_paths: set[pathlib.Path] = set() for indexed_path in indexed_paths: if LSP_SERVER.paths_equal(indexed_path, old_path): new_index_paths.add(new_path) else: relative_path = os.path.relpath(indexed_path, old_path) new_index_paths.add(new_path / relative_path) # Also handles renaming a previously unindexed file to a TCL file. if new_path.suffix.lower() == ".tcl": new_index_paths.add(new_path) LSP_SERVER.remove_file_state(renamed_file.old_uri) for new_index_path in new_index_paths: _index_tcl_file_from_disk(new_index_path.as_uri()) @LSP_SERVER.feature(lsp.WORKSPACE_DID_CHANGE_WATCHED_FILES) def did_change_watched_files(params: lsp.DidChangeWatchedFilesParams) -> None: """Keep indexes for closed Tcl files synchronized with disk changes.""" for change in params.changes: if change.type == lsp.FileChangeType.Deleted: LSP_SERVER.remove_file_state(change.uri) else: _index_tcl_file_from_disk(change.uri) @LSP_SERVER.feature( lsp.TEXT_DOCUMENT_DIAGNOSTIC, lsp.DiagnosticOptions( identifier="pull-diagnostics", inter_file_dependencies=False, workspace_diagnostics=False, ), ) def document_diagnostic(params: lsp.DocumentDiagnosticParams): """Return diagnostics for the requested document""" uri = params.text_document.uri doc = LSP_SERVER.workspace.get_text_document(uri) diagnostic_state = LSP_SERVER.diagnostic_snapshot(uri) was_cached = ( diagnostic_state is not None and diagnostic_state[0] == doc.version ) if not was_cached: LSP_SERVER.compute_diagnostics(doc) diagnostic_state = LSP_SERVER.diagnostic_snapshot(uri) if diagnostic_state is None: return lsp.FullDocumentDiagnosticReport(items=[]) version, diagnostics = diagnostic_state result_id = f"{uri}@{version}" if was_cached and result_id == params.previous_result_id: return lsp.UnchangedDocumentDiagnosticReport(result_id) return lsp.FullDocumentDiagnosticReport(items=diagnostics, result_id=result_id) @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION) def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList: from tools.completion_items import BUILTIN_VAR_LABELS doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri) workspace_items = LSP_SERVER.completion_items_snapshot() tree = LSP_SERVER.get_tree(doc) globals_set, procs_locals, proc_ranges = LSP_SERVER.variable_index_for_document( doc, tree ) # Always include globals (excluding built-ins) dynamic_items = [] for name in sorted(globals_set): if name not in BUILTIN_VAR_LABELS: dynamic_items.append( lsp.CompletionItem(label=name, kind=lsp.CompletionItemKind.Variable) ) # Include proc-local variables when cursor is inside that proc pos = params.position if pos is not None: for pr in proc_ranges: if pr.start_line <= pos.line <= (pr.end_line or pr.start_line): for name in sorted(procs_locals.get(pr.name, set())): # Exclude built-ins and globals to avoid duplication if name not in BUILTIN_VAR_LABELS and name not in globals_set: dynamic_items.append( lsp.CompletionItem( label=name, kind=lsp.CompletionItemKind.Variable ) ) break # Merge with de-duplication. Each file keeps its complete index, so a proc # declared in multiple files must only appear once in the completion list. merged: list[lsp.CompletionItem] = list(STATIC_COMPLETION_ITEMS) seen_items: set[tuple[str, lsp.CompletionItemKind | None]] = set( STATIC_COMPLETION_KEYS ) for it in (*workspace_items, *dynamic_items): key = (it.label, getattr(it, "kind", None)) if key in seen_items: continue seen_items.add(key) merged.append(it) return lsp.CompletionList(is_incomplete=False, items=merged) @LSP_SERVER.feature( lsp.TEXT_DOCUMENT_SIGNATURE_HELP, lsp.SignatureHelpOptions( trigger_characters=[" "], retrigger_characters=[" "], ), ) def signature_help(params: lsp.SignatureHelpParams) -> lsp.SignatureHelp | None: document = LSP_SERVER.workspace.get_text_document(params.text_document.uri) tree = LSP_SERVER.get_tree(document) custom_signatures, custom_docs = LSP_SERVER.proc_metadata_snapshot( document.path ) return build_signature_help( document.source, tree, params.position, custom_signatures, custom_docs, standard_items.json_data.get("MOM_procs", []), ) # @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL) # def document_symbols(params: lsp.DocumentSymbolParams): # doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri) # ast = LSP_SERVER.parser.parse(doc.source) # symbols = LSP_SERVER.extract_tcl_symbols(ast) # return symbols @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL) def document_symbols(params: lsp.DocumentSymbolParams): from tools.document_symbols import build_document_symbols doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri) return build_document_symbols(doc.source) @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_INLAY_HINT) def inlay_hints(params: lsp.InlayHintParams): document = LSP_SERVER.workspace.get_text_document(params.text_document.uri) settings = _get_settings_by_document(document) if not settings.get("inlayHint", False): return [] inlay_settings = settings.get("inlayHints", {}) parameter_names = inlay_settings.get("parameterNames", "all") if parameter_names == "none": return [] # Reuse cached AST tree = LSP_SERVER.get_tree(document) # Built-in NX procedures are the fallback. Workspace procedures replace them, # and a declaration in the current file wins over duplicate workspace names. custom_signatures = LSP_SERVER.custom_inlay_signatures_snapshot( document.path ) signatures = ChainMap(custom_signatures, BUILTIN_INLAY_SIGNATURES) generator = InlayHintGenerator( document.source, signatures, source_lines=LSP_SERVER.get_lines(document), requested_range=params.range, parameter_names=parameter_names, suppress_when_argument_matches_name=inlay_settings.get( "suppressWhenArgumentMatchesName", True ), ) return generator.generate(tree) @LSP_SERVER.feature( lsp.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL, lsp.SemanticTokensLegend( token_types=TOKEN_TYPES, token_modifiers=[m.name for m in TokenModifier], ), ) def semantic_tokens(params: lsp.SemanticTokensParams): document = LSP_SERVER.workspace.get_text_document(params.text_document.uri) data = [] plugins = [] hl = _Highlighter(plugins, LSP_SERVER.custom_function_names_snapshot()) # Reuse cached AST tree = LSP_SERVER.get_tree(document) tree.accept(hl, recurse=True) tokens = hl.tokens() for token in tokens: data.extend( [ token.line, token.offset, token.length, TOKEN_TYPE_INDEX[token.tok_type], reduce(operator.or_, token.tok_modifiers, 0), ] ) return lsp.SemanticTokens(data=data) @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_FOLDING_RANGE) def folding_ranges(params: lsp.FoldingRangeParams): document = LSP_SERVER.workspace.get_text_document(params.text_document.uri) tree = LSP_SERVER.get_tree(document) return build_folding_ranges(tree) @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_HOVER) def hover(params: lsp.HoverParams) -> lsp.Hover: pos = params.position document_uri = params.text_document.uri document = LSP_SERVER.workspace.get_text_document(document_uri) col = params.position.character try: line = LSP_SERVER.get_lines(document)[pos.line] except IndexError: return None # Do not show hover for proc name in its declaration from tools.proc_docs import is_proc_declaration_line if is_proc_declaration_line(line, pos.character): return None # Identify the token under the cursor for m in re.finditer(r"\b\w+\b", line): if m.start() <= col <= m.end(): token = m.group(0) break else: return None # 1) If token is a known MOM proc/variable, return built-in hover match = BUILTIN_HOVER_ITEMS.get(token) if match and match.get("kind") == "function": label = match.get("label", "") parameters = match.get("parameters", []) param_lines = ( "\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_" ) example_data = match.get("example", []) example_md = "\n".join(f"{line}" for line in example_data) returns_data = match.get("returns", ["None"]) returns_md = "\n".join(f"- {line}" for line in returns_data) doc_md = f"""\ ### 📘 {label} **Purpose** {match.get("description", "No description available.")} **Format** `{match.get("format", label)}` **Parameters** {param_lines} **Return value** {returns_md} **Example** ```tcl {example_md}""" return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=doc_md)) # 2) Otherwise, check if the token is a custom proc and show its preceding doc block # Build a merged map of proc -> docs gathered during initialization and updates proc_doc = LSP_SERVER.proc_documentation(token, document.path) if proc_doc is not None: return lsp.Hover( lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=proc_doc) ) return None @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DEFINITION) def goto_definition(params: lsp.DefinitionParams): """Resolve Tcl proc and variable definitions through the symbol index.""" context = _navigation_context(params.text_document.uri, params.position) if context is None: return None indexes, definitions, _, identity = context locations = [ lsp.Location(uri=index.uri, range=occurrence.range) for index, occurrence in matching_occurrences( identity, indexes, definitions ) if occurrence.is_definition ] return _sorted_locations(locations) or None def _navigation_context(uri: str, position: lsp.Position): indexes = LSP_SERVER.navigation_snapshot() filepath = str(pathlib.Path(uris.to_fs_path(uri))) index = indexes.get(filepath) if index is None or LSP_SERVER.index_update_pending(filepath): document = LSP_SERVER.workspace.get_text_document(uri) LSP_SERVER.update_poco_completion_for_file(document) indexes = LSP_SERVER.navigation_snapshot() index = indexes.get(filepath) if index is None: return None definitions = definition_identities(indexes) result = symbol_at_position(index, position, definitions) if result is None: return None occurrence, identity = result return indexes, definitions, occurrence, identity def _sorted_locations(locations: list[lsp.Location]) -> list[lsp.Location]: return sorted( locations, key=lambda location: ( location.uri, location.range.start.line, location.range.start.character, ), ) @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_REFERENCES) def references(params: lsp.ReferenceParams) -> list[lsp.Location]: context = _navigation_context(params.text_document.uri, params.position) if context is None: return [] indexes, definitions, _, identity = context locations = [ lsp.Location(uri=index.uri, range=occurrence.range) for index, occurrence in matching_occurrences( identity, indexes, definitions ) if params.context.include_declaration or not occurrence.is_definition ] return _sorted_locations(locations) def _is_renamable( identity: SymbolIdentity, indexes, definitions: set[SymbolIdentity], ) -> bool: if identity not in definitions or identity.kind not in {"proc", "variable"}: return False basename = identity.name.rsplit("::", 1)[-1] if identity.kind == "proc": if basename in BUILTIN_PROC_NAMES: return False definition_count = sum( occurrence.is_definition and occurrence.identity == identity for index in indexes.values() for occurrence in index.occurrences ) return definition_count == 1 return basename not in BUILTIN_VARIABLE_NAMES @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_PREPARE_RENAME) def prepare_rename(params: lsp.PrepareRenameParams): context = _navigation_context(params.text_document.uri, params.position) if context is None: return None indexes, definitions, occurrence, identity = context if not _is_renamable(identity, indexes, definitions): return None return lsp.PrepareRenameResult_Type1( range=occurrence.range, placeholder=occurrence.placeholder ) @LSP_SERVER.feature( lsp.TEXT_DOCUMENT_RENAME, lsp.RenameOptions(prepare_provider=True), ) def rename(params: lsp.RenameParams) -> lsp.WorkspaceEdit | None: if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", params.new_name): return None context = _navigation_context(params.text_document.uri, params.position) if context is None: return None indexes, definitions, _, identity = context if not _is_renamable(identity, indexes, definitions): return None changes: dict[str, list[lsp.TextEdit]] = {} seen = set() for index, occurrence in matching_occurrences(identity, indexes, definitions): key = ( index.uri, occurrence.range.start.line, occurrence.range.start.character, occurrence.range.end.line, occurrence.range.end.character, ) if key in seen: continue seen.add(key) changes.setdefault(index.uri, []).append( lsp.TextEdit(range=occurrence.range, new_text=params.new_name) ) for edits in changes.values(): edits.sort( key=lambda edit: ( edit.range.start.line, edit.range.start.character, ), reverse=True, ) return lsp.WorkspaceEdit(changes=changes) @LSP_SERVER.feature(lsp.WORKSPACE_SYMBOL) def workspace_symbol(params: lsp.WorkspaceSymbolParams): return workspace_symbols(LSP_SERVER.navigation_snapshot(), params.query) # ********************************************************** # Linting features end here # ********************************************************** # ********************************************************** # Formatting features start here # ********************************************************** # Sample implementations: # Black: https://github.com/microsoft/vscode-black-formatter/blob/main/bundled/tool # ********************************************************** # Formatting features ends here # ********************************************************** @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_FORMATTING) def formatting(params: lsp.DocumentFormattingParams) -> list[lsp.TextEdit] | None: """LSP handler for textDocument/formatting request.""" doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri) source = doc.source start = lsp.Position(line=0, character=0) last_line = source.rsplit("\n", 1)[-1] end = lsp.Position(line=source.count("\n"), character=len(last_line)) if GLOBAL_SETTINGS.get("formatter", True): source = LSP_SERVER.format(doc, params.options) return [ lsp.TextEdit( range=lsp.Range(start=start, end=end), new_text=source, ) ] # ********************************************************** # Required Language Server Initialization and Exit handlers. # ********************************************************** @LSP_SERVER.feature(lsp.WORKSPACE_DID_CHANGE_CONFIGURATION) def did_change_configuration(_: lsp.DidChangeConfigurationParams): """LSP Handler for Config Changes""" @LSP_SERVER.feature(lsp.INITIALIZE) def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult: """LSP handler for initialize request.""" log_to_output(f"CWD Server: {os.getcwd()}") paths = "\r\n ".join(sys.path) log_to_output(f"sys.path used to run Server:\r\n {paths}") GLOBAL_SETTINGS.update(**params.initialization_options.get("globalSettings", {})) settings = params.initialization_options["settings"] _update_workspace_settings(settings) log_to_output( f"Settings used to run Server:\r\n{json.dumps(settings, indent=4, ensure_ascii=False)}\r\n" ) log_to_output( f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n" ) semantic_tokens_legend = lsp.SemanticTokensLegend( token_types=TOKEN_TYPES, token_modifiers=[m.name for m in TokenModifier], ) return lsp.InitializeResult( capabilities=lsp.ServerCapabilities( document_formatting_provider=GLOBAL_SETTINGS.get("formatter", True), folding_range_provider=True, semantic_tokens_provider=lsp.SemanticTokensOptions( legend=semantic_tokens_legend, full=True, range=False ), definition_provider=True, references_provider=True, rename_provider=lsp.RenameOptions(prepare_provider=True), workspace_symbol_provider=True, ) ) @LSP_SERVER.feature(lsp.INITIALIZED) def initialized(_params: lsp.InitializedParams): """Kick off background indexing to avoid blocking initialization.""" def index_workspace(): try: try: root = LSP_SERVER.workspace.root_path except RuntimeError: root = None if not root: log_to_output("Background indexing skipped: no workspace folder is open.") return log_to_output("Background indexing started...") root_path = pathlib.Path(root) skipped_directories = { ".git", ".nox", ".venv", "dist", "node_modules", "out", } tcl_files = ( path for path in root_path.rglob("*.tcl") if not any( part.casefold() in skipped_directories for part in path.relative_to(root_path).parts[:-1] ) ) for filepath in sorted(tcl_files, key=lambda path: str(path).casefold()): try: document = TextDocument( uri=filepath.as_uri(), language_id="tcl" ) LSP_SERVER.update_poco_completion_for_file( document, cache_tree=False, require_file_exists=True, ) except Exception as error: log_to_output(f"Fehler beim Parsen von {filepath}: {error}") log_to_output("Background indexing completed.") except Exception as e: log_to_output(f"Background indexing failed: {e}") threading.Thread(target=index_workspace, name="nxps-indexer", daemon=True).start() @LSP_SERVER.feature(lsp.EXIT) def on_exit(_params: Optional[Any] = None) -> None: """Handle clean up on exit.""" jsonrpc.shutdown_json_rpc() @LSP_SERVER.feature(lsp.SHUTDOWN) def on_shutdown(_params: Optional[Any] = None) -> None: """Handle clean up on shutdown.""" jsonrpc.shutdown_json_rpc() def _get_global_defaults(): return { "path": GLOBAL_SETTINGS.get("path", []), "interpreter": GLOBAL_SETTINGS.get("interpreter", [sys.executable]), "args": GLOBAL_SETTINGS.get("args", []), "importStrategy": GLOBAL_SETTINGS.get("importStrategy", "useBundled"), "showNotifications": GLOBAL_SETTINGS.get("showNotifications", "off"), "formatter": GLOBAL_SETTINGS.get("formatter", True), "inlayHint": GLOBAL_SETTINGS.get("inlayHint", True), "inlayHints": GLOBAL_SETTINGS.get( "inlayHints", { "parameterNames": "all", "suppressWhenArgumentMatchesName": True, }, ), } def _update_workspace_settings(settings): if not settings: key = os.getcwd() WORKSPACE_SETTINGS[key] = { "cwd": key, "workspaceFS": key, "workspace": uris.from_fs_path(key), **_get_global_defaults(), } return for setting in settings: key = uris.to_fs_path(setting["workspace"]) WORKSPACE_SETTINGS[key] = { "cwd": key, **setting, "workspaceFS": key, } def _get_settings_by_path(file_path: pathlib.Path): workspaces = {s["workspaceFS"] for s in WORKSPACE_SETTINGS.values()} while file_path != file_path.parent: str_file_path = str(file_path) if str_file_path in workspaces: return WORKSPACE_SETTINGS[str_file_path] file_path = file_path.parent setting_values = list(WORKSPACE_SETTINGS.values()) return setting_values[0] def _get_document_key(document: workspace.Document): if WORKSPACE_SETTINGS: document_workspace = pathlib.Path(document.path) workspaces = {s["workspaceFS"] for s in WORKSPACE_SETTINGS.values()} # Find workspace settings for the given file. while document_workspace != document_workspace.parent: if str(document_workspace) in workspaces: return str(document_workspace) document_workspace = document_workspace.parent return None def _get_settings_by_document(document: workspace.Document | None): if document is None or document.path is None: return list(WORKSPACE_SETTINGS.values())[0] key = _get_document_key(document) if key is None: # This is either a non-workspace file or there is no workspace. key = os.fspath(pathlib.Path(document.path).parent) return { "cwd": key, "workspaceFS": key, "workspace": uris.from_fs_path(key), **_get_global_defaults(), } return WORKSPACE_SETTINGS[str(key)] # ***************************************************** # Logging and notification. # ***************************************************** def log_to_output( message: str, msg_type: lsp.MessageType = lsp.MessageType.Log ) -> None: LSP_SERVER.show_message_log(message, msg_type) def log_error(message: str) -> None: LSP_SERVER.show_message_log(message, lsp.MessageType.Error) if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["onError", "onWarning", "always"]: LSP_SERVER.show_message(message, lsp.MessageType.Error) def log_warning(message: str) -> None: LSP_SERVER.show_message_log(message, lsp.MessageType.Warning) if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["onWarning", "always"]: LSP_SERVER.show_message(message, lsp.MessageType.Warning) def log_always(message: str) -> None: LSP_SERVER.show_message_log(message, lsp.MessageType.Info) if os.getenv("LS_SHOW_NOTIFICATION", "off") in ["always"]: LSP_SERVER.show_message(message, lsp.MessageType.Info) # ***************************************************** # Start the server. # ***************************************************** if __name__ == "__main__": LSP_SERVER.start_io()