Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/publish-to-pypi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jobs:
test:
name: Test the distribution
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test-workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:
test:
name: Test (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
matrix:
python-version: ["3.10", "3.12", "3.14"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path),
ProcessLaunchInfo(cmd=[clangd_executable_path], cwd=repository_root_path),
"cpp",
)
self.server_ready = asyncio.Event()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(self, config, logger, repository_root_path):
def setup_runtime_dependencies(self, logger: "MultilspyLogger", config: MultilspyConfig) -> str:
if config.server_binary:
assert os.path.exists(config.server_binary), f"Server binary not found: {config.server_binary}"
return f"{config.server_binary} language-server --client-id multilspy.dart --client-version 1.2"
return [config.server_binary, "language-server", "--client-id", "multilspy.dart", "--client-version", "1.2"]

platform_id = PlatformUtils.get_platform_id()

Expand Down Expand Up @@ -64,7 +64,7 @@ def setup_runtime_dependencies(self, logger: "MultilspyLogger", config: Multilsp
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"
return [dart_executable_path, "language-server", "--client-id", "multilspy.dart", "--client-version", "1.2"]


def _get_initialize_params(self, repository_absolute_path: str):
Expand Down
68 changes: 36 additions & 32 deletions src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,7 @@ def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_
# TODO: Add "self.runtime_dependency_paths.jre_home_path"/bin to $PATH as well
proc_env = {"syntaxserver": "false", "JAVA_HOME": self.runtime_dependency_paths.jre_home_path}
proc_cwd = repository_root_path
cmd = " ".join(
[
cmd = [
jre_path,
"--add-modules=ALL-SYSTEM",
"--add-opens",
Expand Down Expand Up @@ -131,7 +130,6 @@ def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_
"-data",
data_dir,
]
)

self.service_ready_event = asyncio.Event()
self.intellicode_enable_command_available = asyncio.Event()
Expand Down Expand Up @@ -318,20 +316,11 @@ async def start_server(self) -> AsyncIterator["EclipseJDTLS"]:
"""

async def register_capability_handler(params):
assert "registrations" in params
for registration in params["registrations"]:
for registration in params.get("registrations", []):
if registration["method"] == "textDocument/completion":
assert registration["registerOptions"]["resolveProvider"] == True
assert registration["registerOptions"]["triggerCharacters"] == [
".",
"@",
"#",
"*",
" ",
]
self.completions_available.set()
if registration["method"] == "workspace/executeCommand":
if "java.intellicode.enable" in registration["registerOptions"]["commands"]:
if "java.intellicode.enable" in registration.get("registerOptions", {}).get("commands", []):
self.intellicode_enable_command_available.set()
return

Expand All @@ -343,8 +332,6 @@ async def lang_status_handler(params):
self.service_ready_event.set()

async def execute_client_command_handler(params):
assert params["command"] == "_java.reloadBundles.command"
assert params["arguments"] == []
return []

async def window_log_message(msg):
Expand Down Expand Up @@ -372,29 +359,46 @@ async def do_nothing(params):
)
init_response = await self.server.send.initialize(initialize_params)
assert init_response["capabilities"]["textDocumentSync"]["change"] == 2
assert "completionProvider" not in init_response["capabilities"]
assert "executeCommandProvider" not in init_response["capabilities"]

# If completionProvider is already in init response (newer JDTLS),
# completions are statically registered and available immediately
if "completionProvider" in init_response["capabilities"]:
self.completions_available.set()

self.server.notify.initialized({})

self.server.notify.workspace_did_change_configuration(
{"settings": initialize_params["initializationOptions"]["settings"]}
)

await self.intellicode_enable_command_available.wait()

java_intellisense_members_path = self.runtime_dependency_paths.intellisense_members_path
assert os.path.exists(java_intellisense_members_path)
intellicode_enable_result = await self.server.send.execute_command(
{
"command": "java.intellicode.enable",
"arguments": [True, java_intellisense_members_path],
}
)
assert intellicode_enable_result

# TODO: Add comments about why we wait here, and how this can be optimized
await self.service_ready_event.wait()
# Wait for dynamic capability registration and enable IntelliCode if available.
# Newer JDTLS versions (>= 1.40) may not use dynamic registration for
# executeCommand, so IntelliCode enabling is best-effort.
try:
await asyncio.wait_for(self.intellicode_enable_command_available.wait(), timeout=30)
java_intellisense_members_path = self.runtime_dependency_paths.intellisense_members_path
if os.path.exists(java_intellisense_members_path):
await self.server.send.execute_command(
{
"command": "java.intellicode.enable",
"arguments": [True, java_intellisense_members_path],
}
)
except asyncio.TimeoutError:
self.logger.log(
"IntelliCode dynamic registration not received, proceeding without IntelliCode",
logging.WARNING,
)

# Wait for service ready, or proceed after timeout (completions may
# already be available via static registration in newer JDTLS versions)
try:
await asyncio.wait_for(self.service_ready_event.wait(), timeout=60)
except asyncio.TimeoutError:
self.logger.log(
"ServiceReady notification not received, proceeding anyway",
logging.WARNING,
)
try:
yield self
finally:
Expand Down
4 changes: 2 additions & 2 deletions src/multilspy/language_servers/gopls/gopls.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ def setup_runtime_dependency(cls):
def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str):
if config.server_binary:
assert os.path.exists(config.server_binary), f"Server binary not found: {config.server_binary}"
cmd = config.server_binary
cmd = [config.server_binary]
else:
# Check runtime dependencies before initializing
self.setup_runtime_dependency()
cmd = "gopls"
cmd = ["gopls"]

super().__init__(
config,
Expand Down
4 changes: 2 additions & 2 deletions src/multilspy/language_servers/intelephense/intelephense.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def __init__(self, config, logger, repository_root_path):
def setup_runtime_dependencies(self, logger, config: MultilspyConfig) -> str:
if config.server_binary:
assert os.path.exists(config.server_binary), f"Server binary not found: {config.server_binary}"
return f"{config.server_binary} --stdio"
return [config.server_binary, "--stdio"]

with open(
os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r"
Expand Down Expand Up @@ -97,7 +97,7 @@ def setup_runtime_dependencies(self, logger, config: MultilspyConfig) -> str:
| stat.S_IXOTH,
)

return f"{intelephense_executable_path} --stdio"
return [intelephense_executable_path, "--stdio"]

def _get_initialize_params(self, repository_absolute_path: str):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=config.server_binary or "jedi-language-server", cwd=repository_root_path),
ProcessLaunchInfo(cmd=[config.server_binary or "jedi-language-server"], cwd=repository_root_path),
"python",
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_
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}"'
cmd = [self.runtime_dependency_paths.kotlin_executable_path]

# Set environment variables including JAVA_HOME
proc_env = {"JAVA_HOME": self.runtime_dependency_paths.java_home_path}
Expand Down
12 changes: 5 additions & 7 deletions src/multilspy/language_servers/omnisharp/omnisharp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import logging
import os
import pathlib
import shlex

import stat
from contextlib import asynccontextmanager
from typing import AsyncIterator, Iterable
Expand Down Expand Up @@ -71,22 +71,21 @@ def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_
logger.log("No *.sln file found in repository", logging.ERROR)
raise MultilspyException("No SLN file found in repository")

cmd = " ".join(
[
shlex.quote(omnisharp_executable_path),
cmd = [
omnisharp_executable_path,
"-lsp",
"--encoding",
"ascii",
"-z",
"-s",
shlex.quote(slnfilename),
slnfilename,
"--hostPID",
str(os.getpid()),
"DotNet:enablePackageRestore=false",
"--loglevel",
"trace",
"--plugin",
shlex.quote(dll_path),
dll_path,
"FileOptions:SystemExcludeSearchPatterns:0=**/.git",
"FileOptions:SystemExcludeSearchPatterns:1=**/.svn",
"FileOptions:SystemExcludeSearchPatterns:2=**/.hg",
Expand All @@ -102,7 +101,6 @@ def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_
"formattingOptions:tabSize=4",
"formattingOptions:indentationSize=4",
]
)
super().__init__(
config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=rustanalyzer_executable_path, cwd=repository_root_path),
ProcessLaunchInfo(cmd=[rustanalyzer_executable_path], cwd=repository_root_path),
"rust",
)
self.server_ready = asyncio.Event()
Expand Down
2 changes: 1 addition & 1 deletion src/multilspy/language_servers/solargraph/solargraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=f"{solargraph_executable_path} stdio", cwd=repository_root_path),
ProcessLaunchInfo(cmd=[solargraph_executable_path, "stdio"], cwd=repository_root_path),
"ruby",
)
self.server_ready = asyncio.Event()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC
"""
if config.server_binary:
assert os.path.exists(config.server_binary), f"Server binary not found: {config.server_binary}"
return f"{config.server_binary} --stdio"
return [config.server_binary, "--stdio"]

platform_id = PlatformUtils.get_platform_id()

Expand Down Expand Up @@ -108,7 +108,7 @@ def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyC
)

assert os.path.exists(tsserver_executable_path), "typescript-language-server executable not found. Please install typescript-language-server and try again."
return f"{tsserver_executable_path} --stdio"
return [tsserver_executable_path, "--stdio"]

def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
"""
Expand Down
16 changes: 10 additions & 6 deletions src/multilspy/lsp_protocol_handler/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ class ProcessLaunchInfo:
This class is used to store the information required to launch a process.
"""

# The command to launch the process
cmd: str
# The command to launch the process as a list of arguments (no shell involved)
cmd: List[str]

# The environment variables to set for the process
env: Dict[str, str] = dataclasses.field(default_factory=dict)
Expand Down Expand Up @@ -214,8 +214,8 @@ async def start(self) -> None:
"""
child_proc_env = os.environ.copy()
child_proc_env.update(self.process_launch_info.env)
self.process = await asyncio.create_subprocess_shell(
self.process_launch_info.cmd,
self.process = await asyncio.create_subprocess_exec(
*self.process_launch_info.cmd,
stdout=asyncio.subprocess.PIPE,
stdin=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
Expand Down Expand Up @@ -294,7 +294,7 @@ 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)
Expand Down Expand Up @@ -350,7 +350,11 @@ async def shutdown(self) -> None:
"""
Perform the shutdown sequence for the client, including sending the shutdown request to the server and notifying it of exit
"""
await self.send.shutdown()
try:
await asyncio.wait_for(self.send.shutdown(), timeout=30)
except (asyncio.TimeoutError, Exception):
# Server didn't respond to shutdown request; proceed to exit/kill
pass
self._received_shutdown = True
self.notify.exit()
if self.process and self.process.stdout:
Expand Down
Loading