diff options
Diffstat (limited to 'server/continuedev/core/lsp.py')
-rw-r--r-- | server/continuedev/core/lsp.py | 416 |
1 files changed, 416 insertions, 0 deletions
diff --git a/server/continuedev/core/lsp.py b/server/continuedev/core/lsp.py new file mode 100644 index 00000000..fc26c85c --- /dev/null +++ b/server/continuedev/core/lsp.py @@ -0,0 +1,416 @@ +import asyncio +import threading +from typing import List, Literal, Optional + +import aiohttp +from pydantic import BaseModel + +from ..models.filesystem import RangeInFile +from ..models.main import Position, Range + + +def filepath_to_uri(filename: str) -> str: + return f"file://{filename}" + + +def uri_to_filepath(uri: str) -> str: + if uri.startswith("file://"): + return uri[7:] + else: + return uri + + +PORT = 8099 + + +class LSPClient: + ready: bool = False + lock: asyncio.Lock = asyncio.Lock() + + def __init__(self, host: str, port: int, workspace_paths: List[str]): + self.host = host + self.port = port + self.session = aiohttp.ClientSession() + self.next_id = 0 + self.workspace_paths = workspace_paths + + async def connect(self): + print("Connecting") + self.ws = await self.session.ws_connect(f"ws://{self.host}:{self.port}/") + print("Connected") + self.ready = True + + async def send(self, data): + await self.ws.send_json(data) + + async def recv(self): + await self.lock.acquire() + + try: + return await self.ws.receive_json() + finally: + self.lock.release() + + async def close(self): + await self.ws.close() + await self.session.close() + + async def call_method(self, method_name, **kwargs): + body = { + "jsonrpc": "2.0", + "id": self.next_id, + "method": method_name, + "params": kwargs, + } + self.next_id += 1 + await self.send(body) + response = await self.recv() + return response + + async def initialize(self): + initialization_args = { + "capabilities": { + "textDocument": { + "codeAction": {"dynamicRegistration": True}, + "codeLens": {"dynamicRegistration": True}, + "colorProvider": {"dynamicRegistration": True}, + "completion": { + "completionItem": { + "commitCharactersSupport": True, + "documentationFormat": ["markdown", "plaintext"], + "snippetSupport": True, + }, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + ] + }, + "contextSupport": True, + "dynamicRegistration": True, + }, + "definition": {"dynamicRegistration": True}, + "documentHighlight": {"dynamicRegistration": True}, + "documentLink": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + ] + }, + }, + "formatting": {"dynamicRegistration": True}, + "hover": { + "contentFormat": ["markdown", "plaintext"], + "dynamicRegistration": True, + }, + "implementation": {"dynamicRegistration": True}, + "onTypeFormatting": {"dynamicRegistration": True}, + "publishDiagnostics": {"relatedInformation": True}, + "rangeFormatting": {"dynamicRegistration": True}, + "references": {"dynamicRegistration": True}, + "rename": {"dynamicRegistration": True}, + "signatureHelp": { + "dynamicRegistration": True, + "signatureInformation": { + "documentationFormat": ["markdown", "plaintext"] + }, + }, + "synchronization": { + "didSave": True, + "dynamicRegistration": True, + "willSave": True, + "willSaveWaitUntil": True, + }, + "typeDefinition": {"dynamicRegistration": True}, + }, + "workspace": { + "applyEdit": True, + "configuration": True, + "didChangeConfiguration": {"dynamicRegistration": True}, + "didChangeWatchedFiles": {"dynamicRegistration": True}, + "executeCommand": {"dynamicRegistration": True}, + "symbol": { + "dynamicRegistration": True, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + ] + }, + }, + "workspaceEdit": {"documentChanges": True}, + "workspaceFolders": True, + }, + }, + "processId": 1234, + "rootPath": None, + "rootUri": filepath_to_uri(self.workspace_paths[0]), + "initializationOptions": {}, + "trace": "off", + "workspaceFolders": [ + { + "uri": filepath_to_uri(workspacePath), + "name": workspacePath.split("/")[-1], + } + for workspacePath in self.workspace_paths + ], + } + return await self.call_method("initialize", **initialization_args) + + async def goto_definition(self, filepath: str, position: Position): + return await self.call_method( + "textDocument/definition", + textDocument={"uri": filepath_to_uri(filepath)}, + position=position.dict(), + ) + + async def document_symbol(self, filepath: str): + return await self.call_method( + "textDocument/documentSymbol", + textDocument={"uri": filepath_to_uri(filepath)}, + ) + + async def find_references( + self, filepath: str, position: Position, include_declaration: bool = False + ): + return await self.call_method( + "textDocument/references", + textDocument={"uri": filepath_to_uri(filepath)}, + position=position.dict(), + context={"includeDeclaration": include_declaration}, + ) + + async def folding_range(self, filepath: str): + response = await self.call_method( + "textDocument/foldingRange", + textDocument={"uri": filepath_to_uri(filepath)}, + ) + return response["result"] + + +async def start_language_server() -> threading.Thread: + """Manually start the python language server. Not used currently.""" + raise NotImplementedError() + # try: + # kill_proc(PORT) + # thread = threading.Thread( + # target=start_ws_lang_server, + # args=(PORT, False, PythonLSPServer), + # ) + # thread.daemon = True + # thread.start() + + # except Exception as e: + # logger.warning("Could not start TCP server: %s", e) + + # await asyncio.sleep(2) + + # return thread + + +class DocumentSymbol(BaseModel): + name: str + containerName: Optional[str] = None + kind: int + location: RangeInFile + + +class FoldingRange(BaseModel): + range: Range + kind: Optional[Literal["comment", "imports", "region"]] = None + + +class ContinueLSPClient(BaseModel): + workspace_dir: str + + lsp_client: LSPClient = None + lsp_thread: Optional[threading.Thread] = None + + @property + def ready(self): + if self.lsp_client is None: + return False + return self.lsp_client.ready + + class Config: + arbitrary_types_allowed = True + + def dict(self, **kwargs): + original_dict = super().dict(**kwargs) + original_dict.pop("lsp_client", None) + return original_dict + + async def start(self): + self.lsp_thread = await start_language_server() + self.lsp_client = LSPClient("localhost", PORT, [self.workspace_dir]) + await self.lsp_client.connect() + await self.lsp_client.initialize() + + async def stop(self): + await self.lsp_client.close() + if self.lsp_thread: + self.lsp_thread.join() + + def location_to_range_in_file(self, location): + return RangeInFile( + filepath=uri_to_filepath(location["uri"]), + range=Range.from_shorthand( + location["range"]["start"]["line"], + location["range"]["start"]["character"], + location["range"]["end"]["line"], + location["range"]["end"]["character"], + ), + ) + + async def goto_definition( + self, position: Position, filename: str + ) -> List[RangeInFile]: + response = self.lsp_client.goto_definition( + filename, + position, + ) + return [self.location_to_range_in_file(x) for x in response] + + async def find_references( + self, position: Position, filename: str, include_declaration: bool = False + ) -> List[RangeInFile]: + response = await self.lsp_client.find_references( + filename, + position, + include_declaration=include_declaration, + ) + return [self.location_to_range_in_file(x) for x in response["result"]] + + async def document_symbol(self, filepath: str) -> List: + response = await self.lsp_client.document_symbol(filepath) + return [ + DocumentSymbol( + name=x["name"], + containerName=x["containerName"], + kind=x["kind"], + location=self.location_to_range_in_file(x["location"]), + ) + for x in response["result"] + ] + + async def folding_range(self, filepath: str) -> List[FoldingRange]: + response = await self.lsp_client.folding_range(filepath) + + return [ + FoldingRange( + range=Range.from_shorthand( + x["startLine"], + x.get("startCharacter", 0), + x["endLine"] if "endCharacter" in x else x["endLine"] + 1, + x.get("endCharacter", 0), + ), + kind=x.get("kind"), + ) + for x in response + ] + + async def get_enclosing_folding_range_of_position( + self, position: Position, filepath: str + ) -> Optional[FoldingRange]: + ranges = await self.folding_range(filepath) + + max_start_position = Position(line=0, character=0) + max_range = None + for r in ranges: + if r.range.contains(position): + if r.range.start > max_start_position: + max_start_position = r.range.start + max_range = r + + return max_range + + async def get_enclosing_folding_range( + self, range_in_file: RangeInFile + ) -> Optional[FoldingRange]: + ranges = await self.folding_range(range_in_file.filepath) + + max_start_position = Position(line=0, character=0) + max_range = None + for r in ranges: + if r.range.contains(range_in_file.range.start) and r.range.contains( + range_in_file.range.end + ): + if r.range.start > max_start_position: + max_start_position = r.range.start + max_range = r + + return max_range |