mirror of
https://github.com/tiennm99/serena.git
synced 2026-09-05 08:17:50 +00:00
Initial implementation of fully synchronous language server (SolidLanguageServer)
with working PyRight implementation
This commit is contained in:
+16
-6
@@ -49,6 +49,7 @@ from serena.util.general import load_yaml, save_yaml
|
||||
from serena.util.inspection import determine_programming_language_composition, iter_subclasses
|
||||
from serena.util.shell import execute_shell_command
|
||||
from serena.util.thread import ExecutionResult, execute_with_timeout
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.gui_log_viewer import GuiLogViewerHandler
|
||||
@@ -666,12 +667,21 @@ def create_ls_for_project(
|
||||
)
|
||||
ls_logger = MultilspyLogger(log_level=log_level)
|
||||
log.info(f"Creating language server instance for {project_instance.project_root}.")
|
||||
return SyncLanguageServer.create(
|
||||
multilspy_config,
|
||||
ls_logger,
|
||||
project_instance.project_root,
|
||||
timeout=ls_timeout,
|
||||
)
|
||||
use_solid_ls = True
|
||||
if use_solid_ls:
|
||||
return SolidLanguageServer.create(
|
||||
multilspy_config,
|
||||
ls_logger,
|
||||
project_instance.project_root,
|
||||
timeout=ls_timeout,
|
||||
)
|
||||
else:
|
||||
return SyncLanguageServer.create(
|
||||
multilspy_config,
|
||||
ls_logger,
|
||||
project_instance.project_root,
|
||||
timeout=ls_timeout,
|
||||
)
|
||||
|
||||
|
||||
@click.command()
|
||||
|
||||
+130
-133
@@ -24,7 +24,6 @@ from multilspy.lsp_protocol_handler.lsp_constants import LSPConstants
|
||||
from multilspy.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind, InitializeParams
|
||||
from multilspy.lsp_protocol_handler.server import (
|
||||
Error,
|
||||
LanguageServerHandler,
|
||||
ProcessLaunchInfo,
|
||||
StringDict,
|
||||
)
|
||||
@@ -33,9 +32,10 @@ from multilspy.multilspy_exceptions import MultilspyException
|
||||
from multilspy.multilspy_logger import MultilspyLogger
|
||||
from multilspy.multilspy_utils import FileUtils, PathUtils, TextUtils
|
||||
from serena.text_utils import MatchedConsecutiveLines, search_files
|
||||
from solidlsp.ls_handler import SolidLanguageServerHandler
|
||||
|
||||
|
||||
class LanguageServer:
|
||||
class SolidLanguageServer:
|
||||
"""
|
||||
The LanguageServer class provides a language agnostic interface to the Language Server Protocol.
|
||||
It is used to communicate with Language Servers of different programming languages.
|
||||
@@ -50,7 +50,7 @@ class LanguageServer:
|
||||
return dirname.startswith('.')
|
||||
|
||||
@classmethod
|
||||
def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str) -> "LanguageServer":
|
||||
def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, timeout) -> "SolidLanguageServer":
|
||||
"""
|
||||
Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language.
|
||||
|
||||
@@ -67,7 +67,7 @@ class LanguageServer:
|
||||
PyrightServer,
|
||||
)
|
||||
|
||||
return PyrightServer(config, logger, repository_root_path)
|
||||
return SolidPyrightServer(config, logger, repository_root_path)
|
||||
# It used to be jedi, but pyright is a bit faster, and also more actively maintained
|
||||
# Keeping the previous code for reference
|
||||
# from multilspy.language_servers.jedi_language_server.jedi_server import (
|
||||
@@ -147,11 +147,6 @@ class LanguageServer:
|
||||
The command must pass appropriate flags to the binary, so that it runs in the stdio mode,
|
||||
as opposed to HTTP, TCP modes supported by some language servers.
|
||||
"""
|
||||
if type(self) == LanguageServer:
|
||||
raise MultilspyException(
|
||||
"LanguageServer is an abstract class and cannot be instantiated directly. Use LanguageServer.create method instead."
|
||||
)
|
||||
|
||||
self.logger = logger
|
||||
self.repository_root_path: str = repository_root_path
|
||||
self.logger.log(f"Creating language server instance for {repository_root_path=} with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG)
|
||||
@@ -168,7 +163,7 @@ class LanguageServer:
|
||||
self.load_cache()
|
||||
|
||||
self.server_started = False
|
||||
self.completions_available = asyncio.Event()
|
||||
self.completions_available = threading.Event()
|
||||
if config.trace_lsp_communication:
|
||||
def logging_fn(source: str, target: str, msg: StringDict | str):
|
||||
self.logger.log(f"LSP: {source} -> {target}: {str(msg)[:90]}...", self.logger.logger.level)
|
||||
@@ -179,7 +174,7 @@ class LanguageServer:
|
||||
# cmd is obtained from the child classes, which provide the language specific command to start the language server
|
||||
# LanguageServerHandler provides the functionality to start the language server and communicate with it
|
||||
self.logger.log(f"Creating language server instance with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG)
|
||||
self.server = LanguageServerHandler(
|
||||
self.server = SolidLanguageServerHandler(
|
||||
process_launch_info,
|
||||
logger=logging_fn,
|
||||
start_independent_lsp_process=config.start_independent_lsp_process,
|
||||
@@ -201,6 +196,8 @@ class LanguageServer:
|
||||
processed_patterns
|
||||
)
|
||||
|
||||
self._server_context = None
|
||||
|
||||
def get_ignore_spec(self) -> pathspec.PathSpec:
|
||||
"""Returns the pathspec matcher for the paths that were configured to be ignored through
|
||||
the multilspy config.
|
||||
@@ -262,7 +259,7 @@ class LanguageServer:
|
||||
|
||||
return False
|
||||
|
||||
async def _shutdown(self, timeout: float = 5.0):
|
||||
def _shutdown(self, timeout: float = 5.0):
|
||||
"""
|
||||
A robust shutdown process designed to terminate cleanly on all platforms, including Windows,
|
||||
by explicitly closing all I/O pipes.
|
||||
@@ -276,27 +273,19 @@ class LanguageServer:
|
||||
reader_tasks = list(self.server.tasks.values())
|
||||
|
||||
# --- Main Shutdown Logic ---
|
||||
# Stage 1: Graceful Termination Request
|
||||
# Send LSP shutdown and close stdin to signal no more input.
|
||||
try:
|
||||
# Stage 1: Graceful Termination Request
|
||||
# Send LSP shutdown and close stdin to signal no more input.
|
||||
try:
|
||||
await asyncio.wait_for(self.server.shutdown(), timeout=2.0)
|
||||
if process.stdin and not process.stdin.is_closing():
|
||||
process.stdin.close()
|
||||
except Exception:
|
||||
pass # Ignore errors here, we are proceeding to terminate anyway.
|
||||
self.server.shutdown()
|
||||
if process.stdin and not process.stdin.is_closing():
|
||||
process.stdin.close()
|
||||
except Exception:
|
||||
pass # Ignore errors here, we are proceeding to terminate anyway.
|
||||
|
||||
# Stage 2: Terminate and Concurrently Drain stdout/stderr
|
||||
process.terminate()
|
||||
|
||||
# Wait for the process to exit AND for the output pipes to be drained.
|
||||
# The reader tasks will exit when they hit EOF.
|
||||
await asyncio.wait_for(
|
||||
asyncio.gather(process.wait(), *reader_tasks),
|
||||
timeout=timeout - 2.0
|
||||
)
|
||||
self.logger.log("Process terminated and output pipes drained.", logging.INFO)
|
||||
# Stage 2: Terminate and Concurrently Drain stdout/stderr
|
||||
process.terminate()
|
||||
|
||||
"""
|
||||
except asyncio.TimeoutError:
|
||||
# Stage 3: Forceful Kill
|
||||
self.logger.log("Graceful termination failed. Forcefully killing process...", logging.WARNING)
|
||||
@@ -307,7 +296,6 @@ class LanguageServer:
|
||||
await process.wait()
|
||||
except Exception as e:
|
||||
self.logger.log(f"Error during forceful kill: {e}", logging.ERROR)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"An unexpected error occurred during shutdown logic: {e}", logging.ERROR)
|
||||
|
||||
@@ -334,18 +322,16 @@ class LanguageServer:
|
||||
# 3. Null out the process object in the handler.
|
||||
self.server.process = None
|
||||
self.logger.log("Shutdown sequence fully finished.", logging.DEBUG)
|
||||
"""
|
||||
|
||||
@asynccontextmanager
|
||||
async def start_server(self) -> AsyncIterator["LanguageServer"]:
|
||||
"""
|
||||
Starts the Language Server and yields the LanguageServer instance.
|
||||
"""
|
||||
@contextmanager
|
||||
def start_server(self) -> Iterator["SolidLanguageServer"]:
|
||||
self.start()
|
||||
yield self
|
||||
self._shutdown()
|
||||
|
||||
def _start_server(self) -> None:
|
||||
self.server_started = True
|
||||
try:
|
||||
yield self
|
||||
finally:
|
||||
self.server_started = False
|
||||
await self._shutdown()
|
||||
|
||||
@contextmanager
|
||||
def open_file(self, relative_file_path: str) -> Iterator[LSPFileBuffer]:
|
||||
@@ -404,7 +390,7 @@ class LanguageServer:
|
||||
self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str
|
||||
) -> multilspy_types.Position:
|
||||
"""
|
||||
Insert text at the given line and column in the given file and return
|
||||
Insert text at the given line and column in the given file and return
|
||||
the updated cursor position after inserting the text.
|
||||
|
||||
:param relative_file_path: The relative path of the file to open.
|
||||
@@ -486,10 +472,10 @@ class LanguageServer:
|
||||
)
|
||||
return deleted_text
|
||||
|
||||
async def _send_definition_request(self, definition_params: DefinitionParams) -> Union[Definition, List[LocationLink], None]:
|
||||
return await self.server.send.definition(definition_params)
|
||||
def _send_definition_request(self, definition_params: DefinitionParams) -> Union[Definition, List[LocationLink], None]:
|
||||
return self.server.send.definition(definition_params)
|
||||
|
||||
async def request_definition(
|
||||
def request_definition(
|
||||
self, relative_file_path: str, line: int, column: int
|
||||
) -> List[multilspy_types.Location]:
|
||||
"""
|
||||
@@ -523,7 +509,7 @@ class LanguageServer:
|
||||
LSPConstants.CHARACTER: column,
|
||||
},
|
||||
})
|
||||
response = await self._send_definition_request(definition_params)
|
||||
response = self._send_definition_request(definition_params)
|
||||
|
||||
ret: List[multilspy_types.Location] = []
|
||||
if isinstance(response, list):
|
||||
@@ -573,8 +559,8 @@ class LanguageServer:
|
||||
return ret
|
||||
|
||||
# Some LS cause problems with this, so the call is isolated from the rest to allow overriding in subclasses
|
||||
async def _send_references_request(self, relative_file_path: str, line: int, column: int) -> List[lsp_types.Location] | None:
|
||||
return await self.server.send.references(
|
||||
def _send_references_request(self, relative_file_path: str, line: int, column: int) -> List[lsp_types.Location] | None:
|
||||
return self.server.send.references(
|
||||
{
|
||||
"textDocument": {"uri": PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))},
|
||||
"position": {"line": line, "character": column},
|
||||
@@ -582,7 +568,7 @@ class LanguageServer:
|
||||
}
|
||||
)
|
||||
|
||||
async def request_references(
|
||||
def request_references(
|
||||
self, relative_file_path: str, line: int, column: int
|
||||
) -> List[multilspy_types.Location]:
|
||||
"""
|
||||
@@ -607,7 +593,7 @@ class LanguageServer:
|
||||
|
||||
with self.open_file(relative_file_path):
|
||||
try:
|
||||
response = await self._send_references_request(relative_file_path, line=line, column=column)
|
||||
response = self._send_references_request(relative_file_path, line=line, column=column)
|
||||
except Exception as e:
|
||||
# Catch LSP internal error (-32603) and raise a more informative exception
|
||||
if isinstance(e, Error) and getattr(e, 'code', None) == -32603:
|
||||
@@ -640,7 +626,7 @@ class LanguageServer:
|
||||
|
||||
return ret
|
||||
|
||||
async def request_references_with_content(
|
||||
def request_references_with_content(
|
||||
self, relative_file_path: str, line: int, column: int, context_lines_before: int = 0, context_lines_after: int = 0
|
||||
) -> List[MatchedConsecutiveLines]:
|
||||
"""
|
||||
@@ -654,7 +640,7 @@ class LanguageServer:
|
||||
|
||||
:return: A list of MatchedConsecutiveLines objects, one for each reference.
|
||||
"""
|
||||
references = await self.request_references(relative_file_path, line, column)
|
||||
references = self.request_references(relative_file_path, line, column)
|
||||
return [self.retrieve_content_around_line(ref["relativePath"], ref["range"]["start"]["line"], context_lines_before, context_lines_after) for ref in references]
|
||||
|
||||
def retrieve_full_file_content(self, relative_file_path: str) -> str:
|
||||
@@ -679,8 +665,7 @@ class LanguageServer:
|
||||
file_contents = file_data.contents
|
||||
return MatchedConsecutiveLines.from_file_contents(file_contents, line=line, context_lines_before=context_lines_before, context_lines_after=context_lines_after, source_file_path=relative_file_path)
|
||||
|
||||
|
||||
async def request_completions(
|
||||
def request_completions(
|
||||
self, relative_file_path: str, line: int, column: int, allow_incomplete: bool = False
|
||||
) -> List[multilspy_types.CompletionItem]:
|
||||
"""
|
||||
@@ -706,10 +691,10 @@ class LanguageServer:
|
||||
|
||||
num_retries = 0
|
||||
while response is None or (response["isIncomplete"] and num_retries < 30):
|
||||
await self.completions_available.wait()
|
||||
self.completions_available.wait()
|
||||
response: Union[
|
||||
List[LSPTypes.CompletionItem], LSPTypes.CompletionList, None
|
||||
] = await self.server.send.completion(completion_params)
|
||||
] = self.server.send.completion(completion_params)
|
||||
if isinstance(response, list):
|
||||
response = {"items": response, "isIncomplete": False}
|
||||
num_retries += 1
|
||||
@@ -774,7 +759,7 @@ class LanguageServer:
|
||||
for json_repr in set([json.dumps(item, sort_keys=True) for item in completions_list])
|
||||
]
|
||||
|
||||
async def request_document_symbols(self, relative_file_path: str, include_body: bool = False) -> Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]:
|
||||
def request_document_symbols(self, relative_file_path: str, include_body: bool = False) -> Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]:
|
||||
"""
|
||||
Raise a [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol) request to the Language Server
|
||||
to find symbols in the given file. Wait for the response and return the result.
|
||||
@@ -805,7 +790,7 @@ class LanguageServer:
|
||||
self.logger.log(f"No cache hit for symbols with {include_body=} in {relative_file_path}", logging.DEBUG)
|
||||
|
||||
self.logger.log(f"Requesting document symbols for {relative_file_path} from the Language Server", logging.DEBUG)
|
||||
response = await self.server.send.document_symbol(
|
||||
response = self.server.send.document_symbol(
|
||||
{
|
||||
"textDocument": {
|
||||
"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()
|
||||
@@ -892,9 +877,9 @@ class LanguageServer:
|
||||
self._cache_has_changed = True
|
||||
return result
|
||||
|
||||
async def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]:
|
||||
def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]:
|
||||
"""
|
||||
Will go through all files in the project or within a relative path and build a tree of symbols.
|
||||
Will go through all files in the project or within a relative path and build a tree of symbols.
|
||||
Note: this may be slow the first time it is called, especially if `within_relative_path` is not used to restrict the search.
|
||||
|
||||
For each file, a symbol of kind File (2) will be created. For directories, a symbol of kind Package (4) will be created.
|
||||
@@ -921,11 +906,11 @@ class LanguageServer:
|
||||
self.logger.log(f"You passed a file explicitly, but it is ignored. This is probably an error. File: {within_relative_path}", logging.ERROR)
|
||||
return []
|
||||
else:
|
||||
_, root_nodes = await self.request_document_symbols(within_relative_path, include_body=include_body)
|
||||
_, root_nodes = self.request_document_symbols(within_relative_path, include_body=include_body)
|
||||
return root_nodes
|
||||
|
||||
# Helper function to recursively process directories
|
||||
async def process_directory(rel_dir_path: str) -> List[multilspy_types.UnifiedSymbolInformation]:
|
||||
def process_directory(rel_dir_path: str) -> List[multilspy_types.UnifiedSymbolInformation]:
|
||||
abs_dir_path = self.repository_root_path if rel_dir_path == "." else os.path.join(self.repository_root_path, rel_dir_path)
|
||||
abs_dir_path = os.path.realpath(abs_dir_path)
|
||||
|
||||
@@ -961,13 +946,13 @@ class LanguageServer:
|
||||
continue
|
||||
|
||||
if os.path.isdir(contained_dir_or_file_abs_path):
|
||||
child_symbols = await process_directory(contained_dir_or_file_rel_path)
|
||||
child_symbols = process_directory(contained_dir_or_file_rel_path)
|
||||
package_symbol["children"].extend(child_symbols)
|
||||
for child in child_symbols:
|
||||
child["parent"] = package_symbol
|
||||
|
||||
elif os.path.isfile(contained_dir_or_file_abs_path):
|
||||
_, file_root_nodes = await self.request_document_symbols(contained_dir_or_file_rel_path, include_body=include_body)
|
||||
_, file_root_nodes = self.request_document_symbols(contained_dir_or_file_rel_path, include_body=include_body)
|
||||
|
||||
# Create file symbol, link with children
|
||||
file_rel_path = str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path))
|
||||
@@ -1013,7 +998,7 @@ class LanguageServer:
|
||||
|
||||
# Start from the root or the specified directory
|
||||
start_rel_path = within_relative_path or "."
|
||||
return await process_directory(start_rel_path)
|
||||
return process_directory(start_rel_path)
|
||||
|
||||
@staticmethod
|
||||
def _get_range_from_file_content(file_content: str) -> multilspy_types.Range:
|
||||
@@ -1028,14 +1013,14 @@ class LanguageServer:
|
||||
end=multilspy_types.Position(line=end_line, character=end_column)
|
||||
)
|
||||
|
||||
async def request_dir_overview(self, relative_dir_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]:
|
||||
def request_dir_overview(self, relative_dir_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]:
|
||||
"""
|
||||
An overview of the given directory.
|
||||
|
||||
Maps relative paths of all contained files to info about top-level symbols in the file
|
||||
(name, kind, line, column).
|
||||
"""
|
||||
symbol_tree = await self.request_full_symbol_tree(relative_dir_path)
|
||||
symbol_tree = self.request_full_symbol_tree(relative_dir_path)
|
||||
# Initialize result dictionary
|
||||
result: dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]] = defaultdict(list)
|
||||
|
||||
@@ -1062,12 +1047,12 @@ class LanguageServer:
|
||||
process_symbol(root)
|
||||
return result
|
||||
|
||||
async def request_document_overview(self, relative_file_path: str) -> list[tuple[str, multilspy_types.SymbolKind, int, int]]:
|
||||
def request_document_overview(self, relative_file_path: str) -> list[tuple[str, multilspy_types.SymbolKind, int, int]]:
|
||||
"""
|
||||
An overview of the given file.
|
||||
Returns the list of tuples (name, kind, line, column) of all top-level symbols in the file.
|
||||
"""
|
||||
_, document_roots = await self.request_document_symbols(relative_file_path)
|
||||
_, document_roots = self.request_document_symbols(relative_file_path)
|
||||
result = []
|
||||
for root in document_roots:
|
||||
try:
|
||||
@@ -1083,7 +1068,7 @@ class LanguageServer:
|
||||
) from e
|
||||
return result
|
||||
|
||||
async def request_overview(self, within_relative_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]:
|
||||
def request_overview(self, within_relative_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]:
|
||||
"""
|
||||
An overview of all symbols in the given file or directory.
|
||||
|
||||
@@ -1095,12 +1080,12 @@ class LanguageServer:
|
||||
raise FileNotFoundError(f"File or directory not found: {abs_path}")
|
||||
|
||||
if abs_path.is_file():
|
||||
symbols_overview = await self.request_document_overview(within_relative_path)
|
||||
symbols_overview = self.request_document_overview(within_relative_path)
|
||||
return {within_relative_path: symbols_overview}
|
||||
else:
|
||||
return await self.request_dir_overview(within_relative_path)
|
||||
return self.request_dir_overview(within_relative_path)
|
||||
|
||||
async def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]:
|
||||
def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]:
|
||||
"""
|
||||
Raise a [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) request to the Language Server
|
||||
to find the hover information at the given line and column in the given file. Wait for the response and return the result.
|
||||
@@ -1112,7 +1097,7 @@ class LanguageServer:
|
||||
:return None
|
||||
"""
|
||||
with self.open_file(relative_file_path):
|
||||
response = await self.server.send.hover(
|
||||
response = self.server.send.hover(
|
||||
{
|
||||
"textDocument": {
|
||||
"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()
|
||||
@@ -1154,7 +1139,7 @@ class LanguageServer:
|
||||
symbol_body = symbol_body[symbol_start_column:]
|
||||
return symbol_body
|
||||
|
||||
async def request_parsed_files(self) -> list[str]:
|
||||
def request_parsed_files(self) -> list[str]:
|
||||
"""Retrieves relative paths of all files analyzed by the Language Server."""
|
||||
if not self.server_started:
|
||||
self.logger.log(
|
||||
@@ -1171,7 +1156,7 @@ class LanguageServer:
|
||||
rel_file_paths.append(rel_file_path)
|
||||
return rel_file_paths
|
||||
|
||||
async def search_files_for_pattern(
|
||||
def search_files_for_pattern(
|
||||
self,
|
||||
pattern: re.Pattern | str,
|
||||
context_lines_before: int = 0,
|
||||
@@ -1192,7 +1177,7 @@ class LanguageServer:
|
||||
if isinstance(pattern, str):
|
||||
pattern = re.compile(pattern)
|
||||
|
||||
relative_file_paths = await self.request_parsed_files()
|
||||
relative_file_paths = self.request_parsed_files()
|
||||
return search_files(
|
||||
relative_file_paths,
|
||||
pattern,
|
||||
@@ -1203,7 +1188,7 @@ class LanguageServer:
|
||||
paths_exclude_glob=paths_exclude_glob
|
||||
)
|
||||
|
||||
async def request_referencing_symbols(
|
||||
def request_referencing_symbols(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
line: int,
|
||||
@@ -1239,7 +1224,7 @@ class LanguageServer:
|
||||
raise MultilspyException("Language Server not started")
|
||||
|
||||
# First, get all references to the symbol
|
||||
references = await self.request_references(relative_file_path, line, column)
|
||||
references = self.request_references(relative_file_path, line, column)
|
||||
if not references:
|
||||
return []
|
||||
|
||||
@@ -1253,7 +1238,7 @@ class LanguageServer:
|
||||
|
||||
with self.open_file(ref_path) as file_data:
|
||||
# Get the containing symbol for this reference
|
||||
containing_symbol = await self.request_containing_symbol(
|
||||
containing_symbol = self.request_containing_symbol(
|
||||
ref_path, ref_line, ref_col, include_body=include_body
|
||||
)
|
||||
if containing_symbol is None:
|
||||
@@ -1273,7 +1258,7 @@ class LanguageServer:
|
||||
ref_text = file_data.contents.split("\n")[ref_line]
|
||||
if "." in ref_text:
|
||||
containing_symbol_name = ref_text.split(".")[0]
|
||||
all_symbols, _ = await self.request_document_symbols(ref_path)
|
||||
all_symbols, _ = self.request_document_symbols(ref_path)
|
||||
for symbol in all_symbols:
|
||||
if symbol["name"] == containing_symbol_name and symbol["kind"] == multilspy_types.SymbolKind.Variable:
|
||||
containing_symbol = copy(symbol)
|
||||
@@ -1350,7 +1335,7 @@ class LanguageServer:
|
||||
|
||||
return result
|
||||
|
||||
async def request_containing_symbol(
|
||||
def request_containing_symbol(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
line: int,
|
||||
@@ -1397,7 +1382,7 @@ class LanguageServer:
|
||||
)
|
||||
return None
|
||||
|
||||
symbols, _ = await self.request_document_symbols(relative_file_path)
|
||||
symbols, _ = self.request_document_symbols(relative_file_path)
|
||||
|
||||
# make jedi and pyright api compatible
|
||||
# the former has no location, the later has no range
|
||||
@@ -1470,7 +1455,7 @@ class LanguageServer:
|
||||
else:
|
||||
return None
|
||||
|
||||
async def request_container_of_symbol(self, symbol: multilspy_types.UnifiedSymbolInformation, include_body: bool = False) -> multilspy_types.UnifiedSymbolInformation | None:
|
||||
def request_container_of_symbol(self, symbol: multilspy_types.UnifiedSymbolInformation, include_body: bool = False) -> multilspy_types.UnifiedSymbolInformation | None:
|
||||
"""
|
||||
Finds the container of the given symbol if there is one. If the parent attribute is present, the parent is returned
|
||||
without further searching.
|
||||
@@ -1482,7 +1467,7 @@ class LanguageServer:
|
||||
if "parent" in symbol:
|
||||
return symbol["parent"]
|
||||
assert "location" in symbol, f"Symbol {symbol} has no location and no parent attribute"
|
||||
return await self.request_containing_symbol(
|
||||
return self.request_containing_symbol(
|
||||
symbol["location"]["relativePath"],
|
||||
symbol["location"]["range"]["start"]["line"],
|
||||
symbol["location"]["range"]["start"]["character"],
|
||||
@@ -1490,7 +1475,7 @@ class LanguageServer:
|
||||
include_body=include_body,
|
||||
)
|
||||
|
||||
async def request_defining_symbol(
|
||||
def request_defining_symbol(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
line: int,
|
||||
@@ -1517,7 +1502,7 @@ class LanguageServer:
|
||||
raise MultilspyException("Language Server not started")
|
||||
|
||||
# Get the definition location(s)
|
||||
definitions = await self.request_definition(relative_file_path, line, column)
|
||||
definitions = self.request_definition(relative_file_path, line, column)
|
||||
if not definitions:
|
||||
return None
|
||||
|
||||
@@ -1528,7 +1513,7 @@ class LanguageServer:
|
||||
def_col = definition["range"]["start"]["character"]
|
||||
|
||||
# Find the symbol at or containing this location
|
||||
defining_symbol = await self.request_containing_symbol(
|
||||
defining_symbol = self.request_containing_symbol(
|
||||
def_path, def_line, def_col, strict=False, include_body=include_body
|
||||
)
|
||||
|
||||
@@ -1541,19 +1526,19 @@ class LanguageServer:
|
||||
"""
|
||||
return Path(self.repository_root_path) / ".serena" / "cache" / self.language_id / "document_symbols_cache_v20-05-25.pkl"
|
||||
|
||||
async def index_repository(self, progress_bar: bool = True, save_after_n_files: int = 10) -> None:
|
||||
def index_repository(self, progress_bar: bool = True, save_after_n_files: int = 10) -> None:
|
||||
"""Will go through the entire repository and "index" all files, meaning save their symbols to the cache.
|
||||
|
||||
:param progress_bar: Whether to show a progress bar while indexing the repository.
|
||||
:param save_after_n_files: How many files to process before saving a checkpoint of the cache.
|
||||
"""
|
||||
parsed_files = await self.request_parsed_files()
|
||||
parsed_files = self.request_parsed_files()
|
||||
files_processed = 0
|
||||
pbar = tqdm.tqdm(parsed_files, disable=not progress_bar)
|
||||
for relative_file_path in pbar:
|
||||
pbar.set_description(f"Indexing ({os.path.basename(relative_file_path)})")
|
||||
await self.request_document_symbols(relative_file_path, include_body=False)
|
||||
await self.request_document_symbols(relative_file_path, include_body=True)
|
||||
self.request_document_symbols(relative_file_path, include_body=False)
|
||||
self.request_document_symbols(relative_file_path, include_body=True)
|
||||
files_processed += 1
|
||||
if files_processed % save_after_n_files == 0:
|
||||
self.save_cache()
|
||||
@@ -1596,8 +1581,7 @@ class LanguageServer:
|
||||
logging.ERROR,
|
||||
)
|
||||
|
||||
|
||||
async def request_workspace_symbol(self, query: str) -> Union[List[multilspy_types.UnifiedSymbolInformation], None]:
|
||||
def request_workspace_symbol(self, query: str) -> Union[List[multilspy_types.UnifiedSymbolInformation], None]:
|
||||
"""
|
||||
Raise a [workspace/symbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) request to the Language Server
|
||||
to find symbols across the whole workspace. Wait for the response and return the result.
|
||||
@@ -1606,7 +1590,7 @@ class LanguageServer:
|
||||
|
||||
:return: A list of matching symbols
|
||||
"""
|
||||
response = await self.server.send.workspace_symbol({"query": query})
|
||||
response = self.server.send.workspace_symbol({"query": query})
|
||||
if response is None:
|
||||
return None
|
||||
|
||||
@@ -1624,8 +1608,25 @@ class LanguageServer:
|
||||
|
||||
return ret
|
||||
|
||||
def start(self) -> "SolidLanguageServer":
|
||||
"""
|
||||
Starts the language server process and connects to it. Call shutdown when ready.
|
||||
|
||||
class PyrightServer(LanguageServer):
|
||||
:return: self for method chaining
|
||||
"""
|
||||
self.logger.log(f"Starting language server with language {self.language_server.language} for {self.language_server.repository_root_path}", logging.INFO)
|
||||
self._server_context = self._start_server()
|
||||
return self
|
||||
|
||||
@property
|
||||
def language_server(self) -> Self:
|
||||
return self
|
||||
|
||||
def is_running(self) -> bool:
|
||||
return self.server.is_running()
|
||||
|
||||
|
||||
class SolidPyrightServer(SolidLanguageServer):
|
||||
"""
|
||||
Provides Python specific instantiation of the LanguageServer class using Pyright.
|
||||
Contains various configurations and settings specific to Python.
|
||||
@@ -1646,7 +1647,7 @@ class PyrightServer(LanguageServer):
|
||||
)
|
||||
|
||||
# Event to signal when initial workspace analysis is complete
|
||||
self.analysis_complete = asyncio.Event()
|
||||
self.analysis_complete = threading.Event()
|
||||
self.found_source_files = False
|
||||
|
||||
@override
|
||||
@@ -1753,8 +1754,7 @@ class PyrightServer(LanguageServer):
|
||||
|
||||
return initialize_params
|
||||
|
||||
@asynccontextmanager
|
||||
async def start_server(self) -> AsyncIterator["PyrightServer"]:
|
||||
def _start_server(self):
|
||||
"""
|
||||
Starts the Pyright Language Server and waits for initial workspace analysis to complete.
|
||||
|
||||
@@ -1772,13 +1772,13 @@ class PyrightServer(LanguageServer):
|
||||
```
|
||||
"""
|
||||
|
||||
async def execute_client_command_handler(params):
|
||||
def execute_client_command_handler(params):
|
||||
return []
|
||||
|
||||
async def do_nothing(params):
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
async def window_log_message(msg):
|
||||
def window_log_message(msg):
|
||||
"""
|
||||
Monitor Pyright's log messages to detect when initial analysis is complete.
|
||||
Pyright logs "Found X source files" when it finishes scanning the workspace.
|
||||
@@ -1794,7 +1794,7 @@ class PyrightServer(LanguageServer):
|
||||
self.analysis_complete.set()
|
||||
self.completions_available.set()
|
||||
|
||||
async def check_experimental_status(params):
|
||||
def check_experimental_status(params):
|
||||
"""
|
||||
Also listen for experimental/serverStatus as a backup signal
|
||||
"""
|
||||
@@ -1814,38 +1814,35 @@ class PyrightServer(LanguageServer):
|
||||
self.server.on_notification("language/actionableNotification", do_nothing)
|
||||
self.server.on_notification("experimental/serverStatus", check_experimental_status)
|
||||
|
||||
async with super().start_server():
|
||||
self.logger.log("Starting pyright-langserver server process", logging.INFO)
|
||||
await self.server.start()
|
||||
super()._start_server()
|
||||
self.logger.log("Starting pyright-langserver server process", logging.INFO)
|
||||
self.server.start()
|
||||
|
||||
# Send proper initialization parameters
|
||||
initialize_params = self._get_initialize_params(self.repository_root_path)
|
||||
# Send proper initialization parameters
|
||||
initialize_params = self._get_initialize_params(self.repository_root_path)
|
||||
|
||||
self.logger.log(
|
||||
"Sending initialize request from LSP client to pyright server and awaiting response",
|
||||
logging.INFO,
|
||||
)
|
||||
init_response = await self.server.send.initialize(initialize_params)
|
||||
self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO)
|
||||
self.logger.log(
|
||||
"Sending initialize request from LSP client to pyright server and awaiting response",
|
||||
logging.INFO,
|
||||
)
|
||||
init_response = self.server.send.initialize(initialize_params)
|
||||
self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO)
|
||||
|
||||
# Verify that the server supports our required features
|
||||
assert "textDocumentSync" in init_response["capabilities"]
|
||||
assert "completionProvider" in init_response["capabilities"]
|
||||
assert "definitionProvider" in init_response["capabilities"]
|
||||
# Verify that the server supports our required features
|
||||
assert "textDocumentSync" in init_response["capabilities"]
|
||||
assert "completionProvider" in init_response["capabilities"]
|
||||
assert "definitionProvider" in init_response["capabilities"]
|
||||
|
||||
# Complete the initialization handshake
|
||||
self.server.notify.initialized({})
|
||||
# Complete the initialization handshake
|
||||
self.server.notify.initialized({})
|
||||
|
||||
# Wait for Pyright to complete its initial workspace analysis
|
||||
# This prevents zombie processes by ensuring background tasks finish
|
||||
self.logger.log("Waiting for Pyright to complete initial workspace analysis...", logging.INFO)
|
||||
try:
|
||||
await asyncio.wait_for(self.analysis_complete.wait(), timeout=1.0)
|
||||
self.logger.log("Pyright initial analysis complete, server ready", logging.INFO)
|
||||
except asyncio.TimeoutError:
|
||||
self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING)
|
||||
# Fallback: assume analysis is complete after timeout
|
||||
self.analysis_complete.set()
|
||||
self.completions_available.set()
|
||||
|
||||
yield self
|
||||
# Wait for Pyright to complete its initial workspace analysis
|
||||
# This prevents zombie processes by ensuring background tasks finish
|
||||
self.logger.log("Waiting for Pyright to complete initial workspace analysis...", logging.INFO)
|
||||
if self.analysis_complete.wait(timeout=5.0):
|
||||
self.logger.log("Pyright initial analysis complete, server ready", logging.INFO)
|
||||
else:
|
||||
self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING)
|
||||
# Fallback: assume analysis is complete after timeout
|
||||
self.analysis_complete.set()
|
||||
self.completions_available.set()
|
||||
|
||||
+125
-143
@@ -1,25 +1,51 @@
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from queue import Queue
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
|
||||
import psutil
|
||||
from typing import Any, Callable, Dict, List, Optional, Union
|
||||
|
||||
from multilspy.lsp_protocol_handler.lsp_requests import LspNotification, LspRequest
|
||||
from multilspy.lsp_protocol_handler.lsp_requests import LspNotification
|
||||
from multilspy.lsp_protocol_handler.lsp_types import ErrorCodes
|
||||
from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo, StringDict, Request, content_length, ENCODING, Error, MessageType, \
|
||||
from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo, StringDict, content_length, ENCODING, Error, MessageType, \
|
||||
make_notification, PayloadLike, make_response, make_error_response, make_request, create_message
|
||||
from multilspy.multilspy_exceptions import MultilspyException
|
||||
|
||||
from solidlsp.lsp_request import SolidLspRequest
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LanguageServerHandler:
|
||||
class Request:
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
payload: Optional[PayloadLike] = None
|
||||
error: Optional[Error] = None
|
||||
|
||||
def is_error(self) -> bool:
|
||||
return self.error is not None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._result_queue = Queue()
|
||||
|
||||
def on_result(self, params: PayloadLike) -> None:
|
||||
self._result_queue.put(Request.Result(payload=params))
|
||||
|
||||
def on_error(self, err: Error) -> None:
|
||||
self._result_queue.put(Request.Result(error=err))
|
||||
|
||||
def get_result(self) -> Result:
|
||||
# TODO could add timeout
|
||||
return self._result_queue.get()
|
||||
|
||||
|
||||
class SolidLanguageServerHandler:
|
||||
"""
|
||||
This class provides the implementation of Python client for the Language Server Protocol.
|
||||
A class that launches the language server and communicates with it
|
||||
@@ -69,7 +95,7 @@ class LanguageServerHandler:
|
||||
logger: An optional function that takes two strings (source and destination) and
|
||||
a payload dictionary, and logs the communication between the client and the server.
|
||||
"""
|
||||
self.send = LspRequest(self.send_request)
|
||||
self.send = SolidLspRequest(self.send_request)
|
||||
self.notify = LspNotification(self.send_notification)
|
||||
|
||||
self.process_launch_info = process_launch_info
|
||||
@@ -92,15 +118,13 @@ class LanguageServerHandler:
|
||||
self._response_handlers_lock = threading.Lock()
|
||||
self._tasks_lock = threading.Lock()
|
||||
|
||||
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""
|
||||
Checks if the language server process is currently running.
|
||||
"""
|
||||
return self.process is not None and self.process.returncode is None
|
||||
|
||||
async def start(self) -> None:
|
||||
def start(self) -> None:
|
||||
"""
|
||||
Starts the language server process and creates a task to continuously read from its stdout to handle communications
|
||||
from the server to the client
|
||||
@@ -109,73 +133,47 @@ class LanguageServerHandler:
|
||||
child_proc_env.update(self.process_launch_info.env)
|
||||
|
||||
log.info("Starting language server process via command: %s", self.process_launch_info.cmd)
|
||||
self.process = await asyncio.create_subprocess_shell(
|
||||
self.process = subprocess.Popen(
|
||||
self.process_launch_info.cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stdin=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=child_proc_env,
|
||||
cwd=self.process_launch_info.cwd,
|
||||
start_new_session=self.start_independent_lsp_process,
|
||||
shell=True
|
||||
)
|
||||
|
||||
# Check if process terminated immediately
|
||||
if self.process.returncode is not None:
|
||||
log.error("Language server has already terminated/could not be started")
|
||||
# Process has already terminated
|
||||
stderr_data = await self.process.stderr.read()
|
||||
stderr_data = self.process.stderr.read()
|
||||
error_message = stderr_data.decode('utf-8', errors='replace')
|
||||
raise RuntimeError(f"Process terminated immediately with code {self.process.returncode}. Error: {error_message}")
|
||||
|
||||
self.loop = asyncio.get_event_loop()
|
||||
# start threads to read stdout and stderr of the process
|
||||
threading.Thread(
|
||||
target=self.run_forever,
|
||||
name="LSP stdout reader",
|
||||
daemon=True,
|
||||
).start()
|
||||
threading.Thread(
|
||||
target=self.run_forever_stderr,
|
||||
name="LSP stderr reader",
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
# Use lock to prevent race conditions on tasks and task_counter during startup
|
||||
with self._tasks_lock:
|
||||
self.tasks[self.task_counter] = self.loop.create_task(self.run_forever())
|
||||
self.task_counter += 1
|
||||
self.tasks[self.task_counter] = self.loop.create_task(self.run_forever_stderr())
|
||||
self.task_counter += 1
|
||||
|
||||
|
||||
|
||||
async def stop(self) -> None:
|
||||
def stop(self) -> None:
|
||||
"""
|
||||
Sends the terminate signal to the language server process and waits for it to exit, with a timeout, killing it if necessary
|
||||
"""
|
||||
# First cancel all tasks
|
||||
await self._cancel_pending_tasks()
|
||||
|
||||
process = self.process
|
||||
self.process = None
|
||||
if process:
|
||||
self._cleanup_process(process)
|
||||
|
||||
if not process:
|
||||
return
|
||||
|
||||
# Clean up the process
|
||||
await self._cleanup_process(process)
|
||||
|
||||
async def _cancel_pending_tasks(self):
|
||||
"""Cancel all pending tasks and wait for them to complete or timeout."""
|
||||
pending_tasks = []
|
||||
|
||||
# Use lock to safely access tasks dictionary
|
||||
with self._tasks_lock:
|
||||
for task in self.tasks.values():
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
pending_tasks.append(task)
|
||||
|
||||
if pending_tasks:
|
||||
try:
|
||||
await asyncio.wait_for(asyncio.gather(*pending_tasks, return_exceptions=True), timeout=5.0)
|
||||
except (asyncio.TimeoutError, Exception):
|
||||
pass
|
||||
|
||||
# Clear tasks dictionary under lock
|
||||
with self._tasks_lock:
|
||||
self.tasks = {}
|
||||
|
||||
async def _cleanup_process(self, process):
|
||||
def _cleanup_process(self, process):
|
||||
"""Clean up a process: close stdin, terminate/kill process, close stdout/stderr."""
|
||||
# Close stdin first to prevent deadlocks
|
||||
# See: https://bugs.python.org/issue35539
|
||||
@@ -183,7 +181,7 @@ class LanguageServerHandler:
|
||||
|
||||
# Terminate/kill the process if it's still running
|
||||
if process.returncode is None:
|
||||
await self._terminate_or_kill_process(process)
|
||||
self._terminate_or_kill_process(process)
|
||||
|
||||
# Close stdout and stderr pipes after process has exited
|
||||
# This is essential to prevent "I/O operation on closed pipe" errors and
|
||||
@@ -192,9 +190,6 @@ class LanguageServerHandler:
|
||||
self._safely_close_pipe(process.stdout)
|
||||
self._safely_close_pipe(process.stderr)
|
||||
|
||||
# Small delay to ensure OS has released file handles
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
def _safely_close_pipe(self, pipe):
|
||||
"""Safely close a pipe, ignoring any exceptions."""
|
||||
if pipe:
|
||||
@@ -203,14 +198,16 @@ class LanguageServerHandler:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _terminate_or_kill_process(self, process):
|
||||
def _terminate_or_kill_process(self, process):
|
||||
"""Try to terminate the process gracefully, then forcefully if necessary."""
|
||||
# First try to terminate the process tree gracefully
|
||||
self._signal_process_tree(process, terminate=True)
|
||||
|
||||
# TODO
|
||||
"""
|
||||
# Wait for the process to exit (with timeout)
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), timeout=10)
|
||||
asyncio.wait_for(process.wait(), timeout=10)
|
||||
except (asyncio.TimeoutError, Exception):
|
||||
# If termination failed, forcefully kill the process tree
|
||||
self._signal_process_tree(process, terminate=False)
|
||||
@@ -219,6 +216,7 @@ class LanguageServerHandler:
|
||||
await asyncio.wait_for(process.wait(), timeout=2)
|
||||
except Exception:
|
||||
pass
|
||||
"""
|
||||
|
||||
def _signal_process_tree(self, process, terminate=True):
|
||||
"""Send signal (terminate or kill) to the process and all its children."""
|
||||
@@ -252,23 +250,25 @@ class LanguageServerHandler:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
Perform the shutdown sequence for the client, including sending the shutdown request to the server and notifying it of exit
|
||||
"""
|
||||
self._log("Sending shutdown request to server")
|
||||
await self.send.shutdown()
|
||||
self.send.shutdown()
|
||||
self._log("Received shutdown response from server")
|
||||
self._received_shutdown = True
|
||||
self._log("Sending exit notification to server")
|
||||
self.notify.exit()
|
||||
self._log("Sent exit notification to server")
|
||||
# TODO
|
||||
"""
|
||||
if self.process and self.process.stdout:
|
||||
self.process.stdout.set_exception(StopLoopException())
|
||||
# This yields the control to the event loop to allow the exception to be handled
|
||||
# in the run_forever and run_forever_stderr methods
|
||||
await asyncio.sleep(0)
|
||||
"""
|
||||
|
||||
def _log(self, message: str | StringDict) -> None:
|
||||
"""
|
||||
@@ -277,14 +277,33 @@ class LanguageServerHandler:
|
||||
if self.logger is not None:
|
||||
self.logger("client", "logger", message)
|
||||
|
||||
async def run_forever(self) -> bool:
|
||||
@staticmethod
|
||||
def _read_bytes_from_process(process, stream, num_bytes):
|
||||
"""Read exactly num_bytes from process stdout"""
|
||||
if process.poll() is not None:
|
||||
# Process has terminated, check if we can still read
|
||||
pass
|
||||
|
||||
data = b''
|
||||
while len(data) < num_bytes:
|
||||
chunk = stream.read(num_bytes - len(data))
|
||||
if not chunk:
|
||||
if process.poll() is not None:
|
||||
raise EOFError(f"Process terminated. Expected {num_bytes} bytes, got {len(data)}")
|
||||
# Process still running but no data available yet
|
||||
time.sleep(0.01) # Small delay
|
||||
continue
|
||||
data += chunk
|
||||
return data
|
||||
|
||||
def run_forever(self) -> bool:
|
||||
"""
|
||||
Continuously read from the language server process stdout and handle the messages
|
||||
invoking the registered response and notification handlers
|
||||
"""
|
||||
try:
|
||||
while self.process and self.process.stdout and not self.process.stdout.at_eof():
|
||||
line = await self.process.stdout.readline()
|
||||
while self.process and self.process.stdout and self.process.stdout.readable():
|
||||
line = self.process.stdout.readline()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
@@ -294,39 +313,35 @@ class LanguageServerHandler:
|
||||
if num_bytes is None:
|
||||
continue
|
||||
while line and line.strip():
|
||||
line = await self.process.stdout.readline()
|
||||
line = self.process.stdout.readline()
|
||||
if not line:
|
||||
continue
|
||||
body = await self.process.stdout.readexactly(num_bytes)
|
||||
body = self._read_bytes_from_process(self.process, self.process.stdout, num_bytes)
|
||||
|
||||
# Use lock to prevent race conditions on tasks and task_counter
|
||||
with self._tasks_lock:
|
||||
self.tasks[self.task_counter] = asyncio.get_event_loop().create_task(self._handle_body(body))
|
||||
self.task_counter += 1
|
||||
except (BrokenPipeError, ConnectionResetError, StopLoopException):
|
||||
self._handle_body(body)
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
return self._received_shutdown
|
||||
|
||||
|
||||
async def run_forever_stderr(self) -> None:
|
||||
def run_forever_stderr(self) -> None:
|
||||
"""
|
||||
Continuously read from the language server process stderr and log the messages
|
||||
"""
|
||||
try:
|
||||
while self.process and self.process.stderr and not self.process.stderr.at_eof():
|
||||
line = await self.process.stderr.readline()
|
||||
while self.process and self.process.stderr and self.process.stderr.readable():
|
||||
line = self.process.stderr.readline()
|
||||
if not line:
|
||||
continue
|
||||
self._log("LSP stderr: " + line.decode(ENCODING, errors='replace'))
|
||||
except (BrokenPipeError, ConnectionResetError, StopLoopException):
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
|
||||
async def _handle_body(self, body: bytes) -> None:
|
||||
def _handle_body(self, body: bytes) -> None:
|
||||
"""
|
||||
Parse the body text received from the language server process and invoke the appropriate handler
|
||||
"""
|
||||
try:
|
||||
await self._receive_payload(json.loads(body))
|
||||
self._receive_payload(json.loads(body))
|
||||
except IOError as ex:
|
||||
self._log(f"malformed {ENCODING}: {ex}")
|
||||
except UnicodeDecodeError as ex:
|
||||
@@ -334,7 +349,7 @@ class LanguageServerHandler:
|
||||
except json.JSONDecodeError as ex:
|
||||
self._log(f"malformed JSON: {ex}")
|
||||
|
||||
async def _receive_payload(self, payload: StringDict) -> None:
|
||||
def _receive_payload(self, payload: StringDict) -> None:
|
||||
"""
|
||||
Determine if the payload received from server is for a request, response, or notification and invoke the appropriate handler
|
||||
"""
|
||||
@@ -343,11 +358,11 @@ class LanguageServerHandler:
|
||||
try:
|
||||
if "method" in payload:
|
||||
if "id" in payload:
|
||||
await self._request_handler(payload)
|
||||
self._request_handler(payload)
|
||||
else:
|
||||
await self._notification_handler(payload)
|
||||
self._notification_handler(payload)
|
||||
elif "id" in payload:
|
||||
await self._response_handler(payload)
|
||||
self._response_handler(payload)
|
||||
else:
|
||||
self._log(f"Unknown payload type: {payload}")
|
||||
except Exception as err:
|
||||
@@ -357,33 +372,22 @@ class LanguageServerHandler:
|
||||
"""
|
||||
Send notification pertaining to the given method to the server with the given parameters
|
||||
"""
|
||||
self._send_payload_sync(make_notification(method, params))
|
||||
self._send_payload(make_notification(method, params))
|
||||
|
||||
def send_response(self, request_id: Any, params: PayloadLike) -> None:
|
||||
"""
|
||||
Send response to the given request id to the server with the given parameters
|
||||
"""
|
||||
# Use lock to prevent race conditions on tasks and task_counter
|
||||
with self._tasks_lock:
|
||||
self.tasks[self.task_counter] = asyncio.get_event_loop().create_task(
|
||||
self._send_payload(make_response(request_id, params))
|
||||
)
|
||||
self.task_counter += 1
|
||||
|
||||
self._send_payload(make_response(request_id, params))
|
||||
|
||||
def send_error_response(self, request_id: Any, err: Error) -> None:
|
||||
"""
|
||||
Send error response to the given request id to the server with the given error
|
||||
"""
|
||||
# Use lock to prevent race conditions on tasks and task_counter
|
||||
with self._tasks_lock:
|
||||
self.tasks[self.task_counter] = asyncio.get_event_loop().create_task(
|
||||
self._send_payload(make_error_response(request_id, err))
|
||||
)
|
||||
self.task_counter += 1
|
||||
self._send_payload(make_error_response(request_id, err))
|
||||
|
||||
|
||||
async def send_request(self, method: str, params: Optional[dict] = None) -> PayloadLike:
|
||||
def send_request(self, method: str, params: Optional[dict] = None) -> PayloadLike:
|
||||
"""
|
||||
Send request to the server, register the request id, and wait for the response
|
||||
"""
|
||||
@@ -397,39 +401,19 @@ class LanguageServerHandler:
|
||||
with self._response_handlers_lock:
|
||||
self._response_handlers[request_id] = request
|
||||
|
||||
async with request.cv:
|
||||
await self._send_payload(make_request(method, request_id, params))
|
||||
self._log(f"Waiting for asyncio condition for request {method} with params:\n{params}")
|
||||
await request.cv.wait()
|
||||
self._log(f"Finished waiting, processing result")
|
||||
if isinstance(request.error, Error):
|
||||
raise MultilspyException(f"Could not process request {method} with params:\n{params}.\n Language server error: {request.error}") from request.error
|
||||
self._log(f"Returning non-error result, which is:\n{request.result}")
|
||||
return request.result
|
||||
self._send_payload(make_request(method, request_id, params))
|
||||
|
||||
self._log(f"Waiting for response to request {method} with params:\n{params}")
|
||||
result = request.get_result()
|
||||
|
||||
def _send_payload_sync(self, payload: StringDict) -> None:
|
||||
"""
|
||||
Send the payload to the server by writing to its stdin synchronously
|
||||
"""
|
||||
if not self.process or not self.process.stdin:
|
||||
return
|
||||
msg = create_message(payload)
|
||||
if self.logger:
|
||||
self.logger("client", "server", payload)
|
||||
self._log(f"Processing result")
|
||||
if result.is_error():
|
||||
raise MultilspyException(f"Could not process request {method} with params:\n{params}.\n Language server error: {result.error}") from result.error
|
||||
|
||||
self._log(f"Returning non-error result, which is:\n{result.payload}")
|
||||
return result.payload
|
||||
|
||||
# Use lock to prevent concurrent writes to stdin that cause buffer corruption
|
||||
with self._stdin_lock:
|
||||
try:
|
||||
self.process.stdin.writelines(msg)
|
||||
except (BrokenPipeError, ConnectionResetError, OSError) as e:
|
||||
# Log the error but don't raise to prevent cascading failures
|
||||
if self.logger:
|
||||
self.logger("client", "logger", f"Failed to write to stdin: {e}")
|
||||
return
|
||||
|
||||
|
||||
async def _send_payload(self, payload: StringDict) -> None:
|
||||
def _send_payload(self, payload: StringDict) -> None:
|
||||
"""
|
||||
Send the payload to the server by writing to its stdin asynchronously.
|
||||
"""
|
||||
@@ -442,14 +426,13 @@ class LanguageServerHandler:
|
||||
with self._stdin_lock:
|
||||
try:
|
||||
self.process.stdin.writelines(msg)
|
||||
await self.process.stdin.drain()
|
||||
self.process.stdin.flush()
|
||||
except (BrokenPipeError, ConnectionResetError, OSError) as e:
|
||||
# Log the error but don't raise to prevent cascading failures
|
||||
if self.logger:
|
||||
self.logger("client", "logger", f"Failed to write to stdin: {e}")
|
||||
return
|
||||
|
||||
|
||||
def on_request(self, method: str, cb) -> None:
|
||||
"""
|
||||
Register the callback function to handle requests from the server to the client for the given method
|
||||
@@ -462,7 +445,7 @@ class LanguageServerHandler:
|
||||
"""
|
||||
self.on_notification_handlers[method] = cb
|
||||
|
||||
async def _response_handler(self, response: StringDict) -> None:
|
||||
def _response_handler(self, response: StringDict) -> None:
|
||||
"""
|
||||
Handle the response received from the server for a request, using the id to determine the request
|
||||
"""
|
||||
@@ -470,14 +453,13 @@ class LanguageServerHandler:
|
||||
request = self._response_handlers.pop(response["id"])
|
||||
|
||||
if "result" in response and "error" not in response:
|
||||
await request.on_result(response["result"])
|
||||
request.on_result(response["result"])
|
||||
elif "result" not in response and "error" in response:
|
||||
await request.on_error(Error.from_lsp(response["error"]))
|
||||
request.on_error(Error.from_lsp(response["error"]))
|
||||
else:
|
||||
await request.on_error(Error(ErrorCodes.InvalidRequest, ""))
|
||||
request.on_error(Error(ErrorCodes.InvalidRequest, ""))
|
||||
|
||||
|
||||
async def _request_handler(self, response: StringDict) -> None:
|
||||
def _request_handler(self, response: StringDict) -> None:
|
||||
"""
|
||||
Handle the request received from the server: call the appropriate callback function and return the result
|
||||
"""
|
||||
@@ -495,13 +477,13 @@ class LanguageServerHandler:
|
||||
)
|
||||
return
|
||||
try:
|
||||
self.send_response(request_id, await handler(params))
|
||||
self.send_response(request_id, handler(params))
|
||||
except Error as ex:
|
||||
self.send_error_response(request_id, ex)
|
||||
except Exception as ex:
|
||||
self.send_error_response(request_id, Error(ErrorCodes.InternalError, str(ex)))
|
||||
|
||||
async def _notification_handler(self, response: StringDict) -> None:
|
||||
def _notification_handler(self, response: StringDict) -> None:
|
||||
"""
|
||||
Handle the notification received from the server: call the appropriate callback function
|
||||
"""
|
||||
@@ -512,7 +494,7 @@ class LanguageServerHandler:
|
||||
self._log(f"unhandled {method}")
|
||||
return
|
||||
try:
|
||||
await handler(params)
|
||||
handler(params)
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
except Exception as ex:
|
||||
|
||||
+103
-103
@@ -2,56 +2,56 @@ from typing import List, Union
|
||||
from multilspy.lsp_protocol_handler import lsp_types
|
||||
|
||||
|
||||
class LspRequest:
|
||||
class SolidLspRequest:
|
||||
def __init__(self, send_request):
|
||||
self.send_request = send_request
|
||||
|
||||
async def implementation(
|
||||
def implementation(
|
||||
self, params: lsp_types.ImplementationParams
|
||||
) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]:
|
||||
"""A request to resolve the implementation locations of a symbol at a given text
|
||||
document position. The request's parameter is of type [TextDocumentPositionParams]
|
||||
(#TextDocumentPositionParams) the response is of type {@link Definition} or a
|
||||
Thenable that resolves to such."""
|
||||
return await self.send_request("textDocument/implementation", params)
|
||||
return self.send_request("textDocument/implementation", params)
|
||||
|
||||
async def type_definition(
|
||||
def type_definition(
|
||||
self, params: lsp_types.TypeDefinitionParams
|
||||
) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]:
|
||||
"""A request to resolve the type definition locations of a symbol at a given text
|
||||
document position. The request's parameter is of type [TextDocumentPositionParams]
|
||||
(#TextDocumentPositionParams) the response is of type {@link Definition} or a
|
||||
Thenable that resolves to such."""
|
||||
return await self.send_request("textDocument/typeDefinition", params)
|
||||
return self.send_request("textDocument/typeDefinition", params)
|
||||
|
||||
async def document_color(
|
||||
def document_color(
|
||||
self, params: lsp_types.DocumentColorParams
|
||||
) -> List["lsp_types.ColorInformation"]:
|
||||
"""A request to list all color symbols found in a given text document. The request's
|
||||
parameter is of type {@link DocumentColorParams} the
|
||||
response is of type {@link ColorInformation ColorInformation[]} or a Thenable
|
||||
that resolves to such."""
|
||||
return await self.send_request("textDocument/documentColor", params)
|
||||
return self.send_request("textDocument/documentColor", params)
|
||||
|
||||
async def color_presentation(
|
||||
def color_presentation(
|
||||
self, params: lsp_types.ColorPresentationParams
|
||||
) -> List["lsp_types.ColorPresentation"]:
|
||||
"""A request to list all presentation for a color. The request's
|
||||
parameter is of type {@link ColorPresentationParams} the
|
||||
response is of type {@link ColorInformation ColorInformation[]} or a Thenable
|
||||
that resolves to such."""
|
||||
return await self.send_request("textDocument/colorPresentation", params)
|
||||
return self.send_request("textDocument/colorPresentation", params)
|
||||
|
||||
async def folding_range(
|
||||
def folding_range(
|
||||
self, params: lsp_types.FoldingRangeParams
|
||||
) -> Union[List["lsp_types.FoldingRange"], None]:
|
||||
"""A request to provide folding ranges in a document. The request's
|
||||
parameter is of type {@link FoldingRangeParams}, the
|
||||
response is of type {@link FoldingRangeList} or a Thenable
|
||||
that resolves to such."""
|
||||
return await self.send_request("textDocument/foldingRange", params)
|
||||
return self.send_request("textDocument/foldingRange", params)
|
||||
|
||||
async def declaration(
|
||||
def declaration(
|
||||
self, params: lsp_types.DeclarationParams
|
||||
) -> Union["lsp_types.Declaration", List["lsp_types.LocationLink"], None]:
|
||||
"""A request to resolve the type definition locations of a symbol at a given text
|
||||
@@ -59,129 +59,129 @@ class LspRequest:
|
||||
(#TextDocumentPositionParams) the response is of type {@link Declaration}
|
||||
or a typed array of {@link DeclarationLink} or a Thenable that resolves
|
||||
to such."""
|
||||
return await self.send_request("textDocument/declaration", params)
|
||||
return self.send_request("textDocument/declaration", params)
|
||||
|
||||
async def selection_range(
|
||||
def selection_range(
|
||||
self, params: lsp_types.SelectionRangeParams
|
||||
) -> Union[List["lsp_types.SelectionRange"], None]:
|
||||
"""A request to provide selection ranges in a document. The request's
|
||||
parameter is of type {@link SelectionRangeParams}, the
|
||||
response is of type {@link SelectionRange SelectionRange[]} or a Thenable
|
||||
that resolves to such."""
|
||||
return await self.send_request("textDocument/selectionRange", params)
|
||||
return self.send_request("textDocument/selectionRange", params)
|
||||
|
||||
async def prepare_call_hierarchy(
|
||||
def prepare_call_hierarchy(
|
||||
self, params: lsp_types.CallHierarchyPrepareParams
|
||||
) -> Union[List["lsp_types.CallHierarchyItem"], None]:
|
||||
"""A request to result a `CallHierarchyItem` in a document at a given position.
|
||||
Can be used as an input to an incoming or outgoing call hierarchy.
|
||||
|
||||
@since 3.16.0"""
|
||||
return await self.send_request("textDocument/prepareCallHierarchy", params)
|
||||
return self.send_request("textDocument/prepareCallHierarchy", params)
|
||||
|
||||
async def incoming_calls(
|
||||
def incoming_calls(
|
||||
self, params: lsp_types.CallHierarchyIncomingCallsParams
|
||||
) -> Union[List["lsp_types.CallHierarchyIncomingCall"], None]:
|
||||
"""A request to resolve the incoming calls for a given `CallHierarchyItem`.
|
||||
|
||||
@since 3.16.0"""
|
||||
return await self.send_request("callHierarchy/incomingCalls", params)
|
||||
return self.send_request("callHierarchy/incomingCalls", params)
|
||||
|
||||
async def outgoing_calls(
|
||||
def outgoing_calls(
|
||||
self, params: lsp_types.CallHierarchyOutgoingCallsParams
|
||||
) -> Union[List["lsp_types.CallHierarchyOutgoingCall"], None]:
|
||||
"""A request to resolve the outgoing calls for a given `CallHierarchyItem`.
|
||||
|
||||
@since 3.16.0"""
|
||||
return await self.send_request("callHierarchy/outgoingCalls", params)
|
||||
return self.send_request("callHierarchy/outgoingCalls", params)
|
||||
|
||||
async def semantic_tokens_full(
|
||||
def semantic_tokens_full(
|
||||
self, params: lsp_types.SemanticTokensParams
|
||||
) -> Union["lsp_types.SemanticTokens", None]:
|
||||
"""@since 3.16.0"""
|
||||
return await self.send_request("textDocument/semanticTokens/full", params)
|
||||
return self.send_request("textDocument/semanticTokens/full", params)
|
||||
|
||||
async def semantic_tokens_delta(
|
||||
def semantic_tokens_delta(
|
||||
self, params: lsp_types.SemanticTokensDeltaParams
|
||||
) -> Union["lsp_types.SemanticTokens", "lsp_types.SemanticTokensDelta", None]:
|
||||
"""@since 3.16.0"""
|
||||
return await self.send_request("textDocument/semanticTokens/full/delta", params)
|
||||
return self.send_request("textDocument/semanticTokens/full/delta", params)
|
||||
|
||||
async def semantic_tokens_range(
|
||||
def semantic_tokens_range(
|
||||
self, params: lsp_types.SemanticTokensRangeParams
|
||||
) -> Union["lsp_types.SemanticTokens", None]:
|
||||
"""@since 3.16.0"""
|
||||
return await self.send_request("textDocument/semanticTokens/range", params)
|
||||
return self.send_request("textDocument/semanticTokens/range", params)
|
||||
|
||||
async def linked_editing_range(
|
||||
def linked_editing_range(
|
||||
self, params: lsp_types.LinkedEditingRangeParams
|
||||
) -> Union["lsp_types.LinkedEditingRanges", None]:
|
||||
"""A request to provide ranges that can be edited together.
|
||||
|
||||
@since 3.16.0"""
|
||||
return await self.send_request("textDocument/linkedEditingRange", params)
|
||||
return self.send_request("textDocument/linkedEditingRange", params)
|
||||
|
||||
async def will_create_files(
|
||||
def will_create_files(
|
||||
self, params: lsp_types.CreateFilesParams
|
||||
) -> Union["lsp_types.WorkspaceEdit", None]:
|
||||
"""The will create files request is sent from the client to the server before files are actually
|
||||
created as long as the creation is triggered from within the client.
|
||||
|
||||
@since 3.16.0"""
|
||||
return await self.send_request("workspace/willCreateFiles", params)
|
||||
return self.send_request("workspace/willCreateFiles", params)
|
||||
|
||||
async def will_rename_files(
|
||||
def will_rename_files(
|
||||
self, params: lsp_types.RenameFilesParams
|
||||
) -> Union["lsp_types.WorkspaceEdit", None]:
|
||||
"""The will rename files request is sent from the client to the server before files are actually
|
||||
renamed as long as the rename is triggered from within the client.
|
||||
|
||||
@since 3.16.0"""
|
||||
return await self.send_request("workspace/willRenameFiles", params)
|
||||
return self.send_request("workspace/willRenameFiles", params)
|
||||
|
||||
async def will_delete_files(
|
||||
def will_delete_files(
|
||||
self, params: lsp_types.DeleteFilesParams
|
||||
) -> Union["lsp_types.WorkspaceEdit", None]:
|
||||
"""The did delete files notification is sent from the client to the server when
|
||||
files were deleted from within the client.
|
||||
|
||||
@since 3.16.0"""
|
||||
return await self.send_request("workspace/willDeleteFiles", params)
|
||||
return self.send_request("workspace/willDeleteFiles", params)
|
||||
|
||||
async def moniker(
|
||||
def moniker(
|
||||
self, params: lsp_types.MonikerParams
|
||||
) -> Union[List["lsp_types.Moniker"], None]:
|
||||
"""A request to get the moniker of a symbol at a given text document position.
|
||||
The request parameter is of type {@link TextDocumentPositionParams}.
|
||||
The response is of type {@link Moniker Moniker[]} or `null`."""
|
||||
return await self.send_request("textDocument/moniker", params)
|
||||
return self.send_request("textDocument/moniker", params)
|
||||
|
||||
async def prepare_type_hierarchy(
|
||||
def prepare_type_hierarchy(
|
||||
self, params: lsp_types.TypeHierarchyPrepareParams
|
||||
) -> Union[List["lsp_types.TypeHierarchyItem"], None]:
|
||||
"""A request to result a `TypeHierarchyItem` in a document at a given position.
|
||||
Can be used as an input to a subtypes or supertypes type hierarchy.
|
||||
|
||||
@since 3.17.0"""
|
||||
return await self.send_request("textDocument/prepareTypeHierarchy", params)
|
||||
return self.send_request("textDocument/prepareTypeHierarchy", params)
|
||||
|
||||
async def type_hierarchy_supertypes(
|
||||
def type_hierarchy_supertypes(
|
||||
self, params: lsp_types.TypeHierarchySupertypesParams
|
||||
) -> Union[List["lsp_types.TypeHierarchyItem"], None]:
|
||||
"""A request to resolve the supertypes for a given `TypeHierarchyItem`.
|
||||
|
||||
@since 3.17.0"""
|
||||
return await self.send_request("typeHierarchy/supertypes", params)
|
||||
return self.send_request("typeHierarchy/supertypes", params)
|
||||
|
||||
async def type_hierarchy_subtypes(
|
||||
def type_hierarchy_subtypes(
|
||||
self, params: lsp_types.TypeHierarchySubtypesParams
|
||||
) -> Union[List["lsp_types.TypeHierarchyItem"], None]:
|
||||
"""A request to resolve the subtypes for a given `TypeHierarchyItem`.
|
||||
|
||||
@since 3.17.0"""
|
||||
return await self.send_request("typeHierarchy/subtypes", params)
|
||||
return self.send_request("typeHierarchy/subtypes", params)
|
||||
|
||||
async def inline_value(
|
||||
def inline_value(
|
||||
self, params: lsp_types.InlineValueParams
|
||||
) -> Union[List["lsp_types.InlineValue"], None]:
|
||||
"""A request to provide inline values in a document. The request's parameter is of
|
||||
@@ -189,9 +189,9 @@ class LspRequest:
|
||||
{@link InlineValue InlineValue[]} or a Thenable that resolves to such.
|
||||
|
||||
@since 3.17.0"""
|
||||
return await self.send_request("textDocument/inlineValue", params)
|
||||
return self.send_request("textDocument/inlineValue", params)
|
||||
|
||||
async def inlay_hint(
|
||||
def inlay_hint(
|
||||
self, params: lsp_types.InlayHintParams
|
||||
) -> Union[List["lsp_types.InlayHint"], None]:
|
||||
"""A request to provide inlay hints in a document. The request's parameter is of
|
||||
@@ -199,9 +199,9 @@ class LspRequest:
|
||||
{@link InlayHint InlayHint[]} or a Thenable that resolves to such.
|
||||
|
||||
@since 3.17.0"""
|
||||
return await self.send_request("textDocument/inlayHint", params)
|
||||
return self.send_request("textDocument/inlayHint", params)
|
||||
|
||||
async def resolve_inlay_hint(
|
||||
def resolve_inlay_hint(
|
||||
self, params: lsp_types.InlayHint
|
||||
) -> "lsp_types.InlayHint":
|
||||
"""A request to resolve additional properties for an inlay hint.
|
||||
@@ -209,25 +209,25 @@ class LspRequest:
|
||||
of type {@link InlayHint} or a Thenable that resolves to such.
|
||||
|
||||
@since 3.17.0"""
|
||||
return await self.send_request("inlayHint/resolve", params)
|
||||
return self.send_request("inlayHint/resolve", params)
|
||||
|
||||
async def text_document_diagnostic(
|
||||
def text_document_diagnostic(
|
||||
self, params: lsp_types.DocumentDiagnosticParams
|
||||
) -> "lsp_types.DocumentDiagnosticReport":
|
||||
"""The document diagnostic request definition.
|
||||
|
||||
@since 3.17.0"""
|
||||
return await self.send_request("textDocument/diagnostic", params)
|
||||
return self.send_request("textDocument/diagnostic", params)
|
||||
|
||||
async def workspace_diagnostic(
|
||||
def workspace_diagnostic(
|
||||
self, params: lsp_types.WorkspaceDiagnosticParams
|
||||
) -> "lsp_types.WorkspaceDiagnosticReport":
|
||||
"""The workspace diagnostic request definition.
|
||||
|
||||
@since 3.17.0"""
|
||||
return await self.send_request("workspace/diagnostic", params)
|
||||
return self.send_request("workspace/diagnostic", params)
|
||||
|
||||
async def initialize(
|
||||
def initialize(
|
||||
self, params: lsp_types.InitializeParams
|
||||
) -> "lsp_types.InitializeResult":
|
||||
"""The initialize request is sent from the client to the server.
|
||||
@@ -235,16 +235,16 @@ class LspRequest:
|
||||
The requests parameter is of type {@link InitializeParams}
|
||||
the response if of type {@link InitializeResult} of a Thenable that
|
||||
resolves to such."""
|
||||
return await self.send_request("initialize", params)
|
||||
return self.send_request("initialize", params)
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
def shutdown(self) -> None:
|
||||
"""A shutdown request is sent from the client to the server.
|
||||
It is sent once when the client decides to shutdown the
|
||||
server. The only notification that is sent after a shutdown request
|
||||
is the exit event."""
|
||||
return await self.send_request("shutdown")
|
||||
return self.send_request("shutdown")
|
||||
|
||||
async def will_save_wait_until(
|
||||
def will_save_wait_until(
|
||||
self, params: lsp_types.WillSaveTextDocumentParams
|
||||
) -> Union[List["lsp_types.TextEdit"], None]:
|
||||
"""A document will save request is sent from the client to the server before
|
||||
@@ -253,9 +253,9 @@ class LspRequest:
|
||||
clients might drop results if computing the text edits took too long or if a
|
||||
server constantly fails on this request. This is done to keep the save fast and
|
||||
reliable."""
|
||||
return await self.send_request("textDocument/willSaveWaitUntil", params)
|
||||
return self.send_request("textDocument/willSaveWaitUntil", params)
|
||||
|
||||
async def completion(
|
||||
def completion(
|
||||
self, params: lsp_types.CompletionParams
|
||||
) -> Union[List["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]:
|
||||
"""Request to request completion at a given text document position. The request's
|
||||
@@ -268,30 +268,30 @@ class LspRequest:
|
||||
request. However, properties that are needed for the initial sorting and filtering, like `sortText`,
|
||||
`filterText`, `insertText`, and `textEdit`, must not be changed during resolve.
|
||||
"""
|
||||
return await self.send_request("textDocument/completion", params)
|
||||
return self.send_request("textDocument/completion", params)
|
||||
|
||||
async def resolve_completion_item(
|
||||
def resolve_completion_item(
|
||||
self, params: lsp_types.CompletionItem
|
||||
) -> "lsp_types.CompletionItem":
|
||||
"""Request to resolve additional information for a given completion item.The request's
|
||||
parameter is of type {@link CompletionItem} the response
|
||||
is of type {@link CompletionItem} or a Thenable that resolves to such."""
|
||||
return await self.send_request("completionItem/resolve", params)
|
||||
return self.send_request("completionItem/resolve", params)
|
||||
|
||||
async def hover(
|
||||
def hover(
|
||||
self, params: lsp_types.HoverParams
|
||||
) -> Union["lsp_types.Hover", None]:
|
||||
"""Request to request hover information at a given text document position. The request's
|
||||
parameter is of type {@link TextDocumentPosition} the response is of
|
||||
type {@link Hover} or a Thenable that resolves to such."""
|
||||
return await self.send_request("textDocument/hover", params)
|
||||
return self.send_request("textDocument/hover", params)
|
||||
|
||||
async def signature_help(
|
||||
def signature_help(
|
||||
self, params: lsp_types.SignatureHelpParams
|
||||
) -> Union["lsp_types.SignatureHelp", None]:
|
||||
return await self.send_request("textDocument/signatureHelp", params)
|
||||
return self.send_request("textDocument/signatureHelp", params)
|
||||
|
||||
async def definition(
|
||||
def definition(
|
||||
self, params: lsp_types.DefinitionParams
|
||||
) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]:
|
||||
"""A request to resolve the definition location of a symbol at a given text
|
||||
@@ -299,27 +299,27 @@ class LspRequest:
|
||||
(#TextDocumentPosition) the response is of either type {@link Definition}
|
||||
or a typed array of {@link DefinitionLink} or a Thenable that resolves
|
||||
to such."""
|
||||
return await self.send_request("textDocument/definition", params)
|
||||
return self.send_request("textDocument/definition", params)
|
||||
|
||||
async def references(
|
||||
def references(
|
||||
self, params: lsp_types.ReferenceParams
|
||||
) -> Union[List["lsp_types.Location"], None]:
|
||||
"""A request to resolve project-wide references for the symbol denoted
|
||||
by the given text document position. The request's parameter is of
|
||||
type {@link ReferenceParams} the response is of type
|
||||
{@link Location Location[]} or a Thenable that resolves to such."""
|
||||
return await self.send_request("textDocument/references", params)
|
||||
return self.send_request("textDocument/references", params)
|
||||
|
||||
async def document_highlight(
|
||||
def document_highlight(
|
||||
self, params: lsp_types.DocumentHighlightParams
|
||||
) -> Union[List["lsp_types.DocumentHighlight"], None]:
|
||||
"""Request to resolve a {@link DocumentHighlight} for a given
|
||||
text document position. The request's parameter is of type [TextDocumentPosition]
|
||||
(#TextDocumentPosition) the request response is of type [DocumentHighlight[]]
|
||||
(#DocumentHighlight) or a Thenable that resolves to such."""
|
||||
return await self.send_request("textDocument/documentHighlight", params)
|
||||
return self.send_request("textDocument/documentHighlight", params)
|
||||
|
||||
async def document_symbol(
|
||||
def document_symbol(
|
||||
self, params: lsp_types.DocumentSymbolParams
|
||||
) -> Union[
|
||||
List["lsp_types.SymbolInformation"], List["lsp_types.DocumentSymbol"], None
|
||||
@@ -328,23 +328,23 @@ class LspRequest:
|
||||
parameter is of type {@link TextDocumentIdentifier} the
|
||||
response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable
|
||||
that resolves to such."""
|
||||
return await self.send_request("textDocument/documentSymbol", params)
|
||||
return self.send_request("textDocument/documentSymbol", params)
|
||||
|
||||
async def code_action(
|
||||
def code_action(
|
||||
self, params: lsp_types.CodeActionParams
|
||||
) -> Union[List[Union["lsp_types.Command", "lsp_types.CodeAction"]], None]:
|
||||
"""A request to provide commands for the given text document and range."""
|
||||
return await self.send_request("textDocument/codeAction", params)
|
||||
return self.send_request("textDocument/codeAction", params)
|
||||
|
||||
async def resolve_code_action(
|
||||
def resolve_code_action(
|
||||
self, params: lsp_types.CodeAction
|
||||
) -> "lsp_types.CodeAction":
|
||||
"""Request to resolve additional information for a given code action.The request's
|
||||
parameter is of type {@link CodeAction} the response
|
||||
is of type {@link CodeAction} or a Thenable that resolves to such."""
|
||||
return await self.send_request("codeAction/resolve", params)
|
||||
return self.send_request("codeAction/resolve", params)
|
||||
|
||||
async def workspace_symbol(
|
||||
def workspace_symbol(
|
||||
self, params: lsp_types.WorkspaceSymbolParams
|
||||
) -> Union[
|
||||
List["lsp_types.SymbolInformation"], List["lsp_types.WorkspaceSymbol"], None
|
||||
@@ -358,78 +358,78 @@ class LspRequest:
|
||||
need to advertise support for WorkspaceSymbols via the client capability
|
||||
`workspace.symbol.resolveSupport`.
|
||||
"""
|
||||
return await self.send_request("workspace/symbol", params)
|
||||
return self.send_request("workspace/symbol", params)
|
||||
|
||||
async def resolve_workspace_symbol(
|
||||
def resolve_workspace_symbol(
|
||||
self, params: lsp_types.WorkspaceSymbol
|
||||
) -> "lsp_types.WorkspaceSymbol":
|
||||
"""A request to resolve the range inside the workspace
|
||||
symbol's location.
|
||||
|
||||
@since 3.17.0"""
|
||||
return await self.send_request("workspaceSymbol/resolve", params)
|
||||
return self.send_request("workspaceSymbol/resolve", params)
|
||||
|
||||
async def code_lens(
|
||||
def code_lens(
|
||||
self, params: lsp_types.CodeLensParams
|
||||
) -> Union[List["lsp_types.CodeLens"], None]:
|
||||
"""A request to provide code lens for the given text document."""
|
||||
return await self.send_request("textDocument/codeLens", params)
|
||||
return self.send_request("textDocument/codeLens", params)
|
||||
|
||||
async def resolve_code_lens(
|
||||
def resolve_code_lens(
|
||||
self, params: lsp_types.CodeLens
|
||||
) -> "lsp_types.CodeLens":
|
||||
"""A request to resolve a command for a given code lens."""
|
||||
return await self.send_request("codeLens/resolve", params)
|
||||
return self.send_request("codeLens/resolve", params)
|
||||
|
||||
async def document_link(
|
||||
def document_link(
|
||||
self, params: lsp_types.DocumentLinkParams
|
||||
) -> Union[List["lsp_types.DocumentLink"], None]:
|
||||
"""A request to provide document links"""
|
||||
return await self.send_request("textDocument/documentLink", params)
|
||||
return self.send_request("textDocument/documentLink", params)
|
||||
|
||||
async def resolve_document_link(
|
||||
def resolve_document_link(
|
||||
self, params: lsp_types.DocumentLink
|
||||
) -> "lsp_types.DocumentLink":
|
||||
"""Request to resolve additional information for a given document link. The request's
|
||||
parameter is of type {@link DocumentLink} the response
|
||||
is of type {@link DocumentLink} or a Thenable that resolves to such."""
|
||||
return await self.send_request("documentLink/resolve", params)
|
||||
return self.send_request("documentLink/resolve", params)
|
||||
|
||||
async def formatting(
|
||||
def formatting(
|
||||
self, params: lsp_types.DocumentFormattingParams
|
||||
) -> Union[List["lsp_types.TextEdit"], None]:
|
||||
"""A request to to format a whole document."""
|
||||
return await self.send_request("textDocument/formatting", params)
|
||||
return self.send_request("textDocument/formatting", params)
|
||||
|
||||
async def range_formatting(
|
||||
def range_formatting(
|
||||
self, params: lsp_types.DocumentRangeFormattingParams
|
||||
) -> Union[List["lsp_types.TextEdit"], None]:
|
||||
"""A request to to format a range in a document."""
|
||||
return await self.send_request("textDocument/rangeFormatting", params)
|
||||
return self.send_request("textDocument/rangeFormatting", params)
|
||||
|
||||
async def on_type_formatting(
|
||||
def on_type_formatting(
|
||||
self, params: lsp_types.DocumentOnTypeFormattingParams
|
||||
) -> Union[List["lsp_types.TextEdit"], None]:
|
||||
"""A request to format a document on type."""
|
||||
return await self.send_request("textDocument/onTypeFormatting", params)
|
||||
return self.send_request("textDocument/onTypeFormatting", params)
|
||||
|
||||
async def rename(
|
||||
def rename(
|
||||
self, params: lsp_types.RenameParams
|
||||
) -> Union["lsp_types.WorkspaceEdit", None]:
|
||||
"""A request to rename a symbol."""
|
||||
return await self.send_request("textDocument/rename", params)
|
||||
return self.send_request("textDocument/rename", params)
|
||||
|
||||
async def prepare_rename(
|
||||
def prepare_rename(
|
||||
self, params: lsp_types.PrepareRenameParams
|
||||
) -> Union["lsp_types.PrepareRenameResult", None]:
|
||||
"""A request to test and perform the setup necessary for a rename.
|
||||
|
||||
@since 3.16 - support for default behavior"""
|
||||
return await self.send_request("textDocument/prepareRename", params)
|
||||
return self.send_request("textDocument/prepareRename", params)
|
||||
|
||||
async def execute_command(
|
||||
def execute_command(
|
||||
self, params: lsp_types.ExecuteCommandParams
|
||||
) -> Union["lsp_types.LSPAny", None]:
|
||||
"""A request send from the client to the server to execute a command. The request might return
|
||||
a workspace edit which the client will apply to the workspace."""
|
||||
return await self.send_request("workspace/executeCommand", params)
|
||||
return self.send_request("workspace/executeCommand", params)
|
||||
|
||||
Reference in New Issue
Block a user