update formatter

This commit is contained in:
2025-07-20 11:53:40 +02:00
parent d77bd5dc29
commit 7c8634b971
6 changed files with 228 additions and 591 deletions
+24 -328
View File
@@ -13,7 +13,6 @@ import sys
import sysconfig
import traceback
from typing import Any, Optional, Sequence
from parser.parser import parse_tcl, format_tree
# **********************************************************
@@ -43,6 +42,8 @@ import lsp_utils as utils
import lsprotocol.types as lsp
from pygls import server, uris, workspace
from common.load_data import standard_items
from common.formatter import format_tcl
WORKSPACE_SETTINGS = {}
GLOBAL_SETTINGS = {}
@@ -105,71 +106,11 @@ def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]:
return lsp.CompletionList(is_incomplete=False, items=items)
# TODO: If your linter outputs in a known format like JSON, then parse
# accordingly. But incase you need to parse the output using RegEx here
# is a helper you can work with.
# flake8 example:
# If you use following format argument with flake8 you can use the regex below to parse it.
# TOOL_ARGS += ["--format='%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s'"]
# DIAGNOSTIC_RE =
# r"(?P<line>\d+),(?P<column>-?\d+),(?P<type>\w+),(?P<code>\w+\d+):(?P<message>[^\r\n]*)"
DIAGNOSTIC_RE = re.compile(r"")
def _parse_output_using_regex(content: str) -> list[lsp.Diagnostic]:
lines: list[str] = content.splitlines()
diagnostics: list[lsp.Diagnostic] = []
# TODO: Determine if your linter reports line numbers starting at 1 (True) or 0 (False).
line_at_1 = True
# TODO: Determine if your linter reports column numbers starting at 1 (True) or 0 (False).
column_at_1 = True
line_offset = 1 if line_at_1 else 0
col_offset = 1 if column_at_1 else 0
for line in lines:
if line.startswith("'") and line.endswith("'"):
line = line[1:-1]
match = DIAGNOSTIC_RE.match(line)
if match:
data = match.groupdict()
position = lsp.Position(
line=max([int(data["line"]) - line_offset, 0]),
character=int(data["column"]) - col_offset,
)
diagnostic = lsp.Diagnostic(
range=lsp.Range(
start=position,
end=position,
),
message=data.get("message"),
severity=_get_severity(data["code"], data["type"]),
code=data["code"],
source=TOOL_MODULE,
)
diagnostics.append(diagnostic)
return diagnostics
# TODO: if you want to handle setting specific severity for your linter
# in a user configurable way, then look at look at how it is implemented
# for `pylint` extension from our team.
# Pylint: https://github.com/microsoft/vscode-pylint
# Follow the flow of severity from the settings in package.json to the server.
def _get_severity(*_codes: list[str]) -> lsp.DiagnosticSeverity:
# TODO: All reported issues from linter are treated as warning.
# change it as appropriate for your linter.
return lsp.DiagnosticSeverity.Warning
# **********************************************************
# Linting features end here
# **********************************************************
# TODO: If your tool is a formatter then update this section.
# Delete "Formatting features" section if your tool is NOT a
# formatter.
# **********************************************************
# Formatting features start here
# **********************************************************
@@ -177,74 +118,31 @@ def _get_severity(*_codes: list[str]) -> lsp.DiagnosticSeverity:
# Black: https://github.com/microsoft/vscode-black-formatter/blob/main/bundled/tool
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_FORMATTING)
def formatting(params: lsp.DocumentFormattingParams) -> list[lsp.TextEdit] | None:
"""LSP handler for textDocument/formatting request."""
text = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
tree, _ = parse_tcl(text)
if not tree:
return []
new_text = format_tree(tree)
return [
{
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": len(text.splitlines()), "character": 0},
},
"newText": new_text,
}
]
def _formatting_helper(document: workspace.Document) -> list[lsp.TextEdit] | None:
# TODO: For formatting on save support the formatter you use must support
# formatting via stdin.
# Read, and update_run_tool_on_document and _run_tool functions as needed
# for your formatter.
result = _run_tool_on_document(document, use_stdin=True)
if result.stdout:
new_source = _match_line_endings(document, result.stdout)
return [
lsp.TextEdit(
range=lsp.Range(
start=lsp.Position(line=0, character=0),
end=lsp.Position(line=len(document.lines), character=0),
),
new_text=new_source,
)
]
return None
def _get_line_endings(lines: list[str]) -> str:
"""Returns line endings used in the text."""
try:
if lines[0][-2:] == "\r\n":
return "\r\n"
return "\n"
except Exception: # pylint: disable=broad-except
return None
def _match_line_endings(document: workspace.Document, text: str) -> str:
"""Ensures that the edited text line endings matches the document line endings."""
expected = _get_line_endings(document.source.splitlines(keepends=True))
actual = _get_line_endings(text.splitlines(keepends=True))
if actual == expected or actual is None or expected is None:
return text
return text.replace(actual, expected)
# **********************************************************
# 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)
text = doc.source
formatted = format_tcl(text)
last_line = len(text.splitlines())
full_range = lsp.Range(
start=lsp.Position(line=0, character=0),
end=lsp.Position(line=last_line, character=0),
)
edit = lsp.TextEdit(range=full_range, new_text=formatted)
return [edit]
# **********************************************************
# Required Language Server Initialization and Exit handlers.
# **********************************************************
@LSP_SERVER.feature(lsp.INITIALIZE)
def initialize(params: lsp.InitializeParams) -> None:
def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
"""LSP handler for initialize request."""
log_to_output(f"CWD Server: {os.getcwd()}")
@@ -261,6 +159,11 @@ def initialize(params: lsp.InitializeParams) -> None:
log_to_output(
f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n"
)
return lsp.InitializeResult(
capabilities=lsp.ServerCapabilities(
document_formatting_provider=True,
)
)
@LSP_SERVER.feature(lsp.EXIT)
@@ -350,213 +253,6 @@ def _get_settings_by_document(document: workspace.Document | None):
return WORKSPACE_SETTINGS[str(key)]
# *****************************************************
# Internal execution APIs.
# *****************************************************
def _run_tool_on_document(
document: workspace.Document,
use_stdin: bool = False,
extra_args: Optional[Sequence[str]] = None,
) -> utils.RunResult | None:
"""Runs tool on the given document.
if use_stdin is true then contents of the document is passed to the
tool via stdin.
"""
if extra_args is None:
extra_args = []
if str(document.uri).startswith("vscode-notebook-cell"):
# TODO: Decide on if you want to skip notebook cells.
# Skip notebook cells
return None
if utils.is_stdlib_file(document.path):
# TODO: Decide on if you want to skip standard library files.
# Skip standard library python files.
return None
# deep copy here to prevent accidentally updating global settings.
settings = copy.deepcopy(_get_settings_by_document(document))
code_workspace = settings["workspaceFS"]
cwd = settings["cwd"]
use_path = False
use_rpc = False
if settings["path"]:
# 'path' setting takes priority over everything.
use_path = True
argv = settings["path"]
elif settings["interpreter"] and not utils.is_current_interpreter(
settings["interpreter"][0]
):
# If there is a different interpreter set use JSON-RPC to the subprocess
# running under that interpreter.
argv = [TOOL_MODULE]
use_rpc = True
else:
# if the interpreter is same as the interpreter running this
# process then run as module.
argv = [TOOL_MODULE]
argv += TOOL_ARGS + settings["args"] + extra_args
if use_stdin:
# TODO: update these to pass the appropriate arguments to provide document contents
# to tool via stdin.
# For example, for pylint args for stdin looks like this:
# pylint --from-stdin <path>
# Here `--from-stdin` path is used by pylint to make decisions on the file contents
# that are being processed. Like, applying exclusion rules.
# It should look like this when you pass it:
# argv += ["--from-stdin", document.path]
# Read up on how your tool handles contents via stdin. If stdin is not supported use
# set use_stdin to False, or provide path, what ever is appropriate for your tool.
argv += []
else:
argv += [document.path]
if use_path:
# This mode is used when running executables.
log_to_output(" ".join(argv))
log_to_output(f"CWD Server: {cwd}")
result = utils.run_path(
argv=argv,
use_stdin=use_stdin,
cwd=cwd,
source=document.source.replace("\r\n", "\n"),
)
if result.stderr:
log_to_output(result.stderr)
elif use_rpc:
# This mode is used if the interpreter running this server is different from
# the interpreter used for running this server.
log_to_output(" ".join(settings["interpreter"] + ["-m"] + argv))
log_to_output(f"CWD Linter: {cwd}")
result = jsonrpc.run_over_json_rpc(
workspace=code_workspace,
interpreter=settings["interpreter"],
module=TOOL_MODULE,
argv=argv,
use_stdin=use_stdin,
cwd=cwd,
source=document.source,
)
if result.exception:
log_error(result.exception)
result = utils.RunResult(result.stdout, result.stderr)
elif result.stderr:
log_to_output(result.stderr)
else:
# In this mode the tool is run as a module in the same process as the language server.
log_to_output(" ".join([sys.executable, "-m"] + argv))
log_to_output(f"CWD Linter: {cwd}")
# This is needed to preserve sys.path, in cases where the tool modifies
# sys.path and that might not work for this scenario next time around.
with utils.substitute_attr(sys, "path", sys.path[:]):
try:
# TODO: `utils.run_module` is equivalent to running `python -m <pytool-module>`.
# If your tool supports a programmatic API then replace the function below
# with code for your tool. You can also use `utils.run_api` helper, which
# handles changing working directories, managing io streams, etc.
# Also update `_run_tool` function and `utils.run_module` in `lsp_runner.py`.
result = utils.run_module(
module=TOOL_MODULE,
argv=argv,
use_stdin=use_stdin,
cwd=cwd,
source=document.source,
)
except Exception:
log_error(traceback.format_exc(chain=True))
raise
if result.stderr:
log_to_output(result.stderr)
log_to_output(f"{document.uri} :\r\n{result.stdout}")
return result
def _run_tool(extra_args: Sequence[str]) -> utils.RunResult:
"""Runs tool."""
# deep copy here to prevent accidentally updating global settings.
settings = copy.deepcopy(_get_settings_by_document(None))
code_workspace = settings["workspaceFS"]
cwd = settings["workspaceFS"]
use_path = False
use_rpc = False
if len(settings["path"]) > 0:
# 'path' setting takes priority over everything.
use_path = True
argv = settings["path"]
elif len(settings["interpreter"]) > 0 and not utils.is_current_interpreter(
settings["interpreter"][0]
):
# If there is a different interpreter set use JSON-RPC to the subprocess
# running under that interpreter.
argv = [TOOL_MODULE]
use_rpc = True
else:
# if the interpreter is same as the interpreter running this
# process then run as module.
argv = [TOOL_MODULE]
argv += extra_args
if use_path:
# This mode is used when running executables.
log_to_output(" ".join(argv))
log_to_output(f"CWD Server: {cwd}")
result = utils.run_path(argv=argv, use_stdin=True, cwd=cwd)
if result.stderr:
log_to_output(result.stderr)
elif use_rpc:
# This mode is used if the interpreter running this server is different from
# the interpreter used for running this server.
log_to_output(" ".join(settings["interpreter"] + ["-m"] + argv))
log_to_output(f"CWD Linter: {cwd}")
result = jsonrpc.run_over_json_rpc(
workspace=code_workspace,
interpreter=settings["interpreter"],
module=TOOL_MODULE,
argv=argv,
use_stdin=True,
cwd=cwd,
)
if result.exception:
log_error(result.exception)
result = utils.RunResult(result.stdout, result.stderr)
elif result.stderr:
log_to_output(result.stderr)
else:
# In this mode the tool is run as a module in the same process as the language server.
log_to_output(" ".join([sys.executable, "-m"] + argv))
log_to_output(f"CWD Linter: {cwd}")
# This is needed to preserve sys.path, in cases where the tool modifies
# sys.path and that might not work for this scenario next time around.
with utils.substitute_attr(sys, "path", sys.path[:]):
try:
# TODO: `utils.run_module` is equivalent to running `python -m <pytool-module>`.
# If your tool supports a programmatic API then replace the function below
# with code for your tool. You can also use `utils.run_api` helper, which
# handles changing working directories, managing io streams, etc.
# Also update `_run_tool_on_document` function and `utils.run_module` in `lsp_runner.py`.
result = utils.run_module(
module=TOOL_MODULE, argv=argv, use_stdin=True, cwd=cwd
)
except Exception:
log_error(traceback.format_exc(chain=True))
raise
if result.stderr:
log_to_output(result.stderr)
log_to_output(f"\r\n{result.stdout}\r\n")
return result
# *****************************************************
# Logging and notification.
# *****************************************************