multilspy 43209c8521e93e8c41bdd2060800474fd0b9f8b1

commit 6c2565baababc9c978b7c1ab40a56fadef16c816
  Author: Developer <trane@trane-VirtualBox>
  Date:   Fri Apr 4 16:08:10 2025 +0530

      address review comments for clangd support

  src/multilspy/language_servers/clangd_language_server/clangd_language_server.py

  commit 78f1c2b43ee16bb29b222a1c1af77956d6fd906d
  Author: IDontHaveBrain <jonamjun.dev@gmail.com>
  Date:   Thu Apr 3 09:00:26 2025 +0900

      chore: Remove win-arm64 support from kotlin runtime dependencies

  src/multilspy/language_servers/kotlin_language_server/runtime_dependencies.json

  commit b907cc227a01f4a500d3445bfa17d7237f91c5ec
  Author: nj.jo <jonamjun.dev@gmail.com>
  Date:   Tue Apr 1 22:39:18 2025 +0900

      chore: use vscode-java runtime dependencies

  src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py
  src/multilspy/language_servers/kotlin_language_server/runtime_dependencies.json

  commit 3d19a282fe3d68d1a0b589c879775cac7ec23b8e
  Author: moz-mdauer <mdauer@mozilla.com>
  Date:   Tue Apr 1 13:56:45 2025 +0200

      feat: add support for workspace/symbol request

  src/multilspy/language_server.py
  src/multilspy/lsp_protocol_handler/lsp_constants.py

  commit e60081cbcac5b938ee8936f715478321c2be9b5c
  Author: nj.jo <jonamjun.dev@gmail.com>
  Date:   Sat Mar 29 14:55:16 2025 +0900

      refactor: improve process termination handling

  src/multilspy/lsp_protocol_handler/server.py

  commit 88345e14e2172390b5f0d48d7877b2ee8620c187
  Author: nj.jo <jonamjun.dev@gmail.com>
  Date:   Sat Mar 29 14:12:28 2025 +0900

      feat: add JDK runtime dependency

  src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py
  src/multilspy/language_servers/kotlin_language_server/runtime_dependencies.json

  commit ef9ad63083ebbbf929fd1cc4d0cc084e5cb69ad4
  Author: Andrej730 <azhilenkov@gmail.com>
  Date:   Sun Mar 23 19:26:08 2025 +0500

      Location.relativePath to use `None` value if relative path is not available #85

  src/multilspy/language_server.py
  src/multilspy/multilspy_types.py
  src/multilspy/multilspy_utils.py

  commit 85b1fe84140ec7bb84db843d618ea4bd8f4a096c
  Author: Huang YunKun <htynkn@gmail.com>
  Date:   Fri Mar 28 11:40:34 2025 +0800

      Update runtime_dependencies.json

  src/multilspy/language_servers/eclipse_jdtls/runtime_dependencies.json

  commit f0a42717cd95f9d7a845603cee60ac2fd22f16ac
  Author: Andrej730 <azhilenkov@gmail.com>
  Date:   Sun Mar 23 19:15:21 2025 +0500

      SyncLanguageServer.create - small documentation note

      To clarify that relative paths for repositories are not supported.

      Currently if it's using relative path, `create_server` fails with traceback similar to the one below:
      ```python
      Traceback (most recent call last):
        File "third_test.py", line 11, in <module>
          with lsp.start_server():
        File "\Python311\Lib\contextlib.py", line 137, in __enter__
          return next(self.gen)
                 ^^^^^^^^^^^^^^
        File "\Python311\Lib\site-packages\multilspy\language_server.py", line 744, in start_server
          asyncio.run_coroutine_threadsafe(ctx.__aenter__(), loop=self.loop).result()
        File "\Python311\Lib\concurrent\futures\_base.py", line 456, in result
          return self.__get_result()
                 ^^^^^^^^^^^^^^^^^^^
        File "\Python311\Lib\concurrent\futures\_base.py", line 401, in __get_result
          raise self._exception
        File "\Python311\Lib\contextlib.py", line 210, in __aenter__
          return await anext(self.gen)
                 ^^^^^^^^^^^^^^^^^^^^^
        File "\Python311\Lib\site-packages\multilspy\language_servers\jedi_language_server\jedi_server.py", line 101, in start_server
          initialize_params = self._get_initialize_params(self.repository_root_path)
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "\Python311\Lib\site-packages\multilspy\language_servers\jedi_language_server\jedi_server.py", line 50, in _get_initialize_params
          d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri()
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "\Python311\Lib\pathlib.py", line 566, in as_uri
          raise ValueError("relative path can't be expressed as a file URI")
      ValueError: relative path can't be expressed as a file URI
      ```

  src/multilspy/language_server.py

  commit 9f69f75b1c9a460d63dbcc9d1b2b50596ee8a49b
  Author: Developer <trane@trane-VirtualBox>
  Date:   Tue Mar 18 18:54:18 2025 +0530

      clangd language server commit

  src/multilspy/language_server.py
  src/multilspy/language_servers/clangd_language_server/clangd_language_server.py
  src/multilspy/language_servers/clangd_language_server/initialize_params.json
  src/multilspy/language_servers/clangd_language_server/runtime_dependencies.json
  src/multilspy/multilspy_config.py

  commit edeb1771b8cd6064e6eca28a1126988780fc4608
  Author: nj.jo <jonamjun.dev@gmail.com>
  Date:   Sun Mar 9 01:26:59 2025 +0900

      test: update Kotlin executable permissions and adjust Java version in workflow

  src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py

  commit 79a8f6902e91c0a1301c63b9f8737efbcbcccf24
  Author: nj.jo <jonamjun.dev@gmail.com>
  Date:   Sun Mar 9 00:47:38 2025 +0900

      fix: improve process termination handling

  src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py
  src/multilspy/lsp_protocol_handler/server.py

  commit 90622f2788df412021565008dd83c27ac4d0b467
  Author: nj.jo <jonamjun.dev@gmail.com>
  Date:   Sun Mar 9 00:10:28 2025 +0900

      fix: improve process termination handling

  src/multilspy/lsp_protocol_handler/server.py

  commit 907e6f340dec6b6e50c5712059bf3563fd7f064c
  Author: nj.jo <jonamjun.dev@gmail.com>
  Date:   Sat Mar 8 23:52:07 2025 +0900

      fix: improve process termination handling

  src/multilspy/lsp_protocol_handler/server.py

  commit 694b30108ff3025b7f6f2e7923c6077f6b0dee23
  Author: nj.jo <jonamjun.dev@gmail.com>
  Date:   Sat Mar 8 23:45:48 2025 +0900

      chore: cleanup

  src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py

  commit 635a6a83617e02a24c141a85b8dd8cabbf3a4601
  Author: nj.jo <jonamjun.dev@gmail.com>
  Date:   Sat Mar 8 21:14:00 2025 +0900

      fix: platform check for Kotlin LSP from architecture-specific to OS-only

  src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py

  commit 2b053ee8b1f8b8d2ff391110baf27bb21163b715
  Author: nj.jo <jonamjun.dev@gmail.com>
  Date:   Sat Mar 8 20:25:05 2025 +0900

      feat: Update Kotlin Language Server configuration and runtime dependencies

  src/multilspy/language_servers/kotlin_language_server/initialize_params.json
  src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py
  src/multilspy/language_servers/kotlin_language_server/runtime_dependencies.json

  commit 59f84082b9cd64e64bfc9a7644a644fdb8cc881e
  Author: nj.jo <jonamjun.dev@gmail.com>
  Date:   Sat Mar 8 17:51:18 2025 +0900

      feat: Add Kotlin language server support

  src/multilspy/language_server.py
  src/multilspy/language_servers/kotlin_language_server/initialize_params.json
  src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py
  src/multilspy/language_servers/kotlin_language_server/runtime_dependencies.json
  src/multilspy/multilspy_config.py

  commit c85e2825aaa85d64611e7bfb392daa6eb7aa54ac
  Author: Jet Zhou <jet@jetzhou.com>
  Date:   Thu Mar 6 16:10:28 2025 -0800

      Add `start_independent_lsp_process` configuration option to `MultilspyConfig`

      Default value is `True`, which means language server process will be in
      its own process group. SIGINT and SIGTERM will only be received by the
      `multilspy` python process itself, not propagated. We rely on the
      existing `shutdown` and `stop` code to clean up the process.

      If set to `False`, the language server process will be under the same
      process group as the `multilspy` python process. This means SIGINT and
      SIGTERM will be received by both processes. This may cause issues if the
      python process has clean up code that runs after receiving
      SIGINT/SIGTERM because the `start_server` context manager will begin
      running its cleanup code, part of which invokes the `shutdown` and
      `stop` in `language_protocol_handler.server`. This will hang forever
      while awaiting an response from the language server process, since it
      would have already terminated itself after receiving SIGINT/SIGTERM.

  src/multilspy/language_server.py
  src/multilspy/lsp_protocol_handler/server.py
  src/multilspy/multilspy_config.py

  commit ff17f9365f4862627f00c43f8f88cd7a76f10fc6
  Author: Jet Zhou <jet@jetzhou.com>
  Date:   Tue Mar 4 22:27:55 2025 -0800

      Let language server subprocess be its own process group

      In situations where `multilspy` is launched from another process, a
      SIGINT or SIGTERM signal will be received by both the launching process
      as well as the process running the language server because
      `asyncio.create_subprocess_shell` defaults to `start_new_session=False`.
      In such a situation, `lsp_protocol_handler.server.shutdown` will be
      called but will hang since the underlying process is already terminated,
      `lsp_protocol_handler.server` won't ever receive an answer back for
      `shutdown`.

      This change makes it so that SIGINT and SIGTERM won't propagate to the
      language server process. We rely on the clean up code already in place
      to shut down the process as desired.

      See
      [python documentation](https://docs.python.org/3.13/library/subprocess.html#subprocess.Popen)
      for more details on `start_new_session`

  src/multilspy/lsp_protocol_handler/server.py

  commit ee104b062d1e0fda4a063937f5ca6488ac42683e
  Author: Jet Zhou <jet@jetzhou.com>
  Date:   Mon Mar 3 18:08:27 2025 -0800

      Add assertion string to `references` and `document_symbol` requests to language server

      These endpoints could return `None` in case the symbol is one that's not
      found. Adding the assertion string can help caller catch these
      exceptions and handle the errors as appropriate.

  src/multilspy/language_server.py

  commit 2edff10b31585fbcd5829afc07d1258b3b83b7c8
  Author: nj.jo <jonamjun.dev@gmail.com>
  Date:   Sun Mar 2 12:49:34 2025 +0900

      refactor: Change to use `PlatformUtils` instead of `sys.platform`

  src/multilspy/language_servers/typescript_language_server/typescript_language_server.py

  commit e2cc4dd5f54adf89a2e4e8140387fc71fd5ebc56
  Author: v4rgas <66626747+v4rgas@users.noreply.github.com>
  Date:   Sat Mar 1 20:25:38 2025 -0300

      feat: add initialization options for Dart Language Server

  src/multilspy/language_servers/dart_language_server/initialize_params.json

  commit 807ec41b1098a2aecccbbe074f80d8c2dbd15d5d
  Author: v4rgas <66626747+v4rgas@users.noreply.github.com>
  Date:   Sat Mar 1 20:15:38 2025 -0300

      fix: update description for Dart Language Server runtime dependencies

  src/multilspy/language_servers/dart_language_server/runtime_dependencies.json

  commit ca5e3e91e7093838f3cc33ef5fb5561fdf9cf7ee
  Author: v4rgas <66626747+v4rgas@users.noreply.github.com>
  Date:   Sat Mar 1 20:12:53 2025 -0300

      feat: implement Dart Language Server runtime dependency setup

  src/multilspy/language_servers/dart_language_server/dart_language_server.py
  src/multilspy/language_servers/dart_language_server/runtime_dependencies.json

  commit 6295dd19db279033e2507dd665bd5c7df79e2623
  Author: nj.jo <jonamjun.dev@gmail.com>
  Date:   Sat Mar 1 15:18:41 2025 +0900

      fix: resolve 'No module named pwd' error on Windows

  src/multilspy/language_servers/typescript_language_server/typescript_language_server.py

  commit 2390a21479553e2b42e06eb10049d5c09ab9ef0a
  Author: v4rgas <66626747+v4rgas@users.noreply.github.com>
  Date:   Fri Feb 28 09:54:58 2025 -0300

      fix race codnition in process termination

  src/multilspy/lsp_protocol_handler/server.py

  commit e193501fb605ba86d52513f9fe044c2576bfc4cd
  Author: v4rgas <66626747+v4rgas@users.noreply.github.com>
  Date:   Thu Feb 27 22:05:03 2025 -0300

      feat: add Dart language support and corresponding tests

  src/multilspy/language_server.py
  src/multilspy/language_servers/dart_language_server/dart_language_server.py
  src/multilspy/language_servers/dart_language_server/initialize_params.json
  src/multilspy/multilspy_config.py

  commit 11c0da5450d0ce032b5ea832561e87ead6a0ca5b
  Author: v4rgas <66626747+v4rgas@users.noreply.github.com>
  Date:   Thu Feb 27 21:43:32 2025 -0300

      Add psutil dependency and enhance process termination handling

  src/multilspy/lsp_protocol_handler/server.py

  commit 7b85f39636e1971b0e9e0b9e69f4f8d7be51f931
  Author: v4rgas <66626747+v4rgas@users.noreply.github.com>
  Date:   Thu Feb 27 09:25:02 2025 -0300

      chore: undo styling changes

  src/multilspy/language_server.py

  commit 0d567618dda795c0789b374667c0325c3df4c87e
  Author: Jet Zhou <jet@jetzhou.com>
  Date:   Tue Feb 11 17:55:35 2025 -0800

      Force `npm i` to use current directory, in case some parent directory already contains a node_modules folder

  src/multilspy/language_servers/typescript_language_server/runtime_dependencies.json
This commit is contained in:
Dominik Jain committed 2025-04-08 20:10:34 +02:00
1 parent 87134b6ba0
commit 90c42923c8
19 files changed
+1608 -201

No files matched your search

+1 -1
View File
@@ -1 +1 @@
07ffa48166beb0c7f20ac76dbf7724d0bca4bbb4
43209c8521e93e8c41bdd2060800474fd0b9f8b1
-1
View File
@@ -1 +0,0 @@
e766d55fd568f5313fb46796b8463097bdb9a293
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,180 @@
"""
Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++.
"""
import asyncio
import json
import logging
import os
import stat
import pathlib
from contextlib import asynccontextmanager
from typing import AsyncIterator
from multilspy.multilspy_logger import MultilspyLogger
from multilspy.language_server import LanguageServer
from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo
from multilspy.lsp_protocol_handler.lsp_types import InitializeParams
from multilspy.multilspy_config import MultilspyConfig
from multilspy.multilspy_utils import FileUtils
from multilspy.multilspy_utils import PlatformUtils
class ClangdLanguageServer(LanguageServer):
"""
Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++.
As the project gets bigger in size, building index will take time. Try running clangd multiple times to ensure index is built properly.
Also make sure compile_commands.json is created at root of the source directory. Check clangd test case for example.
"""
def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str):
"""
Creates a ClangdLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
clangd_executable_path = self.setup_runtime_dependencies(logger, config)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path),
"cpp",
)
self.server_ready = asyncio.Event()
def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str:
"""
Setup runtime dependencies for ClangdLanguageServer.
"""
platform_id = PlatformUtils.get_platform_id()
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f:
d = json.load(f)
del d["_description"]
assert platform_id.value in [
"linux-x64"
], "Only linux-x64 is supported for in multilspy at the moment"
runtime_dependencies = d["runtimeDependencies"]
runtime_dependencies = [
dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value
]
assert len(runtime_dependencies) == 1
dependency = runtime_dependencies[0]
clangd_ls_dir = os.path.join(os.path.dirname(__file__), "static/clangd")
clangd_executable_path = os.path.join(clangd_ls_dir, "clangd_19.1.2", "bin", dependency["binaryName"])
if not os.path.exists(clangd_ls_dir):
os.makedirs(clangd_ls_dir)
if dependency["archiveType"] == "zip":
FileUtils.download_and_extract_archive(
logger, dependency["url"], clangd_ls_dir, dependency["archiveType"]
)
assert os.path.exists(clangd_executable_path)
os.chmod(clangd_executable_path, stat.S_IEXEC)
return clangd_executable_path
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the clangd Language Server.
"""
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r") as f:
d = json.load(f)
del d["_description"]
d["processId"] = os.getpid()
assert d["rootPath"] == "$rootPath"
d["rootPath"] = repository_absolute_path
assert d["rootUri"] == "$rootUri"
d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri()
assert d["workspaceFolders"][0]["uri"] == "$uri"
d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri()
assert d["workspaceFolders"][0]["name"] == "$name"
d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path)
return d
@asynccontextmanager
async def start_server(self) -> AsyncIterator["ClangdLanguageServer"]:
"""
Starts the Clangd Language Server, waits for the server to be ready and yields the LanguageServer instance.
Usage:
```
async with lsp.start_server():
# LanguageServer has been initialized and ready to serve requests
await lsp.request_definition(...)
await lsp.request_references(...)
# Shutdown the LanguageServer on exit from scope
# LanguageServer has been shutdown
"""
async def register_capability_handler(params):
assert "registrations" in params
for registration in params["registrations"]:
if registration["method"] == "workspace/executeCommand":
self.initialize_searcher_command_available.set()
self.resolve_main_method_available.set()
return
async def lang_status_handler(params):
# TODO: Should we wait for
# server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}}
# Before proceeding?
if params["type"] == "ServiceReady" and params["message"] == "ServiceReady":
self.service_ready_event.set()
async def execute_client_command_handler(params):
return []
async def do_nothing(params):
return
async def check_experimental_status(params):
if params["quiescent"] == True:
self.server_ready.set()
async def window_log_message(msg):
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("language/status", lang_status_handler)
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)
async with super().start_server():
self.logger.log("Starting Clangd server process", logging.INFO)
await self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
init_response = await self.server.send.initialize(initialize_params)
assert init_response["capabilities"]["textDocumentSync"]["change"] == 2
assert "completionProvider" in init_response["capabilities"]
assert init_response["capabilities"]["completionProvider"] == {
"triggerCharacters": ['.', '<', '>', ':', '"', '/', '*'],
"resolveProvider": False,
}
self.server.notify.initialized({})
self.completions_available.set()
# set ready flag
self.server_ready.set()
await self.server_ready.wait()
yield self
await self.server.shutdown()
await self.server.stop()
@@ -0,0 +1,36 @@
{
"_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize",
"processId": "os.getpid()",
"locale": "en",
"rootPath": "$rootPath",
"rootUri": "$rootUri",
"capabilities": {
"textDocument": {
"synchronization": {
"didSave": true,
"dynamicRegistration": true
},
"completion": {
"dynamicRegistration": true,
"completionItem": {
"snippetSupport": true
}
},
"definition": {
"dynamicRegistration": true
}
},
"workspace": {
"workspaceFolders": true,
"didChangeConfiguration": {
"dynamicRegistration": true
}
}
},
"workspaceFolders": [
{
"uri": "$uri",
"name": "$name"
}
]
}
@@ -0,0 +1,13 @@
{
"_description": "Used to download the runtime dependencies for running Clangd.",
"runtimeDependencies": [
{
"id": "Clangd",
"description": "Clangd for Linux (x64)",
"url": "https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip",
"platformId": "linux-x64",
"archiveType": "zip",
"binaryName": "clangd"
}
]
}
@@ -0,0 +1,146 @@
from contextlib import asynccontextmanager
import logging
import os
import pathlib
import shutil
import stat
from typing import AsyncIterator
from multilspy.language_server import LanguageServer
from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo
import json
from multilspy.multilspy_utils import FileUtils, PlatformUtils
class DartLanguageServer(LanguageServer):
"""
Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart.
"""
def __init__(self, config, logger, repository_root_path):
"""
Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
executable_path = self.setup_runtime_dependencies(logger)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path),
"dart",
)
def setup_runtime_dependencies(self, logger: "MultilspyLogger") -> str:
platform_id = PlatformUtils.get_platform_id()
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f:
d = json.load(f)
del d["_description"]
runtime_dependencies = d["runtimeDependencies"]
runtime_dependencies = [
dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value
]
assert len(runtime_dependencies) == 1
dependency = runtime_dependencies[0]
dart_ls_dir = os.path.join(os.path.dirname(__file__), "static", "dart-language-server")
dart_executable_path = os.path.join(dart_ls_dir, dependency["binaryName"])
if not os.path.exists(dart_ls_dir):
os.makedirs(dart_ls_dir)
FileUtils.download_and_extract_archive(
logger, dependency["url"], dart_ls_dir, dependency["archiveType"]
)
assert os.path.exists(dart_executable_path)
os.chmod(dart_executable_path, stat.S_IEXEC)
return f"{dart_executable_path} language-server --client-id multilspy.dart --client-version 1.2"
def _get_initialize_params(self, repository_absolute_path: str):
"""
Returns the initialize params for the Dart Language Server.
"""
with open(
os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r"
) as f:
d = json.load(f)
del d["_description"]
d["processId"] = os.getpid()
assert d["rootPath"] == "$rootPath"
d["rootPath"] = repository_absolute_path
assert d["rootUri"] == "$rootUri"
d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri()
assert d["workspaceFolders"][0]["uri"] == "$uri"
d["workspaceFolders"][0]["uri"] = pathlib.Path(
repository_absolute_path
).as_uri()
assert d["workspaceFolders"][0]["name"] == "$name"
d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path)
return d
@asynccontextmanager
async def start_server(self) -> AsyncIterator["DartLanguageServer"]:
"""
Start the language server and yield when the server is ready.
"""
async def execute_client_command_handler(params):
return []
async def do_nothing(params):
return
async def check_experimental_status(params):
pass
async def window_log_message(msg):
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
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
)
async with super().start_server():
self.logger.log(
"Starting dart-language-server server process", logging.INFO
)
await self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request to dart-language-server",
logging.DEBUG,
)
init_response = await self.server.send_request(
"initialize", initialize_params
)
self.logger.log(
f"Received initialize response from dart-language-server: {init_response}",
logging.INFO,
)
self.server.notify.initialized({})
yield self
await self.server.shutdown()
await self.server.stop()
@@ -0,0 +1,23 @@
{
"_description": "This file contains the initialization parameters for the Dart Language Server.",
"processId": "$processId",
"rootPath": "$rootPath",
"rootUri": "$rootUri",
"capabilities": {},
"initializationOptions": {
"onlyAnalyzeProjectsWithOpenFiles": false,
"suggestFromUnimportedLibraries": true,
"closingLabels": false,
"outline": false,
"flutterOutline": false,
"allowOpenUri": false
},
"trace": "verbose",
"workspaceFolders": [
{
"uri": "$uri",
"name": "$name"
}
]
}
@@ -0,0 +1,13 @@
{
"_description": "Used to download the runtime dependencies for running Dart Language Server, downloaded from https://dart.dev/get-dart/archive",
"runtimeDependencies": [
{
"id": "DartLanguageServer",
"description": "Dart Language Server for Linux (x64)",
"url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-x64-release.zip",
"platformId": "linux-x64",
"archiveType": "zip",
"binaryName": "dart-sdk/bin/dart"
}
]
}
@@ -23,6 +23,16 @@
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar",
"jdtls_readonly_config_path": "extension/server/config_mac_arm"
},
"osx-x64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@darwin-x64-1.23.0.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"jre_home_path": "extension/jre/17.0.8.1-macosx-x86_64",
"jre_path": "extension/jre/17.0.8.1-macosx-x86_64/bin/java",
"lombok_jar_path": "extension/lombok/lombok-1.18.30.jar",
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar",
"jdtls_readonly_config_path": "extension/server/config_mac"
},
"linux-arm64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@linux-arm64-1.23.0.vsix",
"archiveType": "zip",
@@ -59,4 +69,4 @@
"intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members"
}
}
}
}
@@ -0,0 +1,521 @@
{
"_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize",
"processId": "os.getpid()",
"clientInfo": {
"name": "Multilspy Kotlin Client",
"version": "1.0.0"
},
"locale": "en",
"rootPath": "repository_absolute_path",
"rootUri": "pathlib.Path(repository_absolute_path).as_uri()",
"capabilities": {
"workspace": {
"applyEdit": true,
"workspaceEdit": {
"documentChanges": true,
"resourceOperations": [
"create",
"rename",
"delete"
],
"failureHandling": "textOnlyTransactional",
"normalizesLineEndings": true,
"changeAnnotationSupport": {
"groupsOnLabel": true
}
},
"didChangeConfiguration": {
"dynamicRegistration": true
},
"didChangeWatchedFiles": {
"dynamicRegistration": true,
"relativePatternSupport": true
},
"symbol": {
"dynamicRegistration": true,
"symbolKind": {
"valueSet": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26
]
},
"tagSupport": {
"valueSet": [
1
]
},
"resolveSupport": {
"properties": [
"location.range"
]
}
},
"codeLens": {
"refreshSupport": true
},
"executeCommand": {
"dynamicRegistration": true
},
"configuration": true,
"workspaceFolders": true,
"semanticTokens": {
"refreshSupport": true
},
"fileOperations": {
"dynamicRegistration": true,
"didCreate": true,
"didRename": true,
"didDelete": true,
"willCreate": true,
"willRename": true,
"willDelete": true
},
"inlineValue": {
"refreshSupport": true
},
"inlayHint": {
"refreshSupport": true
},
"diagnostics": {
"refreshSupport": true
}
},
"textDocument": {
"publishDiagnostics": {
"relatedInformation": true,
"versionSupport": false,
"tagSupport": {
"valueSet": [
1,
2
]
},
"codeDescriptionSupport": true,
"dataSupport": true
},
"synchronization": {
"dynamicRegistration": true,
"willSave": true,
"willSaveWaitUntil": true,
"didSave": true
},
"completion": {
"dynamicRegistration": true,
"contextSupport": true,
"completionItem": {
"snippetSupport": false,
"commitCharactersSupport": true,
"documentationFormat": [
"markdown",
"plaintext"
],
"deprecatedSupport": true,
"preselectSupport": true,
"tagSupport": {
"valueSet": [
1
]
},
"insertReplaceSupport": false,
"resolveSupport": {
"properties": [
"documentation",
"detail",
"additionalTextEdits"
]
},
"insertTextModeSupport": {
"valueSet": [
1,
2
]
},
"labelDetailsSupport": true
},
"insertTextMode": 2,
"completionItemKind": {
"valueSet": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25
]
},
"completionList": {
"itemDefaults": [
"commitCharacters",
"editRange",
"insertTextFormat",
"insertTextMode"
]
}
},
"hover": {
"dynamicRegistration": true,
"contentFormat": [
"markdown",
"plaintext"
]
},
"signatureHelp": {
"dynamicRegistration": true,
"signatureInformation": {
"documentationFormat": [
"markdown",
"plaintext"
],
"parameterInformation": {
"labelOffsetSupport": true
},
"activeParameterSupport": true
},
"contextSupport": true
},
"definition": {
"dynamicRegistration": true,
"linkSupport": true
},
"references": {
"dynamicRegistration": true
},
"documentHighlight": {
"dynamicRegistration": true
},
"documentSymbol": {
"dynamicRegistration": true,
"symbolKind": {
"valueSet": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26
]
},
"hierarchicalDocumentSymbolSupport": true,
"tagSupport": {
"valueSet": [
1
]
},
"labelSupport": true
},
"codeAction": {
"dynamicRegistration": true,
"isPreferredSupport": true,
"disabledSupport": true,
"dataSupport": true,
"resolveSupport": {
"properties": [
"edit"
]
},
"codeActionLiteralSupport": {
"codeActionKind": {
"valueSet": [
"",
"quickfix",
"refactor",
"refactor.extract",
"refactor.inline",
"refactor.rewrite",
"source",
"source.organizeImports"
]
}
},
"honorsChangeAnnotations": false
},
"codeLens": {
"dynamicRegistration": true
},
"formatting": {
"dynamicRegistration": true
},
"rangeFormatting": {
"dynamicRegistration": true
},
"onTypeFormatting": {
"dynamicRegistration": true
},
"rename": {
"dynamicRegistration": true,
"prepareSupport": true,
"prepareSupportDefaultBehavior": 1,
"honorsChangeAnnotations": true
},
"documentLink": {
"dynamicRegistration": true,
"tooltipSupport": true
},
"typeDefinition": {
"dynamicRegistration": true,
"linkSupport": true
},
"implementation": {
"dynamicRegistration": true,
"linkSupport": true
},
"colorProvider": {
"dynamicRegistration": true
},
"foldingRange": {
"dynamicRegistration": true,
"rangeLimit": 5000,
"lineFoldingOnly": true,
"foldingRangeKind": {
"valueSet": [
"comment",
"imports",
"region"
]
},
"foldingRange": {
"collapsedText": false
}
},
"declaration": {
"dynamicRegistration": true,
"linkSupport": true
},
"selectionRange": {
"dynamicRegistration": true
},
"callHierarchy": {
"dynamicRegistration": true
},
"semanticTokens": {
"dynamicRegistration": true,
"tokenTypes": [
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"function",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"regexp",
"operator",
"decorator"
],
"tokenModifiers": [
"declaration",
"definition",
"readonly",
"static",
"deprecated",
"abstract",
"async",
"modification",
"documentation",
"defaultLibrary"
],
"formats": [
"relative"
],
"requests": {
"range": true,
"full": {
"delta": true
}
},
"multilineTokenSupport": false,
"overlappingTokenSupport": false,
"serverCancelSupport": true,
"augmentsSyntaxTokens": true
},
"linkedEditingRange": {
"dynamicRegistration": true
},
"typeHierarchy": {
"dynamicRegistration": true
},
"inlineValue": {
"dynamicRegistration": true
},
"inlayHint": {
"dynamicRegistration": true,
"resolveSupport": {
"properties": [
"tooltip",
"textEdits",
"label.tooltip",
"label.location",
"label.command"
]
}
},
"diagnostic": {
"dynamicRegistration": true,
"relatedDocumentSupport": false
}
},
"window": {
"showMessage": {
"messageActionItem": {
"additionalPropertiesSupport": true
}
},
"showDocument": {
"support": true
},
"workDoneProgress": true
},
"general": {
"staleRequestSupport": {
"cancel": true,
"retryOnContentModified": [
"textDocument/semanticTokens/full",
"textDocument/semanticTokens/range",
"textDocument/semanticTokens/full/delta"
]
},
"regularExpressions": {
"engine": "ECMAScript",
"version": "ES2020"
},
"markdown": {
"parser": "marked",
"version": "1.1.0"
},
"positionEncodings": [
"utf-16"
]
},
"notebookDocument": {
"synchronization": {
"dynamicRegistration": true,
"executionSummarySupport": true
}
}
},
"initializationOptions": {
"workspaceFolders": "[pathlib.Path(repository_absolute_path).as_uri()]",
"storagePath": null,
"codegen": {
"enabled": false
},
"compiler": {
"jvm": {
"target": "default"
}
},
"completion": {
"snippets": {
"enabled": true
}
},
"diagnostics": {
"enabled": true,
"level": 4,
"debounceTime": 250
},
"scripts": {
"enabled": true,
"buildScriptsEnabled": true
},
"indexing": {
"enabled": true
},
"externalSources": {
"useKlsScheme": false,
"autoConvertToKotlin": false
},
"inlayHints": {
"typeHints": false,
"parameterHints": false,
"chainedHints": false
},
"formatting": {
"formatter": "ktfmt",
"ktfmt": {
"style": "google",
"indent": 4,
"maxWidth": 100,
"continuationIndent": 8,
"removeUnusedImports": true
}
}
},
"trace": "verbose",
"workspaceFolders": "[\n {\n \"uri\": pathlib.Path(repository_absolute_path).as_uri(),\n \"name\": os.path.basename(repository_absolute_path),\n }\n ]"
}
@@ -0,0 +1,234 @@
"""
Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin.
"""
import asyncio
import dataclasses
import json
import logging
import os
import stat
import pathlib
from contextlib import asynccontextmanager
from typing import AsyncIterator
from multilspy.multilspy_logger import MultilspyLogger
from multilspy.language_server import LanguageServer
from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo
from multilspy.lsp_protocol_handler.lsp_types import InitializeParams
from multilspy.multilspy_config import MultilspyConfig
from multilspy.multilspy_utils import FileUtils
from multilspy.multilspy_utils import PlatformUtils
@dataclasses.dataclass
class KotlinRuntimeDependencyPaths:
"""
Stores the paths to the runtime dependencies of Kotlin Language Server
"""
java_path: str
java_home_path: str
kotlin_executable_path: str
class KotlinLanguageServer(LanguageServer):
"""
Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin.
"""
def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str):
"""
Creates a Kotlin Language Server instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
runtime_dependency_paths = self.setup_runtime_dependencies(logger, config)
self.runtime_dependency_paths = runtime_dependency_paths
# Create command to execute the Kotlin Language Server script
cmd = f'"{self.runtime_dependency_paths.kotlin_executable_path}"'
# Set environment variables including JAVA_HOME
proc_env = {"JAVA_HOME": self.runtime_dependency_paths.java_home_path}
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=cmd, env=proc_env, cwd=repository_root_path),
"kotlin",
)
def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> KotlinRuntimeDependencyPaths:
"""
Setup runtime dependencies for Kotlin Language Server.
"""
platform_id = PlatformUtils.get_platform_id()
# Verify platform support
assert platform_id.value.startswith("win-") or platform_id.value.startswith("linux-") or platform_id.value.startswith("osx-"), "Only Windows, Linux and macOS platforms are supported for Kotlin in multilspy at the moment"
# Load dependency information
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f:
d = json.load(f)
del d["_description"]
kotlin_dependency = d["runtimeDependency"]
java_dependency = d["java"][platform_id.value]
# Setup paths for dependencies
static_dir = os.path.join(os.path.dirname(__file__), "static")
os.makedirs(static_dir, exist_ok=True)
# Setup Java paths
java_dir = os.path.join(static_dir, "java")
os.makedirs(java_dir, exist_ok=True)
java_home_path = os.path.join(java_dir, java_dependency["java_home_path"])
java_path = os.path.join(java_dir, java_dependency["java_path"])
# Download and extract Java if not exists
if not os.path.exists(java_path):
logger.log(f"Downloading Java for {platform_id.value}...", logging.INFO)
FileUtils.download_and_extract_archive(
logger, java_dependency["url"], java_dir, java_dependency["archiveType"]
)
# Make Java executable
if not platform_id.value.startswith("win-"):
os.chmod(java_path, 0o755)
assert os.path.exists(java_path), f"Java executable not found at {java_path}"
# Setup Kotlin Language Server paths
kotlin_ls_dir = os.path.join(static_dir, "server")
# Get platform-specific executable script path
if platform_id.value.startswith("win-"):
kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server.bat")
else:
kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server")
# Download and extract Kotlin Language Server if script doesn't exist
if not os.path.exists(kotlin_script):
logger.log("Downloading Kotlin Language Server...", logging.INFO)
FileUtils.download_and_extract_archive(
logger, kotlin_dependency["url"], static_dir, kotlin_dependency["archiveType"]
)
# Make script executable on Unix platforms
if os.path.exists(kotlin_script) and not platform_id.value.startswith("win-"):
os.chmod(kotlin_script, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
# Use script file
if os.path.exists(kotlin_script):
kotlin_executable_path = kotlin_script
logger.log(f"Using Kotlin Language Server script at {kotlin_script}", logging.INFO)
else:
raise FileNotFoundError(f"Kotlin Language Server script not found at {kotlin_script}")
return KotlinRuntimeDependencyPaths(
java_path=java_path,
java_home_path=java_home_path,
kotlin_executable_path=kotlin_executable_path
)
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Kotlin Language Server.
"""
with open(str(pathlib.PurePath(os.path.dirname(__file__), "initialize_params.json")), "r") as f:
d: InitializeParams = json.load(f)
del d["_description"]
if not os.path.isabs(repository_absolute_path):
repository_absolute_path = os.path.abspath(repository_absolute_path)
assert d["processId"] == "os.getpid()"
d["processId"] = os.getpid()
assert d["rootPath"] == "repository_absolute_path"
d["rootPath"] = repository_absolute_path
assert d["rootUri"] == "pathlib.Path(repository_absolute_path).as_uri()"
d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri()
assert d["initializationOptions"]["workspaceFolders"] == "[pathlib.Path(repository_absolute_path).as_uri()]"
d["initializationOptions"]["workspaceFolders"] = [pathlib.Path(repository_absolute_path).as_uri()]
assert (
d["workspaceFolders"]
== '[\n {\n "uri": pathlib.Path(repository_absolute_path).as_uri(),\n "name": os.path.basename(repository_absolute_path),\n }\n ]'
)
d["workspaceFolders"] = [
{
"uri": pathlib.Path(repository_absolute_path).as_uri(),
"name": os.path.basename(repository_absolute_path),
}
]
return d
@asynccontextmanager
async def start_server(self) -> AsyncIterator["KotlinLanguageServer"]:
"""
Starts the Kotlin Language Server, waits for the server to be ready and yields the LanguageServer instance.
Usage:
```
async with lsp.start_server():
# LanguageServer has been initialized and ready to serve requests
await lsp.request_definition(...)
await lsp.request_references(...)
# Shutdown the LanguageServer on exit from scope
# LanguageServer has been shutdown
```
"""
async def execute_client_command_handler(params):
return []
async def do_nothing(params):
return
async def window_log_message(msg):
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
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)
async with super().start_server():
self.logger.log("Starting Kotlin server process", logging.INFO)
await self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
init_response = await self.server.send.initialize(initialize_params)
capabilities = init_response["capabilities"]
assert "textDocumentSync" in capabilities, "Server must support textDocumentSync"
assert "hoverProvider" in capabilities, "Server must support hover"
assert "completionProvider" in capabilities, "Server must support code completion"
assert "signatureHelpProvider" in capabilities, "Server must support signature help"
assert "definitionProvider" in capabilities, "Server must support go to definition"
assert "referencesProvider" in capabilities, "Server must support find references"
assert "documentSymbolProvider" in capabilities, "Server must support document symbols"
assert "workspaceSymbolProvider" in capabilities, "Server must support workspace symbols"
assert "semanticTokensProvider" in capabilities, "Server must support semantic tokens"
self.server.notify.initialized({})
self.completions_available.set()
yield self
try:
await self.server.shutdown()
except Exception as e:
self.logger.log(f"Error during Kotlin server shutdown: {str(e)}", logging.WARNING)
finally:
await self.server.stop()
@@ -0,0 +1,41 @@
{
"_description": "Used to download the runtime dependencies for Kotlin Language Server from https://github.com/fwcd/kotlin-language-server",
"runtimeDependency": {
"id": "KotlinLsp",
"description": "Kotlin Language Server",
"url": "https://github.com/fwcd/kotlin-language-server/releases/download/1.3.13/server.zip",
"archiveType": "zip"
},
"java": {
"win-x64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@win32-x64-1.23.0.vsix",
"archiveType": "zip",
"java_home_path": "extension/jre/17.0.8.1-win32-x86_64",
"java_path": "extension/jre/17.0.8.1-win32-x86_64/bin/java.exe"
},
"linux-x64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@linux-x64-1.23.0.vsix",
"archiveType": "zip",
"java_home_path": "extension/jre/17.0.8.1-linux-x86_64",
"java_path": "extension/jre/17.0.8.1-linux-x86_64/bin/java"
},
"linux-arm64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@linux-arm64-1.23.0.vsix",
"archiveType": "zip",
"java_home_path": "extension/jre/17.0.8.1-linux-aarch64",
"java_path": "extension/jre/17.0.8.1-linux-aarch64/bin/java"
},
"osx-x64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@darwin-x64-1.23.0.vsix",
"archiveType": "zip",
"java_home_path": "extension/jre/17.0.8.1-macosx-x86_64",
"java_path": "extension/jre/17.0.8.1-macosx-x86_64/bin/java"
},
"osx-arm64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@darwin-arm64-1.23.0.vsix",
"archiveType": "zip",
"java_home_path": "extension/jre/17.0.8.1-macosx-aarch64",
"java_path": "extension/jre/17.0.8.1-macosx-aarch64/bin/java"
}
}
}
@@ -32,6 +32,11 @@ else:
return type('obj', (), {'pw_name': os.environ.get('USERNAME', 'unknown')})()
# Conditionally import pwd module (Unix-only)
if not PlatformUtils.get_platform_id().value.startswith("win"):
import pwd
class TypeScriptLanguageServer(LanguageServer):
"""
Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript.
@@ -50,7 +55,7 @@ class TypeScriptLanguageServer(LanguageServer):
"typescript",
)
self.server_ready = asyncio.Event()
@override
def should_always_ignore(self, dirname: str) -> bool:
return super().should_always_ignore(dirname) or dirname in [
@@ -95,22 +100,29 @@ class TypeScriptLanguageServer(LanguageServer):
if not os.path.exists(tsserver_ls_dir):
os.makedirs(tsserver_ls_dir, exist_ok=True)
for dependency in runtime_dependencies:
# Handle platform-specific user settings
subprocess_kwargs = {
'shell': True,
'check': True,
'cwd': tsserver_ls_dir,
'stdout': subprocess.DEVNULL,
'stderr': subprocess.DEVNULL
}
# Only add user parameter on Unix-like systems
if os.name != 'nt': # Not Windows
# Windows doesn't support the 'user' parameter and doesn't have pwd module
if PlatformUtils.get_platform_id().value.startswith("win"):
subprocess.run(
dependency["command"],
shell=True,
check=True,
cwd=tsserver_ls_dir,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
else:
# On Unix-like systems, run as non-root user
user = pwd.getpwuid(os.getuid()).pw_name
subprocess_kwargs['user'] = user
subprocess.run(dependency["command"], **subprocess_kwargs)
subprocess.run(
dependency["command"],
shell=True,
check=True,
user=user,
cwd=tsserver_ls_dir,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server")
assert os.path.exists(tsserver_executable_path), "typescript-language-server executable not found. Please install typescript-language-server and try again."
@@ -209,4 +221,4 @@ class TypeScriptLanguageServer(LanguageServer):
yield self
await self.server.shutdown()
await self.server.stop()
await self.server.stop()
@@ -57,3 +57,6 @@ class LSPConstants:
# key used to represent children in document symbols
CHILDREN = "children"
# key used to represent the location in symbols
LOCATION = "location"
+115 -16
View File
@@ -33,6 +33,7 @@ import dataclasses
import json
import logging
import os
import psutil
from typing import Any, Dict, List, Optional, Union
from .lsp_requests import LspNotification, LspRequest
@@ -173,9 +174,18 @@ class LanguageServerHandler:
the asynchronous tasks created by the handler.
task_counter: An integer that represents the next available task id for the handler.
loop: An asyncio.AbstractEventLoop object that represents the event loop used by the handler.
start_independent_lsp_process: An optional boolean flag that indicates whether to start the
language server process in an independent process group. Default is `True`. Setting it to
`False` means that the language server process will be in the same process group as the
the current process, and any SIGINT and SIGTERM signals will be sent to both processes.
"""
def __init__(self, process_launch_info: ProcessLaunchInfo, logger=None) -> None:
def __init__(
self,
process_launch_info: ProcessLaunchInfo,
logger=None,
start_independent_lsp_process=True,
) -> None:
"""
Params:
cmd: A string that represents the command to launch the language server process.
@@ -197,6 +207,7 @@ class LanguageServerHandler:
self.tasks = {}
self.task_counter = 0
self.loop = None
self.start_independent_lsp_process = start_independent_lsp_process
async def start(self) -> None:
"""
@@ -214,6 +225,7 @@ class LanguageServerHandler:
stderr=asyncio.subprocess.PIPE,
env=child_proc_env,
cwd=self.process_launch_info.cwd,
start_new_session=self.start_independent_lsp_process,
)
# Check if process terminated immediately
@@ -234,24 +246,111 @@ class LanguageServerHandler:
"""
Sends the terminate signal to the language server process and waits for it to exit, with a timeout, killing it if necessary
"""
for task in self.tasks.values():
task.cancel()
self.tasks = {}
# First cancel all tasks
await self._cancel_pending_tasks()
process = self.process
self.process = None
if not process:
return
# Clean up the process
await self._cleanup_process(process)
if process:
# TODO: Ideally, we should terminate the process here,
# However, there's an issue with asyncio terminating processes documented at
# https://bugs.python.org/issue35539 and https://bugs.python.org/issue41320
# process.terminate()
wait_for_end = process.wait()
async def _cancel_pending_tasks(self):
"""Cancel all pending tasks and wait for them to complete or timeout."""
pending_tasks = []
for task in self.tasks.values():
if not task.done():
task.cancel()
pending_tasks.append(task)
if pending_tasks:
try:
await asyncio.wait_for(wait_for_end, timeout=60)
except asyncio.TimeoutError:
process.kill()
await asyncio.wait_for(asyncio.gather(*pending_tasks, return_exceptions=True), timeout=5.0)
except (asyncio.TimeoutError, Exception):
pass
self.tasks = {}
async def _cleanup_process(self, process):
"""Clean up a process: close stdin, terminate/kill process, close stdout/stderr."""
# Close stdin first to prevent deadlocks
# See: https://bugs.python.org/issue35539
self._safely_close_pipe(process.stdin)
# Terminate/kill the process if it's still running
if process.returncode is None:
await self._terminate_or_kill_process(process)
# Close stdout and stderr pipes after process has exited
# This is essential to prevent "I/O operation on closed pipe" errors and
# "Event loop is closed" errors during garbage collection
# See: https://bugs.python.org/issue41320 and https://github.com/python/cpython/issues/88050
self._safely_close_pipe(process.stdout)
self._safely_close_pipe(process.stderr)
# Small delay to ensure OS has released file handles
await asyncio.sleep(0.5)
def _safely_close_pipe(self, pipe):
"""Safely close a pipe, ignoring any exceptions."""
if pipe:
try:
pipe.close()
except Exception:
pass
async def _terminate_or_kill_process(self, process):
"""Try to terminate the process gracefully, then forcefully if necessary."""
# First try to terminate the process tree gracefully
self._signal_process_tree(process, terminate=True)
# Wait for the process to exit (with timeout)
try:
await asyncio.wait_for(process.wait(), timeout=10)
except (asyncio.TimeoutError, Exception):
# If termination failed, forcefully kill the process tree
self._signal_process_tree(process, terminate=False)
try:
# Give it one more chance to exit
await asyncio.wait_for(process.wait(), timeout=2)
except Exception:
pass
def _signal_process_tree(self, process, terminate=True):
"""Send signal (terminate or kill) to the process and all its children."""
signal_method = "terminate" if terminate else "kill"
# Try to get the parent process
parent = None
try:
parent = psutil.Process(process.pid)
except (psutil.NoSuchProcess, psutil.AccessDenied, Exception):
pass
# If we have the parent process and it's running, signal the entire tree
if parent and parent.is_running():
# Signal children first
for child in parent.children(recursive=True):
try:
getattr(child, signal_method)()
except (psutil.NoSuchProcess, psutil.AccessDenied, Exception):
pass
# Then signal the parent
try:
getattr(parent, signal_method)()
except (psutil.NoSuchProcess, psutil.AccessDenied, Exception):
pass
else:
# Fall back to direct process signaling
try:
getattr(process, signal_method)()
except Exception:
pass
async def shutdown(self) -> None:
"""
@@ -310,7 +409,7 @@ class LanguageServerHandler:
line = await self.process.stderr.readline()
if not line:
continue
self._log("LSP stderr: " + line.decode(ENCODING))
self._log("LSP stderr: " + line.decode(ENCODING, errors='replace'))
except (BrokenPipeError, ConnectionResetError, StopLoopException):
pass
+4
View File
@@ -30,10 +30,13 @@ class Language(str, Enum):
PYTHON = "python"
RUST = "rust"
JAVA = "java"
KOTLIN = "kotlin"
TYPESCRIPT = "typescript"
JAVASCRIPT = "javascript"
GO = "go"
RUBY = "ruby"
DART = "dart"
CPP = "cpp"
def __str__(self) -> str:
return self.value
@@ -67,6 +70,7 @@ class MultilspyConfig:
"""
code_language: Language
trace_lsp_communication: bool = False
start_independent_lsp_process: bool = True
ignored_paths: list[str] = field(default_factory=list)
"""Paths, dirs or glob-like patterns. The matching will follow the same logic as for .gitignore entries"""
gitignore_file_content: str | None = None
+1 -1
View File
@@ -82,7 +82,7 @@ class Location(TypedDict):
uri: DocumentUri
range: Range
absolutePath: str
relativePath: str
relativePath: Union[str, None]
class CompletionItemKind(IntEnum):
"""The kind of a completion entry."""
+11 -1
View File
@@ -5,7 +5,7 @@ This file contains various utility functions like I/O operations, handling paths
import gzip
import logging
import os
from typing import Tuple
from typing import Tuple, Union
import requests
import shutil
import uuid
@@ -94,6 +94,16 @@ class PathUtils:
"""Check if a pattern contains glob-specific characters."""
return any(c in pattern for c in '*?[]!')
@staticmethod
def get_relative_path(path: str, base_path: str) -> Union[str, None]:
"""
Gets relative path if it's possible (paths should be on the same drive),
returns `None` otherwise.
"""
if PurePath(path).drive == PurePath(base_path).drive:
return str(PurePath(os.path.relpath(path, base_path)))
return None
class FileUtils:
"""
Utility functions for file operations.