forked from Instagram/Fixit
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
resolves Instagram#387 Instagram#122 Support for: - [x] `textDocument/didOpen`, `textDocument/didChange` -> `textDocument/publishDiagnostics` - [x] `textDocument/formatting` Also adds --log-file CLI arg so that the language server can be observable. In the interest of keeping the PR small, the following are not included in this PR: - [ ] `textDocument/codeAction`, `workspace/executeCommand` - [ ] `workspace/didChangeWatchedFiles` to invalidate the config cache - [ ] Vendor [Generic LSP Client](https://github.com/llllvvuu/vscode-glspc) to add Fixit branding (code works out of the box, only `README.md`, `LICENSE`, and `package.json` would need to be changed. I published MIT license so it's free to use.) If this PR gets merged I will create follow-up issues for these items. test: Added new smoke test for the new `fixit lsp` subcommand.
- Loading branch information
Showing
10 changed files
with
406 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
# Copyright (c) Meta Platforms, Inc. and affiliates. | ||
# | ||
# This source code is licensed under the MIT license found in the | ||
# LICENSE file in the root directory of this source tree. | ||
|
||
from pathlib import Path | ||
from typing import Callable, Dict | ||
|
||
import pygls.uris as Uri | ||
|
||
from lsprotocol.types import ( | ||
Diagnostic, | ||
DiagnosticSeverity, | ||
DidChangeTextDocumentParams, | ||
DidOpenTextDocumentParams, | ||
DocumentFormattingParams, | ||
Position, | ||
Range, | ||
TEXT_DOCUMENT_DID_CHANGE, | ||
TEXT_DOCUMENT_DID_OPEN, | ||
TEXT_DOCUMENT_FORMATTING, | ||
TextEdit, | ||
) | ||
from pygls.server import LanguageServer | ||
|
||
from fixit import __version__ | ||
from fixit.util import capture, debounce | ||
|
||
from .api import fixit_bytes | ||
from .config import generate_config | ||
from .ftypes import Config, LspOptions, Options | ||
|
||
|
||
class LSP: | ||
""" | ||
Server for the Language Server Protocol. | ||
Provides diagnostics as you type, and exposes a formatter. | ||
https://microsoft.github.io/language-server-protocol/ | ||
""" | ||
|
||
def __init__(self, fixit_options: Options, lsp_options: LspOptions): | ||
self.fixit_options = fixit_options | ||
self.lsp_options = lsp_options | ||
self.lsp = LanguageServer("fixit-lsp", __version__) | ||
|
||
self._config_cache: Dict[Path, Config] = {} | ||
self._debounced_validators: Dict[str, Callable[[int], None]] = {} | ||
|
||
self.set_handlers() | ||
|
||
def load_config(self, path: Path, bust_cache=False) -> Config: | ||
""" | ||
Cached fetch of fixit.toml(s) for fixit_bytes. | ||
""" | ||
if bust_cache or (path not in self._config_cache): | ||
self._config_cache[path] = generate_config(path, options=self.fixit_options) | ||
return self._config_cache[path] | ||
|
||
def diagnostic_generator(self, uri: str, autofix=False): | ||
""" | ||
LSP wrapper (provides document state from `pygls`) for `fixit_bytes`. | ||
""" | ||
path = Uri.to_fs_path(uri) | ||
if not path: | ||
return None | ||
path = Path(path) | ||
|
||
return fixit_bytes( | ||
path, | ||
self.lsp.workspace.get_document(uri).source.encode(), | ||
autofix=autofix, | ||
config=self.load_config(path), | ||
) | ||
|
||
def validate(self, uri: str, version: int) -> None: | ||
""" | ||
Side-effect: publishes Fixit diagnostics to the LSP client. | ||
""" | ||
generator = self.diagnostic_generator(uri) | ||
if not generator: | ||
return | ||
diagnostics = [] | ||
for result in generator: | ||
violation = result.violation | ||
if not violation: | ||
continue | ||
diagnostic = Diagnostic( | ||
Range( | ||
Position( # LSP is 0-indexed; fixit line numbers are 1-indexed | ||
violation.range.start.line - 1, violation.range.start.column | ||
), | ||
Position(violation.range.end.line - 1, violation.range.end.column), | ||
), | ||
violation.message, | ||
severity=DiagnosticSeverity.Warning, | ||
code=violation.rule_name, | ||
source="fixit", | ||
) | ||
diagnostics.append(diagnostic) | ||
self.lsp.publish_diagnostics(uri, diagnostics, version=version) | ||
|
||
def debounced_validator(self, uri: str) -> Callable[[int], None]: | ||
""" | ||
Per-URI debounced validation function. See: LSP.validate | ||
""" | ||
if uri not in self._debounced_validators: | ||
self._debounced_validators[uri] = debounce( | ||
self.lsp_options.debounce_interval | ||
)(lambda version: self.validate(uri, version)) | ||
return self._debounced_validators[uri] | ||
|
||
def set_handlers(self) -> None: | ||
""" | ||
Side-effect: mutates self.lsp to make it respond to | ||
`textDocument/didOpen`, `textDocument/didChange`, `textDocument/formatting` | ||
""" | ||
|
||
@self.lsp.feature(TEXT_DOCUMENT_DID_OPEN) | ||
def _(params: DidOpenTextDocumentParams): | ||
self.debounced_validator(params.text_document.uri)( | ||
params.text_document.version | ||
) | ||
|
||
@self.lsp.feature(TEXT_DOCUMENT_DID_CHANGE) | ||
def _(params: DidChangeTextDocumentParams): | ||
self.debounced_validator(params.text_document.uri)( | ||
params.text_document.version | ||
) | ||
|
||
@self.lsp.feature(TEXT_DOCUMENT_FORMATTING) | ||
def _(params: DocumentFormattingParams): | ||
generator = self.diagnostic_generator( | ||
params.text_document.uri, autofix=True | ||
) | ||
if generator is None: | ||
return None | ||
|
||
captured = capture(generator) | ||
for _ in captured: | ||
pass | ||
formatted_content = captured.result | ||
if not formatted_content: | ||
return None | ||
|
||
doc = self.lsp.workspace.get_document(params.text_document.uri) | ||
entire_range = Range( | ||
start=Position(line=0, character=0), | ||
end=Position(line=len(doc.lines) - 1, character=len(doc.lines[-1])), | ||
) | ||
|
||
return [TextEdit(new_text=formatted_content.decode(), range=entire_range)] | ||
|
||
def start(self) -> None: | ||
""" | ||
Side-effect: occupies the configured I/O channels. | ||
""" | ||
if self.lsp_options.stdio: | ||
self.lsp.start_io() | ||
if self.lsp_options.tcp: | ||
self.lsp.start_tcp("localhost", self.lsp_options.tcp) | ||
if self.lsp_options.ws: | ||
self.lsp.start_ws("localhost", self.lsp_options.ws) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.