class LangServer(MethodDispatcher): """ Language server for coala base on JSON RPC. """ def __init__(self, rx, tx): self.root_path = None self._jsonrpc_stream_reader = JsonRpcStreamReader(rx) self._jsonrpc_stream_writer = JsonRpcStreamWriter(tx) self._endpoint = Endpoint(self, self._jsonrpc_stream_writer.write) self._dispatchers = [] self._shutdown = False def start(self): self._jsonrpc_stream_reader.listen(self._endpoint.consume) def m_initialize(self, **params): """ Serve for the initialization request. """ # Notice that the root_path could be None. if 'rootUri' in params: self.root_path = path_from_uri(params['rootUri']) elif 'rootPath' in params: self.root_path = path_from_uri(params['rootPath']) return {'capabilities': {'textDocumentSync': 1}} def m_text_document__did_save(self, **params): """ Serve for did_change request. """ uri = params['textDocument']['uri'] path = path_from_uri(uri) diagnostics = output_to_diagnostics( run_coala_with_specific_file(self.root_path, path)) self.send_diagnostics(path, diagnostics) def m_shutdown(self, **_kwargs): self._shutdown = True # TODO: Support did_change and did_change_watched_files. # def serve_change(self, request): # '""Serve for the request of documentation changed.""' # params = request['params'] # uri = params['textDocument']['uri'] # path = path_from_uri(uri) # diagnostics = output_to_diagnostics( # run_coala_with_specific_file(self.root_path, path)) # self.send_diagnostics(path, diagnostics) # return None # # def serve_did_change_watched_files(self, request): # '""Serve for thr workspace/didChangeWatchedFiles request.""' # changes = request['changes'] # for fileEvent in changes: # uri = fileEvent['uri'] # path = path_from_uri(uri) # diagnostics = output_to_diagnostics( # run_coala_with_specific_file(self.root_path, path)) # self.send_diagnostics(path, diagnostics) def send_diagnostics(self, path, diagnostics): _diagnostics = [] if diagnostics is not None: _diagnostics = diagnostics params = { 'uri': 'file://{0}'.format(path), 'diagnostics': _diagnostics, } self._endpoint.notify('textDocument/publishDiagnostics', params=params)
class RstLanguageServer(MethodDispatcher): """ Implementation of the Microsoft VSCode Language Server Protocol https://github.com/Microsoft/language-server-protocol/blob/master/versions/protocol-1-x.md """ def capabilities(self) -> dict: server_capabilities = { # Defines how text documents are synced "textDocumentSync": { "change": constants.TextDocumentSyncKind.INCREMENTAL, "save": { "includeText": True }, "openClose": True, }, "workspace": { "workspaceFolders": { "supported": True, "changeNotifications": True } }, # features provided # "codeActionProvider": True, "codeLensProvider": { # Code lens has a resolve provider as well "resolveProvider": False }, "completionProvider": { "resolveProvider": False, "triggerCharacters": [], # [":"], }, # "documentFormattingProvider": True, # "documentHighlightProvider": True, # "documentRangeFormattingProvider": True, "documentSymbolProvider": True, "definitionProvider": True, "executeCommandProvider": { "commands": utils.flatten(self.call_plugins( PluginTypes.rst_commands.value)) }, "hoverProvider": True, "referencesProvider": True, # "renameProvider": True, "foldingRangeProvider": True, # "signatureHelpProvider": {"triggerCharacters": []}, # "experimental": any, } logger.info("Server capabilities: %s", server_capabilities) return server_capabilities def __init__(self, rx, tx, check_parent_process=False): """Initialise the server.""" self.root_uri = None self.config = None # type: Optional[Config] self.workspaces = {} # type: Dict[str, Workspace] self.watching_thread = None self._jsonrpc_stream_reader = JsonRpcStreamReader(rx) self._jsonrpc_stream_writer = JsonRpcStreamWriter(tx) self._check_parent_process = check_parent_process self._endpoint = Endpoint(self, self._jsonrpc_stream_writer.write, max_workers=MAX_WORKERS) self._dispatchers = [] self._shutdown = False def start(self): """Entry point for the server.""" self._jsonrpc_stream_reader.listen(self._endpoint.consume) def show_message(self, message: str, msg_type: int = constants.MessageType.Info): """Request the client show a pop-up message.""" self._endpoint.notify("window/showMessage", params={ "type": msg_type, "message": message }) def log_message(self, message: str, msg_type: int = constants.MessageType.Info): """Request the client log a message (in the servers output space).""" self._endpoint.notify("window/logMessage", params={ "type": msg_type, "message": str(message) }) def show_message_request( self, message: str, actions: List[dict] = (), msg_type: int = constants.MessageType.Info, ) -> Future: """Request the client show a pop-up message, with action buttons. Parameters ---------- actions: list[dict] e.g. [{"title": "A"}, {"title": "B"}] """ # for use see: https://github.com/Microsoft/language-server-protocol/issues/230 return self._endpoint.request( "window/showMessageRequest", params={ "type": msg_type, "message": message, "actions": list(actions) }, ) def request_config(self, items: List[dict]) -> Future: """Request configuration settings from the client. Parameters ---------- items : list[dict] e.g. [{"section": "rst_lsp"}] """ return self._endpoint.request("workspace/configuration", params={"items": items}) def publish_diagnostics(self, doc_uri: str, diagnostics: List[dict]): """Request configuration settings from the client.""" self._endpoint.notify( "textDocument/publishDiagnostics", params={ "uri": doc_uri, "diagnostics": diagnostics }, ) def apply_workspace_edit(self, edit: WorkspaceEdit): """Request to modify resource on the client side.""" return self._endpoint.request("workspace/applyEdit", params={"edit": edit}) def __getitem__(self, item): """Override getitem to fallback through multiple dispatchers.""" if self._shutdown and item != "exit": # exit is the only allowed method during shutdown logger.debug("Ignoring non-exit method during shutdown: %s", item) raise KeyError try: return super(RstLanguageServer, self).__getitem__(item) except KeyError: # Fallback through extra dispatchers for dispatcher in self._dispatchers: try: return dispatcher[item] except KeyError: continue raise KeyError() def m_shutdown(self, **_kwargs): self._shutdown = True return None def m_exit(self, **_kwargs): # Note: LSP protocol indicates that the server process should remain alive after # the client's Shutdown request, and wait for the client's Exit notification. for workspace in self.workspaces.values(): workspace.close() # TODO remove root cache? self._endpoint.shutdown() self._jsonrpc_stream_reader.close() self._jsonrpc_stream_writer.close() def match_uri_to_workspace(self, uri: str) -> Workspace: return uri2workspace(uri, self.workspaces, self.workspace) def match_uri_to_document(self, uri: str) -> Document: workspace = uri2workspace(uri, self.workspaces, self.workspace) return workspace.get_document(uri) def call_plugins(self, hook_name, doc_uri: Optional[str] = None, **kwargs): """Calls hook_name and returns a list of results from all registered handlers""" logger.debug("calling plugins") workspace = self.match_uri_to_workspace(doc_uri) doc = workspace.get_document(doc_uri) if doc_uri else None hook_handlers = self.config.plugin_manager.subset_hook_caller( hook_name, self.config.disabled_plugins) return hook_handlers(config=self.config, workspace=workspace, document=doc, **kwargs) @debounce(LINT_DEBOUNCE, keyed_by="doc_uri") def lint(self, doc_uri, is_saved): workspace = self.match_uri_to_workspace(doc_uri) if doc_uri in workspace.documents: self.publish_diagnostics( doc_uri, utils.flatten( self.call_plugins("rst_lint", doc_uri, is_saved=is_saved)), ) def m_initialize( self, processId: Optional[int] = None, rootUri: Optional[int] = None, rootPath: Optional[str] = None, initializationOptions: Optional[Any] = None, **_kwargs, ): logger.debug( "Language server initialized with %s %s %s %s", processId, rootUri, rootPath, initializationOptions, ) if rootUri is None: rootUri = uris.from_fs_path( rootPath) if rootPath is not None else "" self.workspaces.pop(self.root_uri, None) self.root_uri = rootUri self.config = Config( rootUri, initializationOptions or {}, processId, _kwargs.get("capabilities", {}), ) self.workspace = Workspace(rootUri, server=self, config=self.config) self.workspaces[rootUri] = self.workspace if (self._check_parent_process and processId is not None and self.watching_thread is None): def watch_parent_process(pid): # exit when the given pid is not alive if not utils.is_process_alive(pid): logger.info("parent process %s is not alive, exiting!", pid) self.m_exit() else: threading.Timer(PARENT_PROCESS_WATCH_INTERVAL, watch_parent_process, args=[pid]).start() self.watching_thread = threading.Thread( target=watch_parent_process, args=(processId, )) self.watching_thread.daemon = True self.watching_thread.start() return {"capabilities": self.capabilities()} def m_initialized(self, **_kwargs): pass def m_workspace__did_change_configuration(self, settings=None): self.config.update((settings or {}).get(CONFIG_NAMESPACE, {})) for workspace_uri in self.workspaces: workspace = self.workspaces[workspace_uri] # TODO debounce update_config (since requires read of all files) workspace.update_config(self.config) for doc_uri in workspace.documents: self.lint(doc_uri, is_saved=False) def m_workspace__did_change_workspace_folders(self, added=None, removed=None, **_kwargs): for removed_info in removed: removed_uri = removed_info["uri"] self.workspaces.pop(removed_uri) for added_info in added: added_uri = added_info["uri"] self.workspaces[added_uri] = Workspace(added_uri, server=self, config=self.config) # Migrate documents that are on the root workspace and have a better match now doc_uris = list(self.workspace.documents.keys()) for uri in doc_uris: doc = self.workspace._open_docs.pop(uri) new_workspace = self.match_uri_to_workspace(uri) new_workspace._docs[uri] = doc def m_workspace__did_change_watched_files(self, changes: List[FileEvent], **_kwargs): self.log_message(f"didChangeWatchedFile {changes}") # TODO use to remove deleted files from the database? # not working at moment, need to watch RST on client? def m_text_document__did_open(self, textDocument: TextDocument, **_kwargs): workspace = self.match_uri_to_workspace(textDocument["uri"]) workspace.put_document(textDocument) self.lint(textDocument["uri"], is_saved=False) def m_text_document__did_close(self, textDocument: TextDocument, **_kwargs): workspace = self.match_uri_to_workspace(textDocument["uri"]) workspace.rm_document(textDocument["uri"]) def m_text_document__did_save(self, textDocument: TextDocument, **_kwargs): self.lint(textDocument["uri"], is_saved=False) def m_text_document__did_change(self, contentChanges: List[TextEdit], textDocument: TextDocument, **_kwargs): workspace = self.match_uri_to_workspace(textDocument["uri"]) for change in contentChanges: workspace.update_document(textDocument["uri"], change, version=textDocument.get("version")) self.lint(textDocument["uri"], is_saved=False) # FEATURES # -------- def m_text_document__code_lens(self, textDocument: TextDocument, **_kwargs): return utils.flatten( self.call_plugins(PluginTypes.rst_code_lens.value, textDocument["uri"])) def m_text_document__completion(self, textDocument: TextDocument, position: Position, **_kwargs) -> CompletionList: completions = self.call_plugins(PluginTypes.rst_completions.value, textDocument["uri"], position=position) return {"isIncomplete": False, "items": utils.flatten(completions)} def m_text_document__definition(self, textDocument: TextDocument, position: Position, **_kwargs) -> List[Location]: # TODO can also return LinkLocation return utils.flatten( self.call_plugins( PluginTypes.rst_definitions.value, textDocument["uri"], position=position, )) def m_text_document__document_symbol(self, textDocument: TextDocument, **_kwargs) -> List[DocumentSymbol]: return utils.flatten( self.call_plugins(PluginTypes.rst_document_symbols.value, textDocument["uri"])) def m_text_document__folding_range(self, textDocument: TextDocument, **_kwargs): return self.call_plugins(PluginTypes.rst_folding_range.value, textDocument["uri"]) def m_text_document__hover(self, textDocument: TextDocument, position: Position, **_kwargs): return self.call_plugins(PluginTypes.rst_hover.value, textDocument["uri"], position=position) or { "contents": "" } def m_text_document__references(self, textDocument: TextDocument, position: Position, context=None, **_kwargs) -> List[Location]: return utils.flatten( self.call_plugins( PluginTypes.rst_references.value, textDocument["uri"], position=position, # Include the declaration of the current symbol exclude_declaration=not context["includeDeclaration"], )) def m_workspace__execute_command(self, command: str, arguments: Optional[List[Any]] = None): """The workspace/executeCommand request is sent from the client to the server, to trigger command execution on the server. In most cases the server creates a WorkspaceEdit structure and applies the changes to the workspace using the request workspace/applyEdit, which is sent from the server to the client. """ edit = self.call_plugins(PluginTypes.rst_execute_command.value, command=command, arguments=arguments) self.apply_workspace_edit(edit)
logging.basicConfig(level=logging.DEBUG) class Dispatcher(MethodDispatcher): def m_add(self, *, x: int, y: int) -> int: print("add!", x, y, x + y) return x + y q = Queue() def consume(params: dict) -> None: global q q.put_nowait(params) endpoint = Endpoint(Dispatcher(), consume) # notifyは単にconsumerに渡すだけ endpoint.notify("add", params={"x": 10, "y": 20}) # requestはfuturesのmappingに格納して終わりっぽい? fut = endpoint.request("add", params={"x": 10, "y": 20}) while not q.empty(): message = q.get() if "id" in message: result = endpoint._dispatcher[message["method"]](message["params"]) endpoint._handle_response(message["id"], result=result) endpoint.shutdown()
class TeaspnServer(MethodDispatcher): """ This class handles JSON-RPC requests to/from a TEASPN client, working as a middle layer that passes requests to a TeaspnHandler. Also does serialization and deseriaization of TEASPN objects. """ def __init__(self, rx, tx, handler: TeaspnHandler, check_parent_process=False): self.workspace = None self.config = None self._jsonrpc_stream_reader = JsonRpcStreamReader(rx) self._jsonrpc_stream_writer = JsonRpcStreamWriter(tx) self._handler = handler self._check_parent_process = check_parent_process self._endpoint = Endpoint(self, self._jsonrpc_stream_writer.write, max_workers=MAX_WORKERS) self._dispatchers = [] self._shutdown = False def start(self): """Entry point for the server.""" self._jsonrpc_stream_reader.listen(self._endpoint.consume) def m_initialize(self, processId=None, rootUri=None, rootPath=None, initializationOptions=None, **_kwargs): return { "capabilities": { "textDocumentSync": { "openClose": True, "change": TextDocumentSyncKind.Incremental }, "completionProvider": { "resolveProvider": True, "triggerCharacters": [' '] + list(__import__('string').ascii_lowercase) }, "codeActionProvider": True, "executeCommandProvider": { "commands": ['refactor.rewrite'] }, "definitionProvider": True, "hoverProvider": True } } def m_shutdown(self, **_kwargs): return None def m_text_document__did_open(self, textDocument=None, **_kwargs): self._handler.initialize_document(textDocument['uri'], textDocument['text']) diagnostics = self._handler.get_diagnostics() self._endpoint.notify( 'textDocument/publishDiagnostics', { 'uri': textDocument['uri'], 'diagnostics': [diagnostic.to_dict() for diagnostic in diagnostics] }) def m_text_document__did_change(self, textDocument=None, contentChanges=None, **_kwargs): for change in contentChanges: rng = Range.from_dict(change['range']) self._handler.update_document(range=rng, text=change['text']) diagnostics = self._handler.get_diagnostics() self._endpoint.notify( 'textDocument/publishDiagnostics', { 'uri': textDocument['uri'], 'diagnostics': [diagnostic.to_dict() for diagnostic in diagnostics] }) def m_text_document__syntax_highlight(self, textDocument=None, **_kwargs): highlights = self._handler.highlight_syntax() return [highlight.to_dict() for highlight in highlights] def m_text_document__completion(self, textDocument=None, position=None, **_kwargs): position = Position.from_dict(position) completion_list = self._handler.get_completion_list(position=position) return completion_list.to_dict() def m_workspace__search_example(self, query=None, **_kwargs): examples = self._handler.search_example(query) return [example.to_dict() for example in examples] def m_text_document__code_action(self, textDocument=None, range=None, context=None, **_kwargs): rng = Range.from_dict(range) actions = [] if context is not None and context.get('diagnostics', []): # code action for resolving diagnostics -> invoke quick fix diagnostics = [ Diagnostic.from_dict(diag) for diag in context['diagnostics'] ] actions = self._handler.run_quick_fix(rng, diagnostics) # obtain paraphrases commands = self._handler.run_code_action(rng) return [ action_or_command.to_dict() for action_or_command in actions + commands ] def m_workspace__execute_command(self, command=None, arguments=None, **_kwargs): if command == 'refactor.rewrite': self._endpoint.request('workspace/applyEdit', {'edit': arguments[0]}) def m_text_document__definition(self, textDocument=None, position=None, **_kwargs): position = Position.from_dict(position) locations = self._handler.search_definition(position, uri=textDocument['uri']) return [location.to_dict() for location in locations] def m_text_document__hover(self, textDocument=None, position=None, **_kwargs): position = Position.from_dict(position) hover = self._handler.hover(position) if hover: return hover.to_dict() else: return None
class LspSession(MethodDispatcher): """Send and Receive messages over LSP as a test LS Client.""" def __init__(self, cwd=None): self.cwd = cwd if cwd else os.getcwd() # pylint: disable=consider-using-with self._thread_pool = ThreadPoolExecutor() self._sub = None self._writer = None self._reader = None self._endpoint = None self._notification_callbacks = {} def __enter__(self): """Context manager entrypoint. shell=True needed for pytest-cov to work in subprocess. """ # pylint: disable=consider-using-with self._sub = subprocess.Popen( [ sys.executable, os.path.join(os.path.dirname(__file__), "lsp_run.py"), ], stdout=subprocess.PIPE, stdin=subprocess.PIPE, bufsize=0, cwd=self.cwd, env=os.environ, shell="WITH_COVERAGE" in os.environ, ) self._writer = JsonRpcStreamWriter( os.fdopen(self._sub.stdin.fileno(), "wb")) self._reader = JsonRpcStreamReader( os.fdopen(self._sub.stdout.fileno(), "rb")) dispatcher = { PUBLISH_DIAGNOSTICS: self._publish_diagnostics, WINDOW_SHOW_MESSAGE: self._window_show_message, WINDOW_LOG_MESSAGE: self._window_log_message, } self._endpoint = Endpoint(dispatcher, self._writer.write) self._thread_pool.submit(self._reader.listen, self._endpoint.consume) return self def __exit__(self, typ, value, _tb): self.shutdown(True) try: self._sub.terminate() except Exception: # pylint:disable=broad-except pass self._endpoint.shutdown() self._thread_pool.shutdown() def initialize( self, initialize_params=None, process_server_capabilities=None, ): """Sends the initialize request to LSP server.""" server_initialized = Event() def _after_initialize(fut): if process_server_capabilities: process_server_capabilities(fut.result()) self.initialized() server_initialized.set() self._send_request( "initialize", params=(initialize_params if initialize_params is not None else defaults.VSCODE_DEFAULT_INITIALIZE), handle_response=_after_initialize, ) server_initialized.wait() def initialized(self, initialized_params=None): """Sends the initialized notification to LSP server.""" self._endpoint.notify("initialized", initialized_params) def shutdown(self, should_exit, exit_timeout=LSP_EXIT_TIMEOUT): """Sends the shutdown request to LSP server.""" def _after_shutdown(_): if should_exit: self.exit_lsp(exit_timeout) self._send_request("shutdown", handle_response=_after_shutdown) def exit_lsp(self, exit_timeout=LSP_EXIT_TIMEOUT): """Handles LSP server process exit.""" self._endpoint.notify("exit") assert self._sub.wait(exit_timeout) == 0 def text_document_completion(self, completion_params): """Sends text document completion request to LSP server.""" fut = self._send_request("textDocument/completion", params=completion_params) return fut.result() def text_document_rename(self, rename_params): """Sends text document rename request to LSP server.""" fut = self._send_request("textDocument/rename", params=rename_params) return fut.result() def text_document_code_action(self, code_action_params): """Sends text document code action request to LSP server.""" fut = self._send_request("textDocument/codeAction", params=code_action_params) return fut.result() def text_document_hover(self, hover_params): """Sends text document hover request to LSP server.""" fut = self._send_request("textDocument/hover", params=hover_params) return fut.result() def text_document_signature_help(self, signature_help_params): """Sends text document hover request to LSP server.""" fut = self._send_request("textDocument/signatureHelp", params=signature_help_params) return fut.result() def text_document_definition(self, definition_params): """Sends text document defintion request to LSP server.""" fut = self._send_request("textDocument/definition", params=definition_params) return fut.result() def text_document_symbol(self, document_symbol_params): """Sends text document symbol request to LSP server.""" fut = self._send_request("textDocument/documentSymbol", params=document_symbol_params) return fut.result() def text_document_highlight(self, document_highlight_params): """Sends text document highlight request to LSP server.""" fut = self._send_request("textDocument/documentHighlight", params=document_highlight_params) return fut.result() def text_document_references(self, references_params): """Sends text document references request to LSP server.""" fut = self._send_request("textDocument/references", params=references_params) return fut.result() def workspace_symbol(self, workspace_symbol_params): """Sends workspace symbol request to LSP server.""" fut = self._send_request("workspace/symbol", params=workspace_symbol_params) return fut.result() def completion_item_resolve(self, resolve_params): """Sends completion item resolve request to LSP server.""" fut = self._send_request("completionItem/resolve", params=resolve_params) return fut.result() def notify_did_change(self, did_change_params): """Sends did change notification to LSP Server.""" self._send_notification("textDocument/didChange", params=did_change_params) def notify_did_save(self, did_save_params): """Sends did save notification to LSP Server.""" self._send_notification("textDocument/didSave", params=did_save_params) def notify_did_open(self, did_open_params): """Sends did open notification to LSP Server.""" self._send_notification("textDocument/didOpen", params=did_open_params) def set_notification_callback(self, notification_name, callback): """Set custom LS notification handler.""" self._notification_callbacks[notification_name] = callback def get_notification_callback(self, notification_name): """Gets callback if set or default callback for a given LS notification.""" try: return self._notification_callbacks[notification_name] except KeyError: def _default_handler(_params): """Default notification handler.""" return _default_handler def _publish_diagnostics(self, publish_diagnostics_params): """Internal handler for text document publish diagnostics.""" return self._handle_notification(PUBLISH_DIAGNOSTICS, publish_diagnostics_params) def _window_log_message(self, window_log_message_params): """Internal handler for window log message.""" return self._handle_notification(WINDOW_LOG_MESSAGE, window_log_message_params) def _window_show_message(self, window_show_message_params): """Internal handler for window show message.""" return self._handle_notification(WINDOW_SHOW_MESSAGE, window_show_message_params) def _handle_notification(self, notification_name, params): """Internal handler for notifications.""" fut = Future() def _handler(): callback = self.get_notification_callback(notification_name) callback(params) fut.set_result(None) self._thread_pool.submit(_handler) return fut def _send_request(self, name, params=None, handle_response=lambda f: f.done()): """Sends {name} request to the LSP server.""" fut = self._endpoint.request(name, params) fut.add_done_callback(handle_response) return fut def _send_notification(self, name, params=None): """Sends {name} notification to the LSP server.""" self._endpoint.notify(name, params)