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
9 changes: 7 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -404,10 +404,15 @@ src/multilspy/language_servers/rust_analyzer/static/
src/multilspy/language_servers/typescript_language_server/static/
src/multilspy/language_servers/dart_language_server/static/
src/multilspy/language_servers/clangd_language_server/static/
src/multilspy/language_servers/metals/static/
src/multilspy/language_servers/expert_elixir/static/
src/multilspy/language_servers/sourcekit_lsp/static/
src/multilspy/language_servers/intelephense/static/
src/multilspy/language_servers/elixir_language_server/static/

# Virtual Environment
.venv/
venv/
uv.lock

src/multilspy/language_servers/intelephense/static/
src/multilspy/language_servers/elixir_language_server/static/
.DS_Store
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,13 @@ pip install multilspy
| dart | Dart |
| ruby | Solargraph |
| kotlin | KotlinLanguageServer |
| php | Intelephense |
| cpp | clangd |
| php | Intelephense |
| scala | Metals |
| swift | SourceKit-LSP |
| elixir | Expert |
| lua | LuaLS |
| bash | bash-language-server |


## Usage
Expand All @@ -60,7 +65,7 @@ from multilspy import SyncLanguageServer
from multilspy.multilspy_config import MultilspyConfig
from multilspy.multilspy_logger import MultilspyLogger
...
config = MultilspyConfig.from_dict({"code_language": "java"}) # Also supports "python", "rust", "csharp", "typescript", "javascript", "go", "dart", "ruby", "kotlin", "php"
config = MultilspyConfig.from_dict({"code_language": "java"}) # Also supports "python", "rust", "csharp", "typescript", "javascript", "go", "dart", "ruby", "kotlin", "cpp", "php", "scala", "swift", "elixir", "lua", "bash"
logger = MultilspyLogger()
lsp = SyncLanguageServer.create(config, logger, "/abs/path/to/project/root/")
with lsp.start_server():
Expand Down
3 changes: 2 additions & 1 deletion src/multilspy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@

from . import multilspy_types as Types
from .language_server import LanguageServer, SyncLanguageServer
from .path_classifier import PathClassifier, PathClassification

__all__ = ["LanguageServer", "Types", "SyncLanguageServer"]
__all__ = ["LanguageServer", "Types", "SyncLanguageServer", "PathClassifier", "PathClassification"]
474 changes: 331 additions & 143 deletions src/multilspy/language_server.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
Bash language server package.
"""

from multilspy.language_servers.bash_language_server.bash_language_server import BashLanguageServer

__all__ = ["BashLanguageServer"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
"""
Provides Bash (bash-language-server) specific instantiation of the LanguageServer class.
"""

import asyncio
import json
import logging
import os
import pathlib
import shutil
import stat
import subprocess
from contextlib import asynccontextmanager
from typing import AsyncIterator, List

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 PlatformUtils
from multilspy.multilspy_settings import MultilspySettings

# Conditionally import pwd module (Unix-only)
if not PlatformUtils.get_platform_id().value.startswith("win"):
import pwd


class BashLanguageServer(LanguageServer):
"""
Provides Bash specific instantiation of the LanguageServer class using bash-language-server.
"""

def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str):
"""
Creates a BashLanguageServer instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
cmd = self.setup_runtime_dependencies(logger, config)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path),
"shellscript",
)
self.server_ready = asyncio.Event()

def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> List[str]:
"""
Setup runtime dependencies for bash-language-server.
"""
if config.server_binary:
assert os.path.exists(config.server_binary), f"Server binary not found: {config.server_binary}"
return [config.server_binary, "start"]

is_node_installed = shutil.which("node") is not None
assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
is_npm_installed = shutil.which("npm") is not None
assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."

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.get("runtimeDependencies", [])
bash_ls_dir = config.server_install_dir or MultilspySettings.get_server_install_directory("bash-language-server")

if PlatformUtils.get_platform_id().value.startswith("win"):
bash_executable_path = os.path.join(bash_ls_dir, "node_modules", ".bin", "bash-language-server.cmd")
else:
bash_executable_path = os.path.join(bash_ls_dir, "node_modules", ".bin", "bash-language-server")

if not os.path.exists(bash_executable_path):
os.makedirs(bash_ls_dir, exist_ok=True)
for dependency in runtime_dependencies:
if PlatformUtils.get_platform_id().value.startswith("win"):
subprocess.run(
dependency["command"],
shell=True,
check=True,
cwd=bash_ls_dir,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
else:
user = pwd.getpwuid(os.getuid()).pw_name
subprocess.run(
dependency["command"],
shell=True,
check=True,
user=user,
cwd=bash_ls_dir,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)

assert os.path.exists(bash_executable_path), "bash-language-server executable not found. Please install bash-language-server and try again."

if not PlatformUtils.get_platform_id().value.startswith("win"):
os.chmod(
bash_executable_path,
os.stat(bash_executable_path).st_mode
| stat.S_IXUSR
| stat.S_IXGRP
| stat.S_IXOTH,
)

return [bash_executable_path, "start"]

def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Bash 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["BashLanguageServer"]:
"""
Starts the Bash Language Server, waits for the server to be ready and yields the LanguageServer instance.
"""

async def register_capability_handler(params):
return None

async def execute_client_command_handler(params):
return []

async def workspace_configuration_handler(params):
items = params.get("items", []) if isinstance(params, dict) else []
return [{} for _ in items]

async def work_done_progress_create_handler(params):
return None

async def do_nothing(params):
return None

async def window_log_message(msg):
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
if isinstance(msg, dict) and "BackgroundAnalysis: Completed" in msg.get("message", ""):
self.server_ready.set()

async def window_show_message(msg):
self.logger.log(f"LSP: window/showMessage: {msg}", logging.INFO)

self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_request("workspace/configuration", workspace_configuration_handler)
self.server.on_request("window/workDoneProgress/create", work_done_progress_create_handler)
self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("window/showMessage", window_show_message)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)

async with super().start_server():
self.logger.log("Starting Bash language 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)
self.logger.log(
f"Received initialize response from bash-language-server: {init_response.get('serverInfo', {})}",
logging.INFO,
)

self.server.notify.initialized({})
self.completions_available.set()

try:
await asyncio.wait_for(self.server_ready.wait(), timeout=3.0)
except asyncio.TimeoutError:
self.server_ready.set()

try:
yield self
finally:
await self.server.shutdown()
await self.server.stop()
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"_description": "This file contains the initialization parameters for the Bash Language Server.",
"processId": "$processId",
"rootPath": "$rootPath",
"rootUri": "$rootUri",
"capabilities": {
"workspace": {
"configuration": true,
"workspaceFolders": true
},
"textDocument": {
"synchronization": {
"dynamicRegistration": true,
"willSave": true,
"didSave": true
},
"completion": {
"dynamicRegistration": true,
"completionItem": {
"snippetSupport": true
}
},
"definition": {
"dynamicRegistration": true,
"linkSupport": true
},
"references": {
"dynamicRegistration": true
},
"documentSymbol": {
"dynamicRegistration": true,
"hierarchicalDocumentSymbolSupport": true
},
"hover": {
"dynamicRegistration": true,
"contentFormat": ["markdown", "plaintext"]
}
},
"window": {
"workDoneProgress": true
}
},
"initializationOptions": {},
"trace": "verbose",
"workspaceFolders": [
{
"uri": "$uri",
"name": "$name"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"_description": "Used to install bash-language-server using npm.",
"runtimeDependencies": [
{
"id": "bash-language-server",
"description": "bash-language-server package for Linux, macOS, and Windows.",
"command": "npm install --prefix ./ bash-language-server@5.8.0"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
Lua language server package.
"""

from multilspy.language_servers.lua_language_server.lua_language_server import LuaLanguageServer

__all__ = ["LuaLanguageServer"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"_description": "This file contains the initialization parameters for the Lua Language Server.",
"processId": "$processId",
"rootPath": "$rootPath",
"rootUri": "$rootUri",
"capabilities": {
"workspace": {
"configuration": true,
"workspaceFolders": true
},
"textDocument": {
"synchronization": {
"dynamicRegistration": true,
"willSave": true,
"didSave": true
},
"completion": {
"dynamicRegistration": true,
"completionItem": {
"snippetSupport": true
}
},
"definition": {
"dynamicRegistration": true,
"linkSupport": true
},
"references": {
"dynamicRegistration": true
},
"documentSymbol": {
"dynamicRegistration": true,
"hierarchicalDocumentSymbolSupport": true
},
"hover": {
"dynamicRegistration": true,
"contentFormat": ["markdown", "plaintext"]
}
},
"window": {
"workDoneProgress": true
}
},
"initializationOptions": {},
"trace": "verbose",
"workspaceFolders": [
{
"uri": "$uri",
"name": "$name"
}
]
}
Loading