示例#1
0
    def __init__(self, rx, tx, check_parent_process=False, consumer=None):
        self.workspace = None
        self.config = None
        self.root_uri = None
        self.watching_thread = None
        self.workspaces = {}
        self.uri_workspace_mapper = {}

        self._check_parent_process = check_parent_process

        if rx is not None:
            self._jsonrpc_stream_reader = JsonRpcStreamReader(rx)
        else:
            self._jsonrpc_stream_reader = None

        if tx is not None:
            self._jsonrpc_stream_writer = JsonRpcStreamWriter(tx)
        else:
            self._jsonrpc_stream_writer = None

        # if consumer is None, it is assumed that the default streams-based approach is being used
        if consumer is None:
            self._endpoint = Endpoint(self,
                                      self._jsonrpc_stream_writer.write,
                                      max_workers=MAX_WORKERS)
        else:
            self._endpoint = Endpoint(self, consumer, max_workers=MAX_WORKERS)

        self._dispatchers = []
        self._shutdown = False
示例#2
0
def setup_bsp_server():
    with setup_pipes() as pipes:
        context = BSPContext()
        rule_runner = RuleRunner(rules=bsp_rules(), extra_session_values={BSPContext: context})
        conn = BSPConnection(
            rule_runner.scheduler,
            rule_runner.union_membership,
            context,
            pipes.inbound_reader,
            pipes.outbound_writer,
        )

        def run_bsp_server():
            conn.run()

        bsp_thread = Thread(target=run_bsp_server)
        bsp_thread.daemon = True
        bsp_thread.start()

        client_reader = JsonRpcStreamReader(pipes.outbound_reader)
        client_writer = JsonRpcStreamWriter(pipes.inbound_writer)
        endpoint = Endpoint({}, lambda msg: client_writer.write(msg))

        def run_client():
            client_reader.listen(lambda msg: endpoint.consume(msg))

        client_thread = Thread(target=run_client)
        client_thread.daemon = True
        client_thread.start()

        try:
            yield endpoint
        finally:
            client_reader.close()
            client_writer.close()
示例#3
0
    def __init__(self, rx, tx, check_parent_process=False):
        self.workspace = None
        self.config = None
        self.root_uri = None
        self.watching_thread = None
        self.workspaces = {}
        self.uri_workspace_mapper = {}

        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
示例#4
0
def setup_bsp_server(
    rule_runner: RuleRunner | None = None, *, notification_names: set[str] | None = None
):
    rule_runner = rule_runner or RuleRunner(rules=bsp_rules())
    notification_names = notification_names or set()
    stdio_destination = native_engine.stdio_thread_get_destination()

    with setup_pipes() as pipes:
        context = BSPContext()
        rule_runner.set_session_values({BSPContext: context})
        conn = BSPConnection(
            rule_runner.scheduler,
            rule_runner.union_membership,
            context,
            pipes.server_reader,
            pipes.server_writer,
        )

        def run_bsp_server():
            native_engine.stdio_thread_set_destination(stdio_destination)
            conn.run()

        bsp_thread = Thread(target=run_bsp_server)
        bsp_thread.daemon = True
        bsp_thread.start()

        client_reader = JsonRpcStreamReader(pipes.client_reader)
        client_writer = JsonRpcStreamWriter(pipes.client_writer)
        notifications = Notifications([])
        endpoint = Endpoint(
            {name: functools.partial(notifications._record, name) for name in notification_names},
            lambda msg: client_writer.write(msg),
        )

        def run_client():
            client_reader.listen(lambda msg: endpoint.consume(msg))

        client_thread = Thread(target=run_client)
        client_thread.daemon = True
        client_thread.start()

        try:
            yield endpoint, notifications
        finally:
            client_reader.close()
            client_writer.close()
示例#5
0
    def __init__(
        self,
        scheduler_session: SchedulerSession,
        union_membership: UnionMembership,
        context: BSPContext,
        inbound: BinaryIO,
        outbound: BinaryIO,
        max_workers: int = 5,
    ) -> None:
        self._scheduler_session = scheduler_session
        self._inbound = JsonRpcStreamReader(inbound)
        self._outbound = JsonRpcStreamWriter(outbound)
        self._context: BSPContext = context
        self._endpoint = Endpoint(self, self._send_outbound_message, max_workers=max_workers)

        self._handler_mappings: dict[str, type[BSPHandlerMapping]] = {}
        impls = union_membership.get(BSPHandlerMapping)
        for impl in impls:
            self._handler_mappings[impl.method_name] = impl
示例#6
0
class PythonLSPServer(MethodDispatcher):
    """ Implementation of the Microsoft VSCode Language Server Protocol
    https://github.com/Microsoft/language-server-protocol/blob/master/versions/protocol-1-x.md
    """

    # pylint: disable=too-many-public-methods,redefined-builtin

    def __init__(self, rx, tx, check_parent_process=False):
        self.workspace = None
        self.config = None
        self.root_uri = None
        self.watching_thread = None
        self.workspaces = {}
        self.uri_workspace_mapper = {}

        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 __getitem__(self, item):
        """Override getitem to fallback through multiple dispatchers."""
        if self._shutdown and item != 'exit':
            # exit is the only allowed method during shutdown
            log.debug("Ignoring non-exit method during shutdown: %s", item)
            raise KeyError

        try:
            return super().__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

    def m_exit(self, **_kwargs):
        self._endpoint.shutdown()
        self._jsonrpc_stream_reader.close()
        self._jsonrpc_stream_writer.close()

    def _match_uri_to_workspace(self, uri):
        workspace_uri = _utils.match_uri_to_workspace(uri, self.workspaces)
        return self.workspaces.get(workspace_uri, self.workspace)

    def _hook(self, hook_name, doc_uri=None, **kwargs):
        """Calls hook_name and returns a list of results from all registered handlers"""
        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)

    def capabilities(self):
        server_capabilities = {
            'codeActionProvider': True,
            'codeLensProvider': {
                'resolveProvider':
                False,  # We may need to make this configurable
            },
            'completionProvider': {
                'resolveProvider': False,  # We know everything ahead of time
                'triggerCharacters': ['.']
            },
            'documentFormattingProvider': True,
            'documentHighlightProvider': True,
            'documentRangeFormattingProvider': True,
            'documentSymbolProvider': True,
            'definitionProvider': True,
            'executeCommandProvider': {
                'commands': flatten(self._hook('pylsp_commands'))
            },
            'hoverProvider': True,
            'referencesProvider': True,
            'renameProvider': True,
            'foldingRangeProvider': True,
            'signatureHelpProvider': {
                'triggerCharacters': ['(', ',', '=']
            },
            'textDocumentSync': {
                'change': lsp.TextDocumentSyncKind.INCREMENTAL,
                'save': {
                    'includeText': True,
                },
                'openClose': True,
            },
            'workspace': {
                'workspaceFolders': {
                    'supported': True,
                    'changeNotifications': True
                }
            },
            'experimental':
            merge(self._hook('pylsp_experimental_capabilities'))
        }
        log.info('Server capabilities: %s', server_capabilities)
        return server_capabilities

    def m_initialize(self,
                     processId=None,
                     rootUri=None,
                     rootPath=None,
                     initializationOptions=None,
                     **_kwargs):
        log.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.Config(rootUri, initializationOptions or {},
                                    processId, _kwargs.get('capabilities', {}))
        self.workspace = Workspace(rootUri, self._endpoint, self.config)
        self.workspaces[rootUri] = self.workspace
        self._dispatchers = self._hook('pylsp_dispatchers')
        self._hook('pylsp_initialize')

        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):
                    log.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()
        # Get our capabilities
        return {'capabilities': self.capabilities()}

    def m_initialized(self, **_kwargs):
        self._hook('pylsp_initialized')

    def code_actions(self, doc_uri, range, context):
        return flatten(
            self._hook('pylsp_code_actions',
                       doc_uri,
                       range=range,
                       context=context))

    def code_lens(self, doc_uri):
        return flatten(self._hook('pylsp_code_lens', doc_uri))

    def completions(self, doc_uri, position):
        completions = self._hook('pylsp_completions',
                                 doc_uri,
                                 position=position)
        return {'isIncomplete': False, 'items': flatten(completions)}

    def definitions(self, doc_uri, position):
        return flatten(
            self._hook('pylsp_definitions', doc_uri, position=position))

    def document_symbols(self, doc_uri):
        return flatten(self._hook('pylsp_document_symbols', doc_uri))

    def execute_command(self, command, arguments):
        return self._hook('pylsp_execute_command',
                          command=command,
                          arguments=arguments)

    def format_document(self, doc_uri):
        return self._hook('pylsp_format_document', doc_uri)

    def format_range(self, doc_uri, range):
        return self._hook('pylsp_format_range', doc_uri, range=range)

    def highlight(self, doc_uri, position):
        return flatten(
            self._hook('pylsp_document_highlight', doc_uri,
                       position=position)) or None

    def hover(self, doc_uri, position):
        return self._hook('pylsp_hover', doc_uri, position=position) or {
            'contents': ''
        }

    @_utils.debounce(LINT_DEBOUNCE_S, keyed_by='doc_uri')
    def lint(self, doc_uri, is_saved):
        # Since we're debounced, the document may no longer be open
        workspace = self._match_uri_to_workspace(doc_uri)
        if doc_uri in workspace.documents:
            workspace.publish_diagnostics(
                doc_uri,
                flatten(self._hook('pylsp_lint', doc_uri, is_saved=is_saved)))

    def references(self, doc_uri, position, exclude_declaration):
        return flatten(
            self._hook('pylsp_references',
                       doc_uri,
                       position=position,
                       exclude_declaration=exclude_declaration))

    def rename(self, doc_uri, position, new_name):
        return self._hook('pylsp_rename',
                          doc_uri,
                          position=position,
                          new_name=new_name)

    def signature_help(self, doc_uri, position):
        return self._hook('pylsp_signature_help', doc_uri, position=position)

    def folding(self, doc_uri):
        return flatten(self._hook('pylsp_folding_range', doc_uri))

    def m_text_document__did_close(self, textDocument=None, **_kwargs):
        workspace = self._match_uri_to_workspace(textDocument['uri'])
        workspace.rm_document(textDocument['uri'])

    def m_text_document__did_open(self, textDocument=None, **_kwargs):
        workspace = self._match_uri_to_workspace(textDocument['uri'])
        workspace.put_document(textDocument['uri'],
                               textDocument['text'],
                               version=textDocument.get('version'))
        self._hook('pylsp_document_did_open', textDocument['uri'])
        self.lint(textDocument['uri'], is_saved=True)

    def m_text_document__did_change(self,
                                    contentChanges=None,
                                    textDocument=None,
                                    **_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)

    def m_text_document__did_save(self, textDocument=None, **_kwargs):
        self.lint(textDocument['uri'], is_saved=True)

    def m_text_document__code_action(self,
                                     textDocument=None,
                                     range=None,
                                     context=None,
                                     **_kwargs):
        return self.code_actions(textDocument['uri'], range, context)

    def m_text_document__code_lens(self, textDocument=None, **_kwargs):
        return self.code_lens(textDocument['uri'])

    def m_text_document__completion(self,
                                    textDocument=None,
                                    position=None,
                                    **_kwargs):
        return self.completions(textDocument['uri'], position)

    def m_text_document__definition(self,
                                    textDocument=None,
                                    position=None,
                                    **_kwargs):
        return self.definitions(textDocument['uri'], position)

    def m_text_document__document_highlight(self,
                                            textDocument=None,
                                            position=None,
                                            **_kwargs):
        return self.highlight(textDocument['uri'], position)

    def m_text_document__hover(self,
                               textDocument=None,
                               position=None,
                               **_kwargs):
        return self.hover(textDocument['uri'], position)

    def m_text_document__document_symbol(self, textDocument=None, **_kwargs):
        return self.document_symbols(textDocument['uri'])

    def m_text_document__formatting(self,
                                    textDocument=None,
                                    _options=None,
                                    **_kwargs):
        # For now we're ignoring formatting options.
        return self.format_document(textDocument['uri'])

    def m_text_document__rename(self,
                                textDocument=None,
                                position=None,
                                newName=None,
                                **_kwargs):
        return self.rename(textDocument['uri'], position, newName)

    def m_text_document__folding_range(self, textDocument=None, **_kwargs):
        return self.folding(textDocument['uri'])

    def m_text_document__range_formatting(self,
                                          textDocument=None,
                                          range=None,
                                          _options=None,
                                          **_kwargs):
        # Again, we'll ignore formatting options for now.
        return self.format_range(textDocument['uri'], range)

    def m_text_document__references(self,
                                    textDocument=None,
                                    position=None,
                                    context=None,
                                    **_kwargs):
        exclude_declaration = not context['includeDeclaration']
        return self.references(textDocument['uri'], position,
                               exclude_declaration)

    def m_text_document__signature_help(self,
                                        textDocument=None,
                                        position=None,
                                        **_kwargs):
        return self.signature_help(textDocument['uri'], position)

    def m_workspace__did_change_configuration(self, settings=None):
        self.config.update((settings or {}).get('pylsp', {}))
        for workspace_uri in self.workspaces:
            workspace = self.workspaces[workspace_uri]
            workspace.update_config(settings)
            for doc_uri in workspace.documents:
                self.lint(doc_uri, is_saved=False)

    def m_workspace__did_change_workspace_folders(self, event=None, **_kwargs):  # pylint: disable=too-many-locals
        if event is None:
            return
        added = event.get('added', [])
        removed = event.get('removed', [])

        for removed_info in removed:
            if 'uri' in removed_info:
                removed_uri = removed_info['uri']
                self.workspaces.pop(removed_uri, None)

        for added_info in added:
            if 'uri' in added_info:
                added_uri = added_info['uri']
                workspace_config = config.Config(added_uri,
                                                 self.config._init_opts,
                                                 self.config._process_id,
                                                 self.config._capabilities)
                workspace_config.update(self.config._settings)
                self.workspaces[added_uri] = Workspace(added_uri,
                                                       self._endpoint,
                                                       workspace_config)

        root_workspace_removed = any(removed_info['uri'] == self.root_uri
                                     for removed_info in removed)
        workspace_added = len(added) > 0 and 'uri' in added[0]
        if root_workspace_removed and workspace_added:
            added_uri = added[0]['uri']
            self.root_uri = added_uri
            new_root_workspace = self.workspaces[added_uri]
            self.config = new_root_workspace._config
            self.workspace = new_root_workspace
        elif root_workspace_removed:
            # NOTE: Removing the root workspace can only happen when the server
            # is closed, thus the else condition of this if can never happen.
            if self.workspaces:
                log.debug('Root workspace deleted!')
                available_workspaces = sorted(self.workspaces)
                first_workspace = available_workspaces[0]
                new_root_workspace = self.workspaces[first_workspace]
                self.root_uri = first_workspace
                self.config = new_root_workspace._config
                self.workspace = new_root_workspace

        # Migrate documents that are on the root workspace and have a better
        # match now
        doc_uris = list(self.workspace._docs.keys())
        for uri in doc_uris:
            doc = self.workspace._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=None, **_kwargs):
        changed_py_files = set()
        config_changed = False
        for d in (changes or []):
            if d['uri'].endswith(PYTHON_FILE_EXTENSIONS):
                changed_py_files.add(d['uri'])
            elif d['uri'].endswith(CONFIG_FILEs):
                config_changed = True

        if config_changed:
            self.config.settings.cache_clear()
        elif not changed_py_files:
            # Only externally changed python files and lint configs may result in changed diagnostics.
            return

        for workspace_uri in self.workspaces:
            workspace = self.workspaces[workspace_uri]
            for doc_uri in workspace.documents:
                # Changes in doc_uri are already handled by m_text_document__did_save
                if doc_uri not in changed_py_files:
                    self.lint(doc_uri, is_saved=False)

    def m_workspace__execute_command(self, command=None, arguments=None):
        return self.execute_command(command, arguments)
示例#7
0
class BSPConnection:
    _INITIALIZE_METHOD_NAME = "build/initialize"
    _SHUTDOWN_METHOD_NAME = "build/shutdown"
    _EXIT_NOTIFCATION_NAME = "build/exit"

    def __init__(
        self,
        scheduler_session: SchedulerSession,
        union_membership: UnionMembership,
        context: BSPContext,
        inbound: BinaryIO,
        outbound: BinaryIO,
        max_workers: int = 5,
    ) -> None:
        self._scheduler_session = scheduler_session
        self._inbound = JsonRpcStreamReader(inbound)
        self._outbound = JsonRpcStreamWriter(outbound)
        self._context: BSPContext = context
        self._endpoint = Endpoint(self, self._send_outbound_message, max_workers=max_workers)

        self._handler_mappings: dict[str, type[BSPHandlerMapping]] = {}
        impls = union_membership.get(BSPHandlerMapping)
        for impl in impls:
            self._handler_mappings[impl.method_name] = impl

    def run(self) -> None:
        """Run the listener for inbound JSON-RPC messages."""
        self._inbound.listen(self._received_inbound_message)

    def _received_inbound_message(self, msg):
        """Process each inbound JSON-RPC message."""
        _logger.info(f"_received_inbound_message: msg={msg}")
        self._endpoint.consume(msg)

    def _send_outbound_message(self, msg):
        _logger.info(f"_send_outbound_message: msg={msg}")
        self._outbound.write(msg)

    # TODO: Figure out how to run this on the `Endpoint`'s thread pool by returing a callable. For now, we
    # need to return errors as futures given that `Endpoint` only handles exceptions returned that way versus using a try ... except block.
    def _handle_inbound_message(self, *, method_name: str, params: Any):
        # If the connection is not yet initialized and this is not the initialization request, BSP requires
        # returning an error for methods (and to discard all notifications).
        #
        # Concurrency: This method can be invoked from multiple threads (for each individual request). By returning
        # an error for all other requests, only the thread running the initialization RPC should be able to proceed.
        # This ensures that we can safely call `initialize_connection` on the BSPContext with the client-supplied
        # init parameters without worrying about multiple threads. (Not entirely true though as this does not handle
        # the client making multiple concurrent initialization RPCs, but which would violate the protocol in any case.)
        if (
            not self._context.is_connection_initialized
            and method_name != self._INITIALIZE_METHOD_NAME
        ):
            return _make_error_future(
                JsonRpcException(
                    code=-32002, message=f"Client must first call `{self._INITIALIZE_METHOD_NAME}`."
                )
            )

        # Handle the `build/shutdown` method and `build/exit` notification.
        if method_name == self._SHUTDOWN_METHOD_NAME:
            # Return no-op success for the `build/shutdown` method. This doesn't actually cause the server to
            # exit. That will occur once the client sends the `build/exit` notification.
            return None
        elif method_name == self._EXIT_NOTIFCATION_NAME:
            # The `build/exit` notification directs the BSP server to immediately exit.
            # The read-dispatch loop will exit once it notices that the inbound handle is closed. So close the
            # inbound handle (and outbound handle for completeness) and then return to the dispatch loop
            # to trigger the exit.
            self._inbound.close()
            self._outbound.close()
            return None

        method_mapping = self._handler_mappings.get(method_name)
        if not method_mapping:
            return _make_error_future(JsonRpcMethodNotFound.of(method_name))

        try:
            request = method_mapping.request_type.from_json_dict(params)
        except Exception:
            return _make_error_future(JsonRpcInvalidRequest())

        # TODO: This should not be necessary: see https://github.com/pantsbuild/pants/issues/15435.
        self._scheduler_session.new_run_id()

        workspace = Workspace(self._scheduler_session)
        params = Params(request, workspace)
        execution_request = self._scheduler_session.execution_request(
            products=[method_mapping.response_type],
            subjects=[params],
        )
        returns, throws = self._scheduler_session.execute(execution_request)
        if len(returns) == 1 and len(throws) == 0:
            # Initialize the BSPContext with the client-supplied init parameters. See earlier comment on why this
            # call to `BSPContext.initialize_connection` is safe.
            if method_name == self._INITIALIZE_METHOD_NAME:
                self._context.initialize_connection(request, self.notify_client)
            return returns[0][1].value.to_json_dict()
        elif len(returns) == 0 and len(throws) == 1:
            raise throws[0][1].exc
        else:
            raise AssertionError(
                f"Received unexpected result from engine: returns={returns}; throws={throws}"
            )

    # Called by `Endpoint` to dispatch requests and notifications.
    # TODO: Should probably vendor `Endpoint` so we can detect notifications versus method calls, which
    # matters when ignoring unknown notifications versus erroring for unknown methods.
    def __getitem__(self, method_name):
        def handler(params):
            return self._handle_inbound_message(method_name=method_name, params=params)

        return handler

    def notify_client(self, notification: BSPNotification) -> None:
        try:
            self._endpoint.notify(notification.notification_name, notification.to_json_dict())
        except Exception as ex:
            _logger.warning(f"Received exception while notifying BSP client: {ex}")