Пример #1
0
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)
Пример #2
0
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()
Пример #3
0
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
Пример #4
0
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)