Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ classifiers = [
dependencies = [
"requests==2.33.0",
"pyright==1.1.403",
"basedpyright==1.38.4",
"fortls==3.2.2",
"overrides==7.7.0",
"python-dotenv==1.2.1",
Expand Down Expand Up @@ -293,6 +294,7 @@ addopts = "--snapshot-patch-pycharm-diff"
markers = [
"clojure: language server running for Clojure",
"python: language server running for Python",
"python_basedpyright: language server running for Python (basedpyright)",
"go: language server running for Go",
"java: language server running for Java",
"kotlin: language server running for kotlin",
Expand Down
163 changes: 163 additions & 0 deletions src/solidlsp/language_servers/basedpyright_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""
Python language server integration using basedpyright (a fork of pyright with additional features).

You can pass the following entries in ``ls_specific_settings["python_basedpyright"]``:
- ls_path: Override the executable used to start basedpyright-langserver.
"""

import logging
import os
import pathlib
import re
import sys
import threading
from typing import cast

from overrides import override

from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings

log = logging.getLogger(__name__)


class BasedPyrightServer(SolidLanguageServer):
"""
Provides Python specific instantiation of the LanguageServer class using basedpyright.
"""

def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
super().__init__(
config,
repository_root_path,
None,
"python",
solidlsp_settings,
)

self.analysis_complete = threading.Event()
self.found_source_files = False

def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)

class DependencyProvider(LanguageServerDependencyProviderSinglePath):
def _get_or_install_core_dependency(self) -> str:
return sys.executable

def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path, "-m", "basedpyright.langserver", "--stdio"]

@override
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"]

@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
initialize_params = { # type: ignore
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": pathlib.Path(repository_absolute_path).as_uri(),
"initializationOptions": {
"exclude": [
"**/__pycache__",
"**/.venv",
"**/.env",
"**/build",
"**/dist",
"**/.pixi",
],
"reportMissingImports": "error",
},
"capabilities": {
"workspace": {
"workspaceEdit": {"documentChanges": True},
"didChangeConfiguration": {"dynamicRegistration": True},
"didChangeWatchedFiles": {"dynamicRegistration": True},
"symbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"executeCommand": {"dynamicRegistration": True},
},
"textDocument": {
"synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"signatureHelp": {
"dynamicRegistration": True,
"signatureInformation": {
"documentationFormat": ["markdown", "plaintext"],
"parameterInformation": {"labelOffsetSupport": True},
},
},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 27))},
"hierarchicalDocumentSymbolSupport": True,
},
"publishDiagnostics": {"relatedInformation": True},
},
},
"workspaceFolders": [
{"uri": pathlib.Path(repository_absolute_path).as_uri(), "name": os.path.basename(repository_absolute_path)}
],
}

return cast(InitializeParams, initialize_params)

def _start_server(self) -> None:
def execute_client_command_handler(params: dict) -> list:
return []

def do_nothing(params: dict) -> None:
return

def window_log_message(msg: dict) -> None:
message_text = msg.get("message", "")
log.info(f"LSP: window/logMessage: {message_text}")

if re.search(r"Found \d+ source files?", message_text):
log.info("basedpyright workspace scanning complete")
self.found_source_files = True
self.analysis_complete.set()

def check_experimental_status(params: dict) -> None:
if params.get("quiescent") == True:
log.info("Received experimental/serverStatus with quiescent=true")
if not self.found_source_files:
self.analysis_complete.set()

self.server.on_request("client/registerCapability", do_nothing)
self.server.on_notification("language/status", do_nothing)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.server.on_notification("language/actionableNotification", do_nothing)
self.server.on_notification("experimental/serverStatus", check_experimental_status)

log.info("Starting basedpyright-langserver server process")
self.server.start()

initialize_params = self._get_initialize_params(self.repository_root_path)

log.info("Sending initialize request from LSP client to basedpyright server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
log.info(f"Received initialize response from basedpyright server: {init_response}")

assert "textDocumentSync" in init_response["capabilities"]
assert "completionProvider" in init_response["capabilities"]
assert "definitionProvider" in init_response["capabilities"]

self.server.notify.initialized({})

log.info("Waiting for basedpyright to complete initial workspace analysis...")
if self.analysis_complete.wait(timeout=5.0):
log.info("basedpyright initial analysis complete, server ready")
else:
log.warning("Timeout waiting for basedpyright analysis completion, proceeding anyway")
self.analysis_complete.set()
9 changes: 8 additions & 1 deletion src/solidlsp/ls_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ class Language(str, Enum):
"""Jedi language server for Python (instead of pyright, which is the default)"""
PYTHON_TY = "python_ty"
"""Ty language server for Python (instead of pyright, which is the default)."""
PYTHON_BASEDPYRIGHT = "python_basedpyright"
"""basedpyright language server for Python (fork of pyright with additional features)."""
CSHARP_OMNISHARP = "csharp_omnisharp"
"""OmniSharp language server for C# (instead of the default csharp-ls by microsoft).
Currently has problems with finding references, and generally seems less stable and performant.
Expand Down Expand Up @@ -157,6 +159,7 @@ def is_experimental(self) -> bool:
self.TYPESCRIPT_VTS,
self.PYTHON_JEDI,
self.PYTHON_TY,
self.PYTHON_BASEDPYRIGHT,
self.CSHARP_OMNISHARP,
self.RUBY_SOLARGRAPH,
self.PHP_PHPACTOR,
Expand Down Expand Up @@ -190,7 +193,7 @@ def get_priority(self) -> int:

def get_source_fn_matcher(self) -> FilenameMatcher:
match self:
case self.PYTHON | self.PYTHON_JEDI | self.PYTHON_TY:
case self.PYTHON | self.PYTHON_JEDI | self.PYTHON_TY | self.PYTHON_BASEDPYRIGHT:
return FilenameMatcher("*.py", "*.pyi")
case self.JAVA:
return FilenameMatcher("*.java")
Expand Down Expand Up @@ -328,6 +331,10 @@ def get_ls_class(self) -> type["SolidLanguageServer"]:
from solidlsp.language_servers.ty_server import TyLanguageServer

return TyLanguageServer
case self.PYTHON_BASEDPYRIGHT:
from solidlsp.language_servers.basedpyright_server import BasedPyrightServer

return BasedPyrightServer
case self.JAVA:
from solidlsp.language_servers.eclipse_jdtls import EclipseJDTLS

Expand Down
1 change: 1 addition & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class LanguageParamRequest:
Language.CPP_CCLS: Language.CPP,
Language.PHP_PHPACTOR: Language.PHP,
Language.PYTHON_JEDI: Language.PYTHON,
Language.PYTHON_BASEDPYRIGHT: Language.PYTHON,
Language.RUBY_SOLARGRAPH: Language.RUBY,
}

Expand Down
Loading
Loading