mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 18:16:10 +00:00
Merge branch 'main' into codex/add-analytics-to-serena-dashboard
# Conflicts: # src/serena/agent.py
This commit is contained in:
commit
194ff3bd69
116 files changed
+8212
-9036
No files matched your search
@@ -15,6 +15,7 @@ jobs:
|
||||
codespell:
|
||||
name: Check for spelling errors
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -4,8 +4,6 @@ on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
tags: [ 'v*' ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
@@ -14,6 +12,7 @@ env:
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
id-token: write
|
||||
pages: write
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
workflow_dispatch: # Manual trigger only - workflow disabled from auto-running
|
||||
|
||||
#on:
|
||||
# release:
|
||||
# types: [created]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
||||
@@ -14,7 +14,6 @@ jobs:
|
||||
cpu:
|
||||
name: Tests on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: "!contains(github.event.head_commit.message, 'ci skip')"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -32,9 +31,25 @@ jobs:
|
||||
# Add Go bin directory to PATH for this workflow
|
||||
# GITHUB_PATH is a special file that GitHub Actions uses to modify PATH
|
||||
# Writing to this file adds the directory to the PATH for subsequent steps
|
||||
- name: Cache Go binaries
|
||||
id: cache-go-binaries
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/go/bin
|
||||
~/.cache/go-build
|
||||
key: go-binaries-${{ runner.os }}-gopls-latest
|
||||
- name: Install gopls
|
||||
if: steps.cache-go-binaries.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: go install golang.org/x/tools/gopls@latest
|
||||
- name: Set up Elixir
|
||||
if: runner.os != 'Windows'
|
||||
uses: erlef/setup-beam@v1
|
||||
with:
|
||||
elixir-version: '1.18.4'
|
||||
otp-version: '26.1'
|
||||
|
||||
- name: Prepare java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
@@ -44,6 +59,11 @@ jobs:
|
||||
uses: DeLaGuardo/setup-clojure@13.4
|
||||
with:
|
||||
cli: latest
|
||||
- name: Install Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: "1.5.0"
|
||||
terraform_wrapper: false
|
||||
- name: Install uv
|
||||
shell: bash
|
||||
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
@@ -53,6 +73,14 @@ jobs:
|
||||
with:
|
||||
path: .venv
|
||||
key: uv-venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('uv.lock') }}
|
||||
- name: Cache language servers
|
||||
id: cache-language-servers
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: src/solidlsp/language_servers/static
|
||||
key: language-servers-${{ runner.os }}-v1
|
||||
restore-keys: |
|
||||
language-servers-${{ runner.os }}-
|
||||
- name: Create virtual environment
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -204,6 +204,7 @@ pylint.html
|
||||
# dynamic LS installations
|
||||
/src/multilspy/language_servers/*/static
|
||||
/src/solidlsp/language_servers/*/static
|
||||
/src/solidlsp/language_servers/static
|
||||
|
||||
# clojure-lsp temporary files
|
||||
.calva/
|
||||
@@ -221,3 +222,8 @@ tmp/
|
||||
|
||||
# Claude settings
|
||||
.claude/settings.local.json
|
||||
|
||||
# Elixir
|
||||
/test/resources/repos/elixir/test_repo/deps
|
||||
# Exception: Don't ignore Elixir test repository lib directory (contains source code)
|
||||
!/test/resources/repos/elixir/test_repo/lib
|
||||
@@ -1,126 +0,0 @@
|
||||
# Process Isolation: Current Status and Architecture
|
||||
|
||||
## Current Status: Disabled by Default
|
||||
|
||||
Process isolation is **disabled by default** in the current Serena implementation:
|
||||
- **Configuration**: `USE_PROCESS_ISOLATION = False` in `src/serena/constants.py:22`
|
||||
- **Reason**: No longer needed with solid-lsp architecture
|
||||
- **Default Operation**: Single process mode with MCP server, agent, and language servers in same process
|
||||
|
||||
## Why Process Isolation Is No Longer Needed
|
||||
|
||||
### Historical Context
|
||||
Process isolation was **mandatory** when using multilspy because:
|
||||
1. **Asyncio Contamination**: Multilspy leaked coroutines into MCP server's event loop
|
||||
2. **Event Loop Conflicts**: Multiple asyncio contexts caused deadlocks
|
||||
3. **Only Solution**: Complete process separation was the only way to prevent issues
|
||||
|
||||
### Current Architecture Benefits
|
||||
With solid-lsp, process isolation became **unnecessary** because:
|
||||
1. **Clean Async Boundaries**: No coroutine leakage between MCP server and language server contexts
|
||||
2. **Single Process Safety**: Solid-lsp designed to work safely within MCP server process
|
||||
3. **Performance Gains**: Direct method calls instead of expensive IPC
|
||||
|
||||
## Architecture Components (Still Present)
|
||||
|
||||
The process isolation infrastructure remains **available but unused** by default:
|
||||
|
||||
### Core Components (`src/serena/process_isolated_agent.py`)
|
||||
|
||||
#### 1. **ProcessIsolatedSerenaAgent** (lines 392-550)
|
||||
- **Purpose**: Wrapper that manages isolated agent process
|
||||
- **Communication**: Uses `multiprocessing.Pipe()` for bidirectional communication
|
||||
- **Process Management**: Creates and manages `SerenaAgentWorker` subprocess
|
||||
- **Status**: **Available but not used by default**
|
||||
|
||||
#### 2. **SerenaAgentWorker** (lines 173-389)
|
||||
- **Purpose**: Worker process hosting actual SerenaAgent
|
||||
- **Event Loop**: Polling loop checking for requests every 500ms
|
||||
- **Request Handling**: INITIALIZE, TOOL_CALL, SHUTDOWN, etc.
|
||||
- **Status**: **Available but not used by default**
|
||||
|
||||
#### 3. **ProcessIsolatedDashboard** (lines 133-170)
|
||||
- **Purpose**: Runs web dashboard in separate process
|
||||
- **Integration**: Async web server with port management
|
||||
- **Status**: **Available but not used by default**
|
||||
|
||||
### Global Synchronization (lines 25-28)
|
||||
```python
|
||||
_global_log_queue: multiprocessing.Queue = multiprocessing.Queue()
|
||||
_dashboard_ready_event = multiprocessing.Event()
|
||||
_dashboard_port_value = multiprocessing.Value("i", 0)
|
||||
global_shutdown_event = multiprocessing.Event()
|
||||
```
|
||||
**Status**: **Available but not actively used**
|
||||
|
||||
## Current MCP Factory Selection
|
||||
|
||||
In `src/serena/mcp.py:590`:
|
||||
```python
|
||||
if not USE_PROCESS_ISOLATION:
|
||||
mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file)
|
||||
else:
|
||||
mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file)
|
||||
```
|
||||
|
||||
### Default Flow
|
||||
1. **`USE_PROCESS_ISOLATION = False`** → **`SerenaMCPFactorySingleProcess`**
|
||||
2. **Single Process**: MCP server, SerenaAgent, and solid-lsp in same process
|
||||
3. **Direct Communication**: No IPC overhead, direct method calls
|
||||
|
||||
### Fallback Option
|
||||
If `USE_PROCESS_ISOLATION = True`:
|
||||
1. **`SerenaMCPFactoryWithProcessIsolation`** would be used
|
||||
2. **Separate Processes**: MCP server and SerenaAgent in different processes
|
||||
3. **IPC Communication**: Higher latency but complete isolation
|
||||
|
||||
## Benefits of Current Single Process Architecture
|
||||
|
||||
### 1. **Performance Improvements**
|
||||
- **Lower Latency**: Direct method calls vs IPC communication
|
||||
- **Reduced Memory**: No duplicate process memory footprints
|
||||
- **Faster Startup**: No process creation and initialization overhead
|
||||
|
||||
### 2. **Operational Simplicity**
|
||||
- **Unified Logging**: All components log to same destination
|
||||
- **Simpler Debugging**: Single process, unified stack traces
|
||||
- **Resource Management**: Simpler cleanup and shutdown procedures
|
||||
|
||||
### 3. **Stability Benefits**
|
||||
- **No IPC Failures**: Eliminated inter-process communication failure modes
|
||||
- **Consistent State**: No synchronization issues between processes
|
||||
- **Reliable Shutdown**: No orphaned processes or cleanup complexity
|
||||
|
||||
## When Process Isolation Might Still Be Useful
|
||||
|
||||
While not needed by default, process isolation could still be beneficial for:
|
||||
|
||||
### 1. **Fault Isolation**
|
||||
- **Agent Crashes**: Prevent agent failures from affecting MCP server
|
||||
- **Memory Protection**: Isolate memory leaks to specific processes
|
||||
- **Recovery**: Restart failed components without full system restart
|
||||
|
||||
### 2. **Resource Management**
|
||||
- **Memory Limits**: Constrain memory usage of specific components
|
||||
- **CPU Isolation**: Prevent CPU-intensive operations from blocking MCP server
|
||||
- **Security**: Additional process boundaries for security-sensitive environments
|
||||
|
||||
### 3. **Debugging and Development**
|
||||
- **Component Isolation**: Debug specific components in isolation
|
||||
- **Performance Analysis**: Measure resource usage per component
|
||||
- **Development Safety**: Prevent development errors from affecting stable components
|
||||
|
||||
## Configuration Management
|
||||
|
||||
### Enabling Process Isolation
|
||||
To re-enable process isolation:
|
||||
1. **Set**: `USE_PROCESS_ISOLATION = True` in `src/serena/constants.py`
|
||||
2. **Result**: Automatic fallback to `SerenaMCPFactoryWithProcessIsolation`
|
||||
3. **Trade-off**: Higher resource usage but complete component isolation
|
||||
|
||||
### Current Recommendation
|
||||
- **Default**: Keep `USE_PROCESS_ISOLATION = False` for optimal performance
|
||||
- **Special Cases**: Enable only when specific isolation requirements exist
|
||||
- **Testing**: Both modes should be tested to ensure compatibility
|
||||
|
||||
The current architecture successfully eliminated the need for process isolation while maintaining the capability as a fallback option for specialized use cases.
|
||||
@@ -17,7 +17,6 @@ The following tasks should generally be executed using `uv run poe <task_name>`.
|
||||
"typescript: language server running for TypeScript",
|
||||
"php: language server running for PHP",
|
||||
"snapshot: snapshot tests for symbolic editing operations",
|
||||
"isolated_process: test runs with process isolated agent",
|
||||
]
|
||||
```
|
||||
By default, `uv run poe test` uses the markers set in the env var `PYTEST_MARKERS`, or, if it unset, uses `-m "not java and not rust and not isolated process"`.
|
||||
|
||||
Vendored
+1
-1
@@ -11,4 +11,4 @@
|
||||
"sensai",
|
||||
"vibing"
|
||||
],
|
||||
}
|
||||
}
|
||||
+6
-3
@@ -9,8 +9,6 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
* The MCP server is the only asyncio-based component now, which resolves cross-component loop contamination,
|
||||
such that process isolation is no longer required.
|
||||
Neither are non-graceful shutdowns on Windows.
|
||||
* **Support for Clojure**
|
||||
* Better default, extended parameters and description for restricting the search in `search_for_pattern`
|
||||
* **Improved editing tools**: The editing logic was simplified and improved, making it more robust.
|
||||
* The "minimal indentation" logic was removed, because LLMs did not understand it.
|
||||
* The logic for the insertion of empty lines was improved (mostly controlled by the LLM now)
|
||||
@@ -18,7 +16,12 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
* allows the language server to be initialized in the background, making the MCP server respond to requests
|
||||
immediately upon startup,
|
||||
* ensures that all tool executions are fully synchronized (executed linearly).
|
||||
* Better support for C# by switching from `omnisharp` to Microsoft's official C# language server.
|
||||
* `SearchForPatternTool`: Better default, extended parameters and description for restricting the search
|
||||
* Language support:
|
||||
* Better support for C# by switching from `omnisharp` to Microsoft's official C# language server.
|
||||
* **Add support for Clojure**
|
||||
* Configuration:
|
||||
* Add option `web_dashboard_open_on_launch` (allowing the dashboard to be enabled without opening a browser window)
|
||||
|
||||
Fixes:
|
||||
* Fix `ExecuteShellCommandTool` and `GetCurrentConfigTool` hanging on Windows
|
||||
|
||||
@@ -73,6 +73,7 @@ With Serena, we provide
|
||||
* Rust
|
||||
* C# (requires dotnet to be installed. We switched the underlying language server recently, please report any issues you encounter)
|
||||
* Java (_Note_: startup is slow, initial startup especially so. There may be issues with java on macos and linux, we are working on it.)
|
||||
* Elixir (Requires NextLS and Elixir install; **Windows not supported** - Next LS does not provide Windows binaries)
|
||||
* Clojure
|
||||
* C/C++ (You may experience issues with finding references, we are working on it)
|
||||
* indirect support (may require some code changes/manual installation) for:
|
||||
@@ -201,9 +202,15 @@ This and other settings can be adjusted in the [configuration](#configuration) a
|
||||
git clone https://github.com/oraios/serena
|
||||
cd serena
|
||||
```
|
||||
2. Optionally create a config file from the template and adjust it according to your preferences.
|
||||
2. Optionally create the configuration file in your home directory, i.e.
|
||||
|
||||
* `~/.serena/serena_config.yml` on Linux and macOS, or
|
||||
* `%USERPROFILE%\.serena\serena_config.yml` on Windows.
|
||||
|
||||
by copying the template and then adjusting it according to your needs:
|
||||
```shell
|
||||
cp src/serena/resources/serena_config.template.yml serena_config.yml
|
||||
mkdir ~/.serena
|
||||
cp src/serena/resources/serena_config.template.yml ~/.serena/serena_config.yml
|
||||
```
|
||||
If you just want the default config, you can skip this part, and a config file will be created when you first run Serena.
|
||||
3. Run the server with `uv`:
|
||||
@@ -279,7 +286,9 @@ Run with parameter `--help` to get a list of available options.
|
||||
|
||||
Serena's behavior (active tools and prompts as well as logging configuration, etc.) is configured in four places:
|
||||
|
||||
1. The `serena_config.yml` for general settings that apply to all clients and projects
|
||||
1. The `serena_config.yml` for general settings that apply to all clients and projects.
|
||||
It is located in your user directory under `.serena/serena_config.yml`.
|
||||
If you do not explicitly create the file, it will be auto-generated when you first run Serena.
|
||||
2. In the arguments passed to the `serena-mcp-server` in your client's config (see below),
|
||||
which will apply to all sessions started by the respective client. In particular, the [context](#contexts) parameter
|
||||
should be set appropriately for Serena to be best adjusted to existing tools and capabilities of your client.
|
||||
@@ -346,7 +355,8 @@ claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena-
|
||||
```
|
||||
|
||||
ℹ️ Once in Claude Code, you should ask Claude to "Read the initial instructions" as your first prompt, such that it will receive information
|
||||
on how to use Serena's tools.
|
||||
on how to use Serena's tools. Provide this prompt whenever you start a new conversation and perhaps even after a compacting operation,
|
||||
such that Claude is correctly primed to use Serena's tools at all times.
|
||||
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
+4
-2
@@ -104,7 +104,7 @@ PYDEVD_DISABLE_FILE_VALIDATION = "1"
|
||||
# Uses PYTEST_MARKERS env var for default markers
|
||||
# For custom markers, one can either adjust the env var or just use -m option in the command line,
|
||||
# as the second -m option will override the first one.
|
||||
test = "pytest test -vv -m \"${PYTEST_MARKERS:-not java and not rust and not isolated_process}\""
|
||||
test = "pytest test -vv -m \"${PYTEST_MARKERS:-not java and not rust}\""
|
||||
_black_check = "black --check src scripts test"
|
||||
_ruff_check = "ruff check src scripts test"
|
||||
_black_format = "black src scripts test"
|
||||
@@ -224,6 +224,7 @@ ignore = [
|
||||
"TID252", # forbids relative imports
|
||||
"B904", # forces use of raise from other_exception
|
||||
"RUF012", # forbids mutable attributes as ClassVar
|
||||
"SIM117", # forbids nested with statements
|
||||
]
|
||||
unfixable = ["F841", "F601", "F602", "B018"]
|
||||
extend-fixable = ["F401", "B905", "W291"]
|
||||
@@ -246,8 +247,9 @@ markers = [
|
||||
"typescript: language server running for TypeScript",
|
||||
"php: language server running for PHP",
|
||||
"csharp: language server running for C#",
|
||||
"elixir: language server running for Elixir",
|
||||
"terraform: language server running for Terraform",
|
||||
"snapshot: snapshot tests for symbolic editing operations",
|
||||
"isolated_process: test runs with process isolated agent",
|
||||
]
|
||||
|
||||
[tool.codespell]
|
||||
|
||||
@@ -8,19 +8,8 @@ from pprint import pprint
|
||||
from serena.agent import *
|
||||
from serena.constants import REPO_ROOT
|
||||
|
||||
|
||||
@dataclass
|
||||
class InMemorySerenaConfig(SerenaConfigBase):
|
||||
"""
|
||||
In-memory implementation of Serena configuration with the GUI disabled.
|
||||
"""
|
||||
|
||||
gui_log_window_enabled: bool = False
|
||||
web_dashboard: bool = False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
agent = SerenaAgent(project=REPO_ROOT, serena_config=InMemorySerenaConfig())
|
||||
agent = SerenaAgent(project=REPO_ROOT, serena_config=SerenaConfig(gui_log_window_enabled=False, web_dashboard=False))
|
||||
|
||||
# apply a tool
|
||||
find_refs_tool = agent.get_tool(FindReferencingSymbolsTool)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from serena.config import SerenaAgentContext, SerenaAgentMode
|
||||
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("---------- Available modes: ----------")
|
||||
|
||||
+32
-1842
File diff suppressed because it is too large.
Load diff
+3
-2
@@ -14,9 +14,10 @@ from agno.tools.toolkit import Toolkit
|
||||
from dotenv import load_dotenv
|
||||
from sensai.util.logging import LogTime
|
||||
|
||||
from serena.agent import SerenaAgent, Tool, show_fatal_exception_safe
|
||||
from serena.config import SerenaAgentContext
|
||||
from serena.agent import SerenaAgent, Tool
|
||||
from serena.config.context_mode import SerenaAgentContext
|
||||
from serena.constants import REPO_ROOT
|
||||
from serena.util.exception import show_fatal_exception_safe
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
Whitespace-only changes.
File renamed without changes.
@@ -0,0 +1,412 @@
|
||||
"""
|
||||
The Serena Model Context Protocol (MCP) Server
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass, field
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Self, TypeVar
|
||||
|
||||
import yaml
|
||||
from ruamel.yaml.comments import CommentedMap
|
||||
from sensai.util import logging
|
||||
from sensai.util.string import ToStringMixin
|
||||
|
||||
from serena.constants import (
|
||||
DEFAULT_ENCODING,
|
||||
PROJECT_TEMPLATE_FILE,
|
||||
REPO_ROOT,
|
||||
SELENA_CONFIG_TEMPLATE_FILE,
|
||||
SERENA_MANAGED_DIR_NAME,
|
||||
)
|
||||
from serena.tools import Tool, ToolRegistry
|
||||
from serena.util.general import load_yaml, save_yaml
|
||||
from serena.util.inspection import determine_programming_language_composition
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
T = TypeVar("T")
|
||||
DEFAULT_TOOL_TIMEOUT: float = 240
|
||||
|
||||
|
||||
class SerenaConfigError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_serena_managed_dir(project_root: str | Path) -> str:
|
||||
return os.path.join(project_root, SERENA_MANAGED_DIR_NAME)
|
||||
|
||||
|
||||
def is_running_in_docker() -> bool:
|
||||
"""Check if we're running inside a Docker container."""
|
||||
# Check for Docker-specific files
|
||||
if os.path.exists("/.dockerenv"):
|
||||
return True
|
||||
# Check cgroup for docker references
|
||||
try:
|
||||
with open("/proc/self/cgroup") as f:
|
||||
return "docker" in f.read()
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectConfig(ToStringMixin):
|
||||
project_name: str
|
||||
language: Language
|
||||
ignored_paths: list[str] = field(default_factory=list)
|
||||
excluded_tools: set[str] = field(default_factory=set)
|
||||
read_only: bool = False
|
||||
ignore_all_files_in_gitignore: bool = True
|
||||
initial_prompt: str = ""
|
||||
encoding: str = DEFAULT_ENCODING
|
||||
|
||||
SERENA_DEFAULT_PROJECT_FILE = "project.yml"
|
||||
|
||||
@classmethod
|
||||
def autogenerate(cls, project_root: str | Path, project_name: str | None = None, save_to_disk: bool = True) -> Self:
|
||||
"""
|
||||
Autogenerate a project configuration for a given project root.
|
||||
|
||||
:param project_root: the path to the project root
|
||||
:param project_name: the name of the project; if None, the name of the project will be the name of the directory
|
||||
containing the project
|
||||
:param save_to_disk: whether to save the project configuration to disk
|
||||
:return: the project configuration
|
||||
"""
|
||||
project_root = Path(project_root).resolve()
|
||||
if not project_root.exists():
|
||||
raise FileNotFoundError(f"Project root not found: {project_root}")
|
||||
project_name = project_name or project_root.name
|
||||
language_composition = determine_programming_language_composition(str(project_root))
|
||||
if len(language_composition) == 0:
|
||||
raise ValueError(
|
||||
f"No source files found in {project_root}\n\n"
|
||||
f"To use Serena with this project, you need to either:\n"
|
||||
f"1. Add source files in one of the supported languages (Python, JavaScript/TypeScript, Java, C#, Rust, Go, Ruby, C++, PHP)\n"
|
||||
f"2. Create a project configuration file manually at:\n"
|
||||
f" {os.path.join(project_root, cls.rel_path_to_project_yml())}\n\n"
|
||||
f"Example project.yml:\n"
|
||||
f" project_name: {project_name}\n"
|
||||
f" language: python # or typescript, java, csharp, rust, go, ruby, cpp, php\n"
|
||||
)
|
||||
# find the language with the highest percentage
|
||||
dominant_language = max(language_composition.keys(), key=lambda lang: language_composition[lang])
|
||||
config_with_comments = load_yaml(PROJECT_TEMPLATE_FILE, preserve_comments=True)
|
||||
config_with_comments["project_name"] = project_name
|
||||
config_with_comments["language"] = dominant_language
|
||||
if save_to_disk:
|
||||
save_yaml(str(project_root / cls.rel_path_to_project_yml()), config_with_comments, preserve_comments=True)
|
||||
return cls._from_dict(config_with_comments)
|
||||
|
||||
@classmethod
|
||||
def rel_path_to_project_yml(cls) -> str:
|
||||
return os.path.join(SERENA_MANAGED_DIR_NAME, cls.SERENA_DEFAULT_PROJECT_FILE)
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, data: dict[str, Any]) -> Self:
|
||||
"""
|
||||
Create a ProjectConfig instance from a configuration dictionary
|
||||
"""
|
||||
language_str = data["language"].lower()
|
||||
project_name = data["project_name"]
|
||||
# backwards compatibility
|
||||
if language_str == "javascript":
|
||||
log.warning(f"Found deprecated project language `javascript` in project {project_name}, please change to `typescript`")
|
||||
language_str = "typescript"
|
||||
try:
|
||||
language = Language(language_str)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Invalid language: {data['language']}.\nValid languages are: {[l.value for l in Language]}") from e
|
||||
return cls(
|
||||
project_name=project_name,
|
||||
language=language,
|
||||
ignored_paths=data.get("ignored_paths", []),
|
||||
excluded_tools=set(data.get("excluded_tools", [])),
|
||||
read_only=data.get("read_only", False),
|
||||
ignore_all_files_in_gitignore=data.get("ignore_all_files_in_gitignore", True),
|
||||
initial_prompt=data.get("initial_prompt", ""),
|
||||
encoding=data.get("encoding", DEFAULT_ENCODING),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load(cls, project_root: Path | str, autogenerate: bool = True) -> Self:
|
||||
"""
|
||||
Load a ProjectConfig instance from the path to the project root.
|
||||
"""
|
||||
project_root = Path(project_root)
|
||||
yaml_path = project_root / cls.rel_path_to_project_yml()
|
||||
if not yaml_path.exists():
|
||||
if autogenerate:
|
||||
return cls.autogenerate(project_root)
|
||||
else:
|
||||
raise FileNotFoundError(f"Project configuration file not found: {yaml_path}")
|
||||
with open(yaml_path, encoding="utf-8") as f:
|
||||
yaml_data = yaml.safe_load(f)
|
||||
if "project_name" not in yaml_data:
|
||||
yaml_data["project_name"] = project_root.name
|
||||
return cls._from_dict(yaml_data)
|
||||
|
||||
def get_excluded_tool_classes(self) -> set[type["Tool"]]:
|
||||
return set(ToolRegistry.get_tool_class_by_name(tool_name) for tool_name in self.excluded_tools)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Project:
|
||||
project_root: str
|
||||
project_config: ProjectConfig
|
||||
|
||||
@property
|
||||
def project_name(self) -> str:
|
||||
return self.project_config.project_name
|
||||
|
||||
@property
|
||||
def language(self) -> Language:
|
||||
return self.project_config.language
|
||||
|
||||
@classmethod
|
||||
def load(cls, project_root: str | Path, autogenerate: bool = True) -> Self:
|
||||
project_root = Path(project_root).resolve()
|
||||
if not project_root.exists():
|
||||
raise FileNotFoundError(f"Project root not found: {project_root}")
|
||||
project_config = ProjectConfig.load(project_root, autogenerate=autogenerate)
|
||||
return cls(project_root=str(project_root), project_config=project_config)
|
||||
|
||||
def path_to_project_yml(self) -> str:
|
||||
return os.path.join(self.project_root, self.project_config.rel_path_to_project_yml())
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class SerenaConfig:
|
||||
"""
|
||||
Holds the Serena agent configuration, which is typically loaded from a YAML configuration file
|
||||
(when instantiated via :method:`from_config_file`), which is updated when projects are added or removed.
|
||||
For testing purposes, it can also be instantiated directly with the desired parameters.
|
||||
"""
|
||||
|
||||
projects: list[Project] = field(default_factory=list)
|
||||
gui_log_window_enabled: bool = False
|
||||
log_level: int = logging.INFO
|
||||
trace_lsp_communication: bool = False
|
||||
web_dashboard: bool = True
|
||||
web_dashboard_open_on_launch: bool = True
|
||||
tool_timeout: float = DEFAULT_TOOL_TIMEOUT
|
||||
loaded_commented_yaml: CommentedMap | None = None
|
||||
config_file_path: str | None = None
|
||||
"""
|
||||
the path to the configuration file to which updates of the configuration shall be saved;
|
||||
if None, the configuration is not saved to disk
|
||||
"""
|
||||
|
||||
CONFIG_FILE = "serena_config.yml"
|
||||
CONFIG_FILE_DOCKER = "serena_config.docker.yml" # Docker-specific config file; auto-generated if missing, mounted via docker-compose for user customization
|
||||
|
||||
@classmethod
|
||||
def _generate_config_file(cls, config_file_path: str) -> None:
|
||||
"""
|
||||
Generates a Serena configuration file at the specified path from the template file.
|
||||
|
||||
:param config_file_path: the path where the configuration file should be generated
|
||||
"""
|
||||
log.info(f"Auto-generating Serena configuration file in {config_file_path}")
|
||||
loaded_commented_yaml = load_yaml(SELENA_CONFIG_TEMPLATE_FILE, preserve_comments=True)
|
||||
save_yaml(config_file_path, loaded_commented_yaml, preserve_comments=True)
|
||||
|
||||
@classmethod
|
||||
def _determine_config_file_path(cls) -> str:
|
||||
"""
|
||||
:return: the location where the Serena configuration file is stored/should be stored
|
||||
"""
|
||||
if is_running_in_docker():
|
||||
return os.path.join(REPO_ROOT, cls.CONFIG_FILE_DOCKER)
|
||||
else:
|
||||
config_path = str(Path.home() / SERENA_MANAGED_DIR_NAME / cls.CONFIG_FILE)
|
||||
|
||||
# if the config file does not exist, check if we can migrate it from the old location
|
||||
if not os.path.exists(config_path):
|
||||
old_config_path = os.path.join(REPO_ROOT, cls.CONFIG_FILE)
|
||||
if os.path.exists(old_config_path):
|
||||
log.info(f"Moving Serena configuration file from {old_config_path} to {config_path}")
|
||||
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
||||
shutil.move(old_config_path, config_path)
|
||||
|
||||
return config_path
|
||||
|
||||
@classmethod
|
||||
def from_config_file(cls, generate_if_missing: bool = True) -> "SerenaConfig":
|
||||
"""
|
||||
Static constructor to create SerenaConfig from the configuration file
|
||||
"""
|
||||
config_file_path = cls._determine_config_file_path()
|
||||
|
||||
# create the configuration file from the template if necessary
|
||||
if not os.path.exists(config_file_path):
|
||||
if not generate_if_missing:
|
||||
raise FileNotFoundError(f"Serena configuration file not found: {config_file_path}")
|
||||
log.info(f"Serena configuration file not found at {config_file_path}, autogenerating...")
|
||||
cls._generate_config_file(config_file_path)
|
||||
|
||||
# load the configuration
|
||||
log.info(f"Loading Serena configuration from {config_file_path}")
|
||||
try:
|
||||
loaded_commented_yaml = load_yaml(config_file_path, preserve_comments=True)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error loading Serena configuration from {config_file_path}: {e}") from e
|
||||
|
||||
# create the configuration instance
|
||||
instance = cls(loaded_commented_yaml=loaded_commented_yaml, config_file_path=config_file_path)
|
||||
|
||||
# read projects
|
||||
if "projects" not in loaded_commented_yaml:
|
||||
raise SerenaConfigError("`projects` key not found in Serena configuration. Please update your `serena_config.yml` file.")
|
||||
|
||||
# load list of known projects
|
||||
instance.projects = []
|
||||
num_project_migrations = 0
|
||||
for path in loaded_commented_yaml["projects"]:
|
||||
path = Path(path).resolve()
|
||||
if not path.exists() or (path.is_dir() and not (path / ProjectConfig.rel_path_to_project_yml()).exists()):
|
||||
log.warning(f"Project path {path} does not exist or does not contain a project configuration file, skipping.")
|
||||
continue
|
||||
if path.is_file():
|
||||
path = cls._migrate_out_of_project_config_file(path)
|
||||
if path is None:
|
||||
continue
|
||||
num_project_migrations += 1
|
||||
project = Project.load(path)
|
||||
instance.projects.append(project)
|
||||
|
||||
# set other configuration parameters
|
||||
if is_running_in_docker():
|
||||
instance.gui_log_window_enabled = False # not supported in Docker
|
||||
else:
|
||||
instance.gui_log_window_enabled = loaded_commented_yaml.get("gui_log_window", False)
|
||||
instance.log_level = loaded_commented_yaml.get("log_level", loaded_commented_yaml.get("gui_log_level", logging.INFO))
|
||||
instance.web_dashboard = loaded_commented_yaml.get("web_dashboard", True)
|
||||
instance.web_dashboard_open_on_launch = loaded_commented_yaml.get("web_dashboard_open_on_launch", True)
|
||||
instance.tool_timeout = loaded_commented_yaml.get("tool_timeout", DEFAULT_TOOL_TIMEOUT)
|
||||
instance.trace_lsp_communication = loaded_commented_yaml.get("trace_lsp_communication", False)
|
||||
|
||||
# re-save the configuration file if any migrations were performed
|
||||
if num_project_migrations > 0:
|
||||
log.info(
|
||||
f"Migrated {num_project_migrations} project configurations from legacy format to in-project configuration; re-saving configuration"
|
||||
)
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def _migrate_out_of_project_config_file(cls, path: Path) -> Path | None:
|
||||
"""
|
||||
Migrates a legacy project configuration file (which is a YAML file containing the project root) to the
|
||||
in-project configuration file (project.yml) inside the project root directory.
|
||||
|
||||
:param path: the path to the legacy project configuration file
|
||||
:return: the project root path if the migration was successful, None otherwise.
|
||||
"""
|
||||
log.info(f"Found legacy project configuration file {path}, migrating to in-project configuration.")
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
project_config_data = yaml.safe_load(f)
|
||||
if "project_name" not in project_config_data:
|
||||
project_name = path.stem
|
||||
with open(path, "a", encoding="utf-8") as f:
|
||||
f.write(f"\nproject_name: {project_name}")
|
||||
project_root = project_config_data["project_root"]
|
||||
shutil.move(str(path), str(Path(project_root) / ProjectConfig.rel_path_to_project_yml()))
|
||||
return Path(project_root).resolve()
|
||||
except Exception as e:
|
||||
log.error(f"Error migrating configuration file: {e}")
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def project_paths(self) -> list[str]:
|
||||
return sorted(project.project_root for project in self.projects)
|
||||
|
||||
@cached_property
|
||||
def project_names(self) -> list[str]:
|
||||
return sorted(project.project_config.project_name for project in self.projects)
|
||||
|
||||
def get_project(self, project_root_or_name: str) -> Project | None:
|
||||
for project in self.projects:
|
||||
if project.project_config.project_name == project_root_or_name:
|
||||
return project
|
||||
if os.path.isdir(project_root_or_name):
|
||||
project_root = Path(project_root_or_name).resolve()
|
||||
for project in self.projects:
|
||||
if Path(project.project_root).resolve() == project_root:
|
||||
return project
|
||||
return None
|
||||
|
||||
def add_project_from_path(self, project_root: Path | str, project_name: str | None = None) -> tuple[Project, bool]:
|
||||
"""
|
||||
Add a project to the Serena configuration from a given path. Will raise a FileExistsError if the
|
||||
name or path is already registered.
|
||||
|
||||
:param project_root: the path to the project to add
|
||||
:param project_name: the name of the project to add; if None, the name of the project will be the name of the directory
|
||||
containing the project
|
||||
:return: the project that was added and a boolean indicating whether a new project configuration was generated and
|
||||
saved to disk. It may be that no new project configuration was generated if the project configuration already
|
||||
exists on disk but the project itself was not added yet to the Serena configuration.
|
||||
"""
|
||||
project_root = Path(project_root).resolve()
|
||||
if not project_root.exists():
|
||||
raise FileNotFoundError(f"Error: Path does not exist: {project_root}")
|
||||
if not project_root.is_dir():
|
||||
raise FileNotFoundError(f"Error: Path is not a directory: {project_root}")
|
||||
|
||||
if project_name is None:
|
||||
project_name = project_root.name
|
||||
for already_registered_project in self.projects:
|
||||
if already_registered_project.project_name == project_name:
|
||||
raise FileExistsError(
|
||||
f"Project name '{project_name}' already exists and points to {already_registered_project.project_root}."
|
||||
)
|
||||
if str(already_registered_project.project_root) == str(project_root):
|
||||
raise FileExistsError(
|
||||
f"Project with path {project_root} was already added with name '{already_registered_project.project_name}'."
|
||||
)
|
||||
|
||||
try:
|
||||
project_config = ProjectConfig.load(project_root)
|
||||
new_project_config_generated = False
|
||||
except FileNotFoundError:
|
||||
project_config = ProjectConfig.autogenerate(project_root, save_to_disk=True)
|
||||
new_project_config_generated = True
|
||||
|
||||
new_project = Project(project_root=str(project_root), project_config=project_config)
|
||||
self.projects.append(new_project)
|
||||
self.save()
|
||||
|
||||
return new_project, new_project_config_generated
|
||||
|
||||
def remove_project(self, project_name: str) -> None:
|
||||
# find the index of the project with the desired name and remove it
|
||||
for i, project in enumerate(self.projects):
|
||||
if project.project_name == project_name:
|
||||
del self.projects[i]
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"Project '{project_name}' not found in Serena configuration; valid project names: {self.project_names}")
|
||||
self.save()
|
||||
|
||||
def save(self) -> None:
|
||||
"""
|
||||
Saves the configuration to the file from which it was loaded (if any)
|
||||
"""
|
||||
if self.config_file_path is None:
|
||||
return
|
||||
assert self.loaded_commented_yaml is not None, "Cannot save configuration without loaded YAML"
|
||||
loaded_original_yaml = deepcopy(self.loaded_commented_yaml)
|
||||
# projects are unique absolute paths
|
||||
# we also canonicalize them before saving
|
||||
loaded_original_yaml["projects"] = sorted({str(Path(project.project_root).resolve()) for project in self.projects})
|
||||
save_yaml(self.config_file_path, loaded_original_yaml, preserve_comments=True)
|
||||
@@ -19,6 +19,4 @@ DEFAULT_MODES = ("interactive", "editing")
|
||||
PROJECT_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "project.template.yml")
|
||||
SELENA_CONFIG_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "serena_config.template.yml")
|
||||
|
||||
USE_PROCESS_ISOLATION = False
|
||||
|
||||
SERENA_LOG_FORMAT = "%(levelname)-5s %(asctime)-15s [%(threadName)s] %(name)s:%(funcName)s:%(lineno)d - %(message)s"
|
||||
@@ -150,11 +150,8 @@ class SerenaDashboardAPI:
|
||||
if self._shutdown_callback:
|
||||
self._shutdown_callback()
|
||||
else:
|
||||
# Try to use the global shutdown function from process_isolated_agent
|
||||
from serena.process_isolated_agent import request_global_shutdown
|
||||
|
||||
request_global_shutdown()
|
||||
# noinspection PyProtectedMember
|
||||
# noinspection PyUnresolvedReferences
|
||||
os._exit(0)
|
||||
|
||||
@staticmethod
|
||||
|
||||
+22
-266
@@ -2,20 +2,14 @@
|
||||
The Serena Model Context Protocol (MCP) Server
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from abc import abstractmethod
|
||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
from logging import Formatter, Logger, StreamHandler
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
import click
|
||||
import docstring_parser
|
||||
@@ -26,24 +20,13 @@ from pydantic_settings import SettingsConfigDict
|
||||
from sensai.util import logging
|
||||
|
||||
from serena.agent import (
|
||||
ActivateProjectTool,
|
||||
Project,
|
||||
SerenaAgent,
|
||||
SerenaConfig,
|
||||
ToolInterface,
|
||||
ToolRegistry,
|
||||
create_serena_config,
|
||||
show_fatal_exception_safe,
|
||||
)
|
||||
from serena.config import RegisteredContext, SerenaAgentContext, SerenaAgentMode
|
||||
from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES, USE_PROCESS_ISOLATION
|
||||
from serena.process_isolated_agent import (
|
||||
ProcessIsolatedDashboard,
|
||||
ProcessIsolatedSerenaAgent,
|
||||
ProcessIsolatedTool,
|
||||
global_shutdown_event,
|
||||
request_global_shutdown,
|
||||
)
|
||||
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
|
||||
from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES
|
||||
from serena.tools import ToolInterface
|
||||
from serena.util.exception import show_fatal_exception_safe
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s"
|
||||
@@ -165,15 +148,23 @@ class SerenaMCPFactory:
|
||||
:param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration.
|
||||
"""
|
||||
try:
|
||||
serena_config = create_serena_config(
|
||||
enable_web_dashboard=enable_web_dashboard,
|
||||
enable_gui_log_window=enable_gui_log_window,
|
||||
log_level=log_level,
|
||||
trace_lsp_communication=trace_lsp_communication,
|
||||
tool_timeout=tool_timeout,
|
||||
)
|
||||
config = SerenaConfig.from_config_file()
|
||||
|
||||
# update configuration with the provided parameters
|
||||
if enable_web_dashboard is not None:
|
||||
config.web_dashboard = enable_web_dashboard
|
||||
if enable_gui_log_window is not None:
|
||||
config.gui_log_window_enabled = enable_gui_log_window
|
||||
if log_level is not None:
|
||||
log_level = cast(Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], log_level.upper())
|
||||
config.log_level = logging.getLevelNamesMapping()[log_level]
|
||||
if trace_lsp_communication is not None:
|
||||
config.trace_lsp_communication = trace_lsp_communication
|
||||
if tool_timeout is not None:
|
||||
config.tool_timeout = tool_timeout
|
||||
|
||||
modes_instances = [SerenaAgentMode.load(mode) for mode in modes]
|
||||
self._instantiate_agent(serena_config, modes_instances)
|
||||
self._instantiate_agent(config, modes_instances)
|
||||
|
||||
except Exception as e:
|
||||
show_fatal_exception_safe(e)
|
||||
@@ -224,235 +215,6 @@ class SerenaMCPFactorySingleProcess(SerenaMCPFactory):
|
||||
yield
|
||||
|
||||
|
||||
class SerenaMCPFactoryWithProcessIsolation(SerenaMCPFactory):
|
||||
"""
|
||||
MCP server factory with process isolation for the SerenaAgent and its language server; they run in a separate process
|
||||
from the MCP server.
|
||||
"""
|
||||
|
||||
def __init__(self, context: str = DEFAULT_CONTEXT, project: str | None = None):
|
||||
"""
|
||||
:param context: The context name or path to context file
|
||||
:param project: Either an absolute path to the project directory or a name of an already registered project.
|
||||
If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name
|
||||
afterward.
|
||||
"""
|
||||
super().__init__(context=context, project=project)
|
||||
|
||||
self.active_tool_names: set[str] | None = None
|
||||
self.serena_agent_process: ProcessIsolatedSerenaAgent | None = None
|
||||
self.serena_dashboard_process: ProcessIsolatedDashboard | None = None
|
||||
|
||||
@staticmethod
|
||||
def _determine_active_tool_names(context: SerenaAgentContext, project: Project | None) -> set[str]:
|
||||
"""
|
||||
Determine the names of tools that should be included in this session based on the context.
|
||||
"""
|
||||
tools_excluded_in_this_session = context.get_excluded_tool_classes()
|
||||
|
||||
# if a project has been loaded, it will be activated at startup and in ide-assistant context,
|
||||
# we assume that no other project will be activated in this session.
|
||||
# Therefore, we exclude the activate project tool
|
||||
is_ide_assistant = context.name == RegisteredContext.IDE_ASSISTANT.value
|
||||
if is_ide_assistant and project is not None:
|
||||
tools_excluded_in_this_session.extend(project.project_config.get_excluded_tool_classes())
|
||||
tools_excluded_in_this_session.append(ActivateProjectTool)
|
||||
|
||||
tool_names_excluded_in_this_session = {tool.get_name_from_cls() for tool in tools_excluded_in_this_session}
|
||||
|
||||
all_tool_names = set(ToolRegistry.get_tool_names())
|
||||
tool_names_included_in_this_session = all_tool_names - tool_names_excluded_in_this_session
|
||||
return tool_names_included_in_this_session
|
||||
|
||||
@staticmethod
|
||||
def make_mcp_tool(tool: ToolInterface) -> MCPTool:
|
||||
func_name = tool.get_name()
|
||||
func_doc = tool.get_apply_docstring() or ""
|
||||
func_arg_metadata = tool.get_apply_fn_metadata()
|
||||
is_async = False
|
||||
parameters = func_arg_metadata.arg_model.model_json_schema()
|
||||
|
||||
docstring = docstring_parser.parse(func_doc)
|
||||
|
||||
# Mount the tool description as a combination of the docstring description and
|
||||
# the return value description, if it exists.
|
||||
if docstring.description:
|
||||
func_doc = f"{docstring.description.strip().strip('.')}."
|
||||
else:
|
||||
func_doc = ""
|
||||
if docstring.returns and (docstring_returns_descr := docstring.returns.description):
|
||||
# Only add a space before "Returns" if func_doc is not empty
|
||||
prefix = " " if func_doc else ""
|
||||
func_doc = f"{func_doc}{prefix}Returns {docstring_returns_descr.strip().strip('.')}."
|
||||
|
||||
# Parse the parameter descriptions from the docstring and add pass its description
|
||||
# to the parameter schema.
|
||||
docstring_params = {param.arg_name: param for param in docstring.params}
|
||||
parameters_properties: dict[str, dict[str, Any]] = parameters["properties"]
|
||||
for parameter, properties in parameters_properties.items():
|
||||
if (param_doc := docstring_params.get(parameter)) and param_doc.description:
|
||||
param_desc = f"{param_doc.description.strip().strip('.') + '.'}"
|
||||
properties["description"] = param_desc[0].upper() + param_desc[1:]
|
||||
|
||||
def execute_fn(**kwargs) -> str: # type: ignore
|
||||
return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs)
|
||||
|
||||
return MCPTool(
|
||||
fn=execute_fn,
|
||||
name=func_name,
|
||||
description=func_doc,
|
||||
parameters=parameters,
|
||||
fn_metadata=func_arg_metadata,
|
||||
is_async=is_async,
|
||||
context_kwarg=None,
|
||||
annotations=None,
|
||||
)
|
||||
|
||||
def _iter_tools(self) -> Iterator[ToolInterface]:
|
||||
assert self.active_tool_names is not None
|
||||
assert self.serena_agent_process is not None
|
||||
for tool_name in self.active_tool_names:
|
||||
yield ProcessIsolatedTool(process_agent=self.serena_agent_process, tool_name=tool_name)
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
def _set_mcp_tools(self, mcp: FastMCP) -> None:
|
||||
"""Update the tools in the MCP server"""
|
||||
if mcp is not None:
|
||||
mcp._tool_manager._tools = {}
|
||||
for tool in self._iter_tools():
|
||||
mcp_tool = self.make_mcp_tool(tool)
|
||||
mcp._tool_manager._tools[tool.get_name()] = mcp_tool
|
||||
|
||||
def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None:
|
||||
if self.project is not None:
|
||||
self.project_instance = serena_config.get_project(self.project)
|
||||
self.serena_agent_process = ProcessIsolatedSerenaAgent(
|
||||
project=self.project, serena_config=serena_config, modes=modes, context=self.context
|
||||
)
|
||||
self.active_tool_names = self._determine_active_tool_names(self.context, self.project_instance)
|
||||
if serena_config.web_dashboard:
|
||||
assert self.active_tool_names is not None
|
||||
self.serena_dashboard_process = ProcessIsolatedDashboard(tool_names=sorted(self.active_tool_names))
|
||||
|
||||
def create_mcp_server(
|
||||
self,
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 8000,
|
||||
modes: Sequence[str] = DEFAULT_MODES,
|
||||
enable_web_dashboard: bool | None = None,
|
||||
enable_gui_log_window: bool | None = None,
|
||||
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None,
|
||||
trace_lsp_communication: bool | None = None,
|
||||
tool_timeout: float | None = None,
|
||||
) -> FastMCP:
|
||||
"""
|
||||
Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination.
|
||||
|
||||
:param host: The host to bind to
|
||||
:param port: The port to bind to
|
||||
:param modes: List of mode names or paths to mode files
|
||||
:param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration.
|
||||
:param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then.
|
||||
If not specified, will take the value from the serena configuration.
|
||||
:param log_level: Log level. If not specified, will take the value from the serena configuration.
|
||||
:param trace_lsp_communication: Whether to trace the communication between Serena and the language servers.
|
||||
This is useful for debugging language server issues.
|
||||
:param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration.
|
||||
"""
|
||||
try:
|
||||
serena_config = create_serena_config(
|
||||
enable_web_dashboard=enable_web_dashboard,
|
||||
enable_gui_log_window=enable_gui_log_window,
|
||||
log_level=log_level,
|
||||
trace_lsp_communication=trace_lsp_communication,
|
||||
tool_timeout=tool_timeout,
|
||||
)
|
||||
modes_instances = [SerenaAgentMode.load(mode) for mode in modes]
|
||||
self._instantiate_agent(serena_config, modes_instances)
|
||||
|
||||
except Exception as e:
|
||||
show_fatal_exception_safe(e)
|
||||
raise
|
||||
|
||||
# Override model_config to disable the use of `.env` files for reading settings, because user projects are likely to contain
|
||||
# `.env` files (e.g. containing LOG_LEVEL) that are not supposed to override the MCP settings;
|
||||
# retain only FASTMCP_ prefix for already set environment variables.
|
||||
Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_")
|
||||
|
||||
mcp_settings = Settings(lifespan=self.server_lifespan, host=host, port=port)
|
||||
mcp = FastMCP(**mcp_settings.model_dump())
|
||||
return mcp
|
||||
|
||||
@asynccontextmanager
|
||||
async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]:
|
||||
"""Manage server startup and shutdown lifecycle."""
|
||||
|
||||
def signal_handler(signum: int, frame: Any) -> None:
|
||||
log.info(f"Received signal {signum} in main process")
|
||||
request_global_shutdown()
|
||||
|
||||
def force_exit() -> None:
|
||||
time.sleep(2.0) # Wait 2 seconds for graceful shutdown
|
||||
log.warning("Forcing exit after timeout")
|
||||
# noinspection PyProtectedMember
|
||||
# noinspection PyUnresolvedReferences
|
||||
os._exit(1)
|
||||
|
||||
threading.Thread(target=force_exit, daemon=True).start()
|
||||
|
||||
# Install signal handlers
|
||||
sigint_singal = signal.signal(signal.SIGINT, signal_handler)
|
||||
sigterm_signal = signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
if self.serena_dashboard_process is not None:
|
||||
log.info("Starting dashboard process")
|
||||
assert self.serena_dashboard_process is not None
|
||||
self.serena_dashboard_process.start()
|
||||
log.info("Starting serena agent process")
|
||||
assert self.serena_agent_process is not None
|
||||
self.serena_agent_process.start()
|
||||
|
||||
self._set_mcp_tools(mcp_server)
|
||||
|
||||
async def monitor_global_shutdown() -> None:
|
||||
"""Monitor the global shutdown event and trigger local shutdown."""
|
||||
while not global_shutdown_event.is_set():
|
||||
# Poll the multiprocessing Event in async context
|
||||
await asyncio.sleep(0.1)
|
||||
continue
|
||||
log.info("Global shutdown event detected, initiating server shutdown")
|
||||
request_global_shutdown()
|
||||
# Send SIGTERM to self to trigger graceful shutdown
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
|
||||
# Start monitoring task
|
||||
monitor_task = asyncio.create_task(monitor_global_shutdown())
|
||||
|
||||
log.info("MCP server lifetime setup complete")
|
||||
try:
|
||||
yield
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
log.info("Received shutdown signal")
|
||||
request_global_shutdown()
|
||||
except Exception as e:
|
||||
log.error(f"Error in server lifespan: {e}")
|
||||
request_global_shutdown()
|
||||
finally:
|
||||
# Cancel monitor task
|
||||
monitor_task.cancel()
|
||||
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await monitor_task
|
||||
|
||||
self.serena_agent_process.stop()
|
||||
if self.serena_dashboard_process is not None:
|
||||
self.serena_dashboard_process.stop()
|
||||
request_global_shutdown()
|
||||
log.info("Shutting down all processes")
|
||||
signal.signal(signal.SIGINT, sigint_singal)
|
||||
signal.signal(signal.SIGTERM, sigterm_signal)
|
||||
|
||||
|
||||
class ProjectType(click.ParamType):
|
||||
name = "[PROJECT_NAME|PROJECT_PATH]"
|
||||
|
||||
@@ -588,13 +350,7 @@ def start_mcp_server(
|
||||
# This is for backward compatibility with the old CLI, should be removed in the future!
|
||||
project_file = project_file_arg if project_file_arg is not None else project
|
||||
|
||||
mcp_factory: SerenaMCPFactory
|
||||
if not USE_PROCESS_ISOLATION:
|
||||
mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file)
|
||||
else:
|
||||
mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file)
|
||||
|
||||
# Use process isolation by default to prevent asyncio event loop contamination
|
||||
mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file)
|
||||
mcp_server = mcp_factory.create_mcp_server(
|
||||
host=host,
|
||||
port=port,
|
||||
|
||||
@@ -1,571 +0,0 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import threading
|
||||
import traceback
|
||||
import webbrowser
|
||||
from enum import StrEnum
|
||||
from logging.handlers import QueueHandler
|
||||
from multiprocessing.connection import Connection
|
||||
from multiprocessing.sharedctypes import Synchronized
|
||||
from multiprocessing.synchronize import Event as EventClass
|
||||
from typing import Any, Literal, Self
|
||||
|
||||
from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata
|
||||
|
||||
from serena.agent import SerenaAgent, SerenaConfig, SerenaConfigBase, Tool, ToolInterface, ToolRegistry
|
||||
from serena.config import SerenaAgentContext, SerenaAgentMode
|
||||
from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Global synchronization primitives
|
||||
_global_log_queue: multiprocessing.Queue = multiprocessing.Queue()
|
||||
_dashboard_ready_event = multiprocessing.Event()
|
||||
_dashboard_port_value = multiprocessing.Value("i", 0)
|
||||
global_shutdown_event = multiprocessing.Event()
|
||||
|
||||
|
||||
def request_global_shutdown() -> None:
|
||||
"""Signal the global shutdown event."""
|
||||
global_shutdown_event.set()
|
||||
log.info("Global shutdown event set")
|
||||
|
||||
|
||||
def _dashboard_worker(
|
||||
tool_names: list[str],
|
||||
log_q: "multiprocessing.Queue[Any]",
|
||||
dashboard_ready_event: EventClass,
|
||||
port_value: "Synchronized[int]",
|
||||
shutdown_evt: EventClass,
|
||||
) -> None:
|
||||
"""Entry point for the dashboard process."""
|
||||
# Route all logging to the shared queue
|
||||
root = logging.getLogger()
|
||||
root.handlers.clear()
|
||||
root.setLevel(logging.DEBUG)
|
||||
root.addHandler(QueueHandler(log_q))
|
||||
|
||||
async def _process_logs(api: SerenaDashboardAPI) -> None:
|
||||
while not shutdown_evt.is_set():
|
||||
while not log_q.empty():
|
||||
record = log_q.get_nowait()
|
||||
if record is None:
|
||||
break
|
||||
api.memory_log_handler.emit(record)
|
||||
# Small delay to avoid busy waiting
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def _monitor_shutdown() -> None:
|
||||
# Poll the multiprocessing Event in async context
|
||||
# Check every 100ms until shutdown is requested
|
||||
loop = asyncio.get_event_loop()
|
||||
while True:
|
||||
# Check in executor to avoid blocking
|
||||
result = await loop.run_in_executor(None, shutdown_evt.is_set)
|
||||
if result:
|
||||
break
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def _async_main() -> None:
|
||||
api = SerenaDashboardAPI(
|
||||
memory_log_handler=MemoryLogHandler(),
|
||||
tool_names=tool_names,
|
||||
shutdown_callback=shutdown_evt.set,
|
||||
)
|
||||
# Pick a free port and signal readiness
|
||||
port = api._find_first_free_port(0x5EDA)
|
||||
port_value.value = port
|
||||
|
||||
# Start Flask server in a thread
|
||||
def run_flask_server() -> None:
|
||||
api._app.run(host="0.0.0.0", port=port, debug=False, use_reloader=False, threaded=True)
|
||||
|
||||
server_thread = threading.Thread(target=run_flask_server, daemon=True)
|
||||
server_thread.start()
|
||||
|
||||
shutdown_task = asyncio.create_task(_monitor_shutdown())
|
||||
logging_loop_task = asyncio.create_task(_process_logs(api))
|
||||
dashboard_ready_event.set()
|
||||
|
||||
done, pending = await asyncio.wait(
|
||||
[shutdown_task, logging_loop_task],
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
|
||||
# Cancel remaining tasks
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
try:
|
||||
asyncio.run(_async_main())
|
||||
except BaseException:
|
||||
logging.exception("Dashboard worker crashed")
|
||||
finally:
|
||||
logging.info("Dashboard worker exiting")
|
||||
|
||||
|
||||
def _shutdown_process(proc: multiprocessing.Process, timeout: float = 1.0) -> None:
|
||||
"""Helper to shutdown a process gracefully."""
|
||||
if proc.is_alive():
|
||||
proc.terminate()
|
||||
proc.join(timeout=timeout)
|
||||
if proc.is_alive():
|
||||
log.error("Process did not terminate gracefully, forcing kill")
|
||||
proc.kill()
|
||||
proc.join(timeout=timeout)
|
||||
if proc.is_alive():
|
||||
log.error("Process did not respond to kill")
|
||||
|
||||
|
||||
class ProcessIsolatedDashboard:
|
||||
"""Dashboard running in a separate process to avoid asyncio contamination."""
|
||||
|
||||
def __init__(self, tool_names: list[str]):
|
||||
self.tool_names = tool_names
|
||||
self.process: multiprocessing.Process | None = None
|
||||
|
||||
def start(self, timeout: float = 10.0) -> None:
|
||||
"""Start the dashboard process."""
|
||||
if self.process is not None:
|
||||
raise RuntimeError("Dashboard already started")
|
||||
|
||||
self.process = multiprocessing.Process(
|
||||
target=_dashboard_worker,
|
||||
args=(self.tool_names, _global_log_queue, _dashboard_ready_event, _dashboard_port_value, global_shutdown_event),
|
||||
daemon=True,
|
||||
)
|
||||
self.process.start()
|
||||
|
||||
if not _dashboard_ready_event.wait(timeout):
|
||||
self.process.terminate()
|
||||
self.process.join(timeout=1.0)
|
||||
self.process = None
|
||||
raise RuntimeError("Dashboard failed to start within timeout")
|
||||
|
||||
port = _dashboard_port_value.value
|
||||
log.info(f"Dashboard started on port {port}")
|
||||
webbrowser.open(f"http://localhost:{port}/dashboard/index.html")
|
||||
|
||||
def stop(self, timeout: float = 1.0) -> None:
|
||||
"""Signal shutdown and wait for the dashboard process to exit."""
|
||||
if self.process is None:
|
||||
return
|
||||
|
||||
log.info("Stopping dashboard process")
|
||||
request_global_shutdown()
|
||||
_shutdown_process(self.process, timeout=timeout)
|
||||
self.process = None
|
||||
|
||||
|
||||
class SerenaAgentWorker:
|
||||
"""Worker process that hosts the actual SerenaAgent."""
|
||||
|
||||
class RequestMethod(StrEnum):
|
||||
INITIALIZE = "initialize"
|
||||
TOOL_CALL = "tool_call"
|
||||
GET_ACTIVE_TOOL_NAMES = "get_active_tool_names"
|
||||
IS_LANGUAGE_SERVER_RUNNING = "is_language_server_running"
|
||||
RESET_LANGUAGE_SERVER = "reset_language_server"
|
||||
GET_EXPOSED_TOOL_NAMES = "get_exposed_tool_names"
|
||||
SHUTDOWN = "shutdown"
|
||||
|
||||
def __init__(self, conn: Connection):
|
||||
self.conn = conn
|
||||
self.agent: SerenaAgent | None = None
|
||||
|
||||
def run(self, log_queue: "multiprocessing.Queue[str]") -> None:
|
||||
"""Main worker loop - runs in separate process."""
|
||||
qh = QueueHandler(log_queue)
|
||||
root = logging.getLogger()
|
||||
root.setLevel(logging.DEBUG)
|
||||
root.addHandler(qh)
|
||||
|
||||
log.info("SerenaAgent worker process started")
|
||||
try:
|
||||
while not global_shutdown_event.is_set():
|
||||
try:
|
||||
# Use polling to avoid blocking indefinitely
|
||||
if self.conn.poll(timeout=0.5): # Poll every 500ms
|
||||
try:
|
||||
request = self.conn.recv()
|
||||
if request is None: # Explicit shutdown signal
|
||||
break
|
||||
|
||||
response = self._handle_request(request)
|
||||
self.conn.send(response)
|
||||
except EOFError:
|
||||
# Connection closed - parent process terminated
|
||||
log.info("Connection closed, worker shutting down")
|
||||
break
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
# Connection broken, can't communicate
|
||||
log.info("Connection broken, worker shutting down")
|
||||
break
|
||||
except Exception as e:
|
||||
log.error(f"Error processing request: {e}")
|
||||
try:
|
||||
response = {
|
||||
"error": f"Worker process error: {e!s}",
|
||||
"traceback": traceback.format_exc(),
|
||||
}
|
||||
self.conn.send(response)
|
||||
except (EOFError, BrokenPipeError, ConnectionResetError):
|
||||
# Connection is broken, can't send error response
|
||||
log.info("Connection broken during error response, shutting down")
|
||||
break
|
||||
# Continue polling if no data available
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error in worker main loop: {e}")
|
||||
break
|
||||
except Exception as e:
|
||||
log.error(f"Fatal error in worker process: {e}")
|
||||
finally:
|
||||
self._cleanup()
|
||||
log.info("SerenaAgent worker process stopped")
|
||||
os._exit(0) # Exit without raising any further exceptions
|
||||
|
||||
def _handle_request(self, request: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Handle a single request."""
|
||||
try:
|
||||
method = request["method"]
|
||||
params = request.get("params", {})
|
||||
|
||||
match method:
|
||||
case self.RequestMethod.INITIALIZE:
|
||||
return self._initialize(params)
|
||||
case self.RequestMethod.TOOL_CALL:
|
||||
return self._tool_call(params)
|
||||
case self.RequestMethod.GET_ACTIVE_TOOL_NAMES:
|
||||
return self._get_active_tool_names()
|
||||
case self.RequestMethod.IS_LANGUAGE_SERVER_RUNNING:
|
||||
return self._is_language_server_running()
|
||||
case self.RequestMethod.RESET_LANGUAGE_SERVER:
|
||||
return self._reset_language_server()
|
||||
case self.RequestMethod.GET_EXPOSED_TOOL_NAMES:
|
||||
return self._get_exposed_tool_names()
|
||||
case self.RequestMethod.SHUTDOWN:
|
||||
return self.shutdown()
|
||||
case _:
|
||||
return {"error": f"Unknown method: {method}"}
|
||||
except Exception as e:
|
||||
return {"error": str(e), "traceback": traceback.format_exc()}
|
||||
|
||||
def _initialize(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Initialize the SerenaAgent."""
|
||||
if self.agent is not None:
|
||||
return {"result": "SerenaAgent already initialized"}
|
||||
try:
|
||||
# Extract all possible initialization parameters
|
||||
context_param = params.get("context")
|
||||
project = params.get("project")
|
||||
serena_config = SerenaConfig.from_json_dict(params["serena_config"])
|
||||
context = SerenaAgentContext.from_json_dict(context_param) if context_param is not None else None
|
||||
modes = [SerenaAgentMode.from_json_dict(m) for m in params["modes"]]
|
||||
log_level = params.get("log_level")
|
||||
trace_lsp_communication = params.get("trace_lsp_communication")
|
||||
tool_timeout = params.get("tool_timeout")
|
||||
|
||||
self.agent = SerenaAgent(
|
||||
project=project,
|
||||
serena_config=serena_config,
|
||||
context=context,
|
||||
modes=modes,
|
||||
enable_web_dashboard=False,
|
||||
enable_gui_log_window=False,
|
||||
log_level=log_level,
|
||||
trace_lsp_communication=trace_lsp_communication,
|
||||
tool_timeout=tool_timeout,
|
||||
)
|
||||
return {"result": "SerenaAgent initialized successfully"}
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to initialize SerenaAgent: {e!s}", "traceback": traceback.format_exc()}
|
||||
|
||||
def _tool_call(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Execute a tool call."""
|
||||
if self.agent is None:
|
||||
return {"error": "SerenaAgent not initialized"}
|
||||
|
||||
try:
|
||||
tool_name = params["tool_name"]
|
||||
tool_params = params["tool_params"]
|
||||
|
||||
# Get the tool by name
|
||||
tool = None
|
||||
for tool_instance in self.agent._active_tools.values():
|
||||
if tool_instance.get_name_from_cls() == tool_name:
|
||||
tool = tool_instance
|
||||
break
|
||||
|
||||
if tool is None:
|
||||
return {"error": f"Tool '{tool_name}' not found or not active"}
|
||||
|
||||
# Execute the tool
|
||||
result = tool.apply_ex(**tool_params)
|
||||
|
||||
return {"result": result}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": str(e), "traceback": traceback.format_exc()}
|
||||
|
||||
def _get_active_tool_names(self) -> dict[str, Any]:
|
||||
"""Get list of active tool names."""
|
||||
if self.agent is None:
|
||||
return {"error": "SerenaAgent not initialized"}
|
||||
|
||||
try:
|
||||
tool_names = self.agent.get_active_tool_names()
|
||||
return {"result": tool_names}
|
||||
except Exception as e:
|
||||
return {"error": str(e), "traceback": traceback.format_exc()}
|
||||
|
||||
def _is_language_server_running(self) -> dict[str, Any]:
|
||||
"""Check if language server is running."""
|
||||
if self.agent is None:
|
||||
return {"error": "SerenaAgent not initialized"}
|
||||
|
||||
try:
|
||||
is_running = self.agent.is_language_server_running()
|
||||
return {"result": is_running}
|
||||
except Exception as e:
|
||||
return {"error": str(e), "traceback": traceback.format_exc()}
|
||||
|
||||
def _reset_language_server(self) -> dict[str, Any]:
|
||||
"""Reset the language server."""
|
||||
if self.agent is None:
|
||||
return {"error": "SerenaAgent not initialized"}
|
||||
|
||||
try:
|
||||
self.agent.reset_language_server()
|
||||
return {"result": "Language server reset successfully"}
|
||||
except Exception as e:
|
||||
return {"error": str(e), "traceback": traceback.format_exc()}
|
||||
|
||||
def _get_exposed_tool_names(self) -> dict[str, Any]:
|
||||
"""Get exposed tool names for MCP tool creation."""
|
||||
if self.agent is None:
|
||||
return {"error": "SerenaAgent not initialized"}
|
||||
|
||||
try:
|
||||
tool_instances = self.agent.get_exposed_tool_instances()
|
||||
# Return only tool names - metadata will be reconstructed from ToolRegistry
|
||||
tool_names = [tool.get_name_from_cls() for tool in tool_instances]
|
||||
return {"result": tool_names}
|
||||
except Exception as e:
|
||||
return {"error": str(e), "traceback": traceback.format_exc()}
|
||||
|
||||
def shutdown(self) -> dict[str, Any]:
|
||||
try:
|
||||
log.info("Shutting down SerenaAgent worker process on request")
|
||||
self._cleanup()
|
||||
request_global_shutdown()
|
||||
# Return successful response before exiting
|
||||
return {"result": "Shutdown initiated"}
|
||||
except Exception as e:
|
||||
log.error(f"Error during shutdown: {e}")
|
||||
return {"error": str(e), "traceback": traceback.format_exc()}
|
||||
|
||||
def _cleanup(self) -> None:
|
||||
"""Clean up resources."""
|
||||
if self.agent is not None:
|
||||
try:
|
||||
if self.agent.is_language_server_running() and self.agent.language_server is not None:
|
||||
self.agent.language_server.stop()
|
||||
except Exception as e:
|
||||
log.error(f"Error stopping language server: {e}")
|
||||
self.agent = None
|
||||
|
||||
|
||||
class ProcessIsolatedSerenaAgent:
|
||||
"""Process-isolated wrapper for SerenaAgent that prevents asyncio contamination."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
project: str | None = None,
|
||||
serena_config: SerenaConfigBase | None = None,
|
||||
context: SerenaAgentContext | None = None,
|
||||
modes: list[SerenaAgentMode] | None = None,
|
||||
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None,
|
||||
trace_lsp_communication: bool | None = None,
|
||||
tool_timeout: float | None = None,
|
||||
):
|
||||
self.project = project
|
||||
self.serena_config = serena_config or SerenaConfig.from_config_file()
|
||||
self.context = context
|
||||
self.modes = modes or []
|
||||
self.log_level = log_level
|
||||
self.trace_lsp_communication = trace_lsp_communication
|
||||
self.tool_timeout = tool_timeout
|
||||
|
||||
self.process: multiprocessing.Process | None = None
|
||||
self.conn: Connection | None = None
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start the worker process."""
|
||||
if self.process is not None:
|
||||
raise RuntimeError("ProcessIsolatedSerenaAgent already started")
|
||||
|
||||
log.info("Starting process-isolated SerenaAgent")
|
||||
|
||||
# Create communication pipe
|
||||
parent_conn, child_conn = multiprocessing.Pipe()
|
||||
self.conn = parent_conn # type: ignore
|
||||
|
||||
# Create and start worker process, passing along the dashboard's queue if available
|
||||
worker = SerenaAgentWorker(child_conn) # type: ignore
|
||||
self.process = multiprocessing.Process(target=worker.run, args=[_global_log_queue])
|
||||
self.process.start()
|
||||
|
||||
# Prepare initialization parameters, converting complex objects to dict if present
|
||||
init_params = {
|
||||
"project": self.project,
|
||||
"serena_config": self.serena_config.to_json_dict(),
|
||||
"context": self.context.to_json_dict() if self.context is not None else None,
|
||||
"modes": [m.to_json_dict() for m in self.modes],
|
||||
"log_level": self.log_level,
|
||||
"trace_lsp_communication": self.trace_lsp_communication,
|
||||
"tool_timeout": self.tool_timeout,
|
||||
}
|
||||
# Initialize the agent in the worker process
|
||||
try:
|
||||
self._make_request_with_result(SerenaAgentWorker.RequestMethod.INITIALIZE, init_params)
|
||||
except Exception as e:
|
||||
self.stop()
|
||||
raise RuntimeError(f"Failed to initialize SerenaAgent: {e}") from e
|
||||
|
||||
log.info("Process-isolated SerenaAgent started successfully")
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop the worker process."""
|
||||
if self.process is None:
|
||||
return
|
||||
log.info("Stopping SerenaAgent process")
|
||||
try:
|
||||
# Close connection to signal worker to shutdown
|
||||
if self.conn is not None:
|
||||
self.conn.close()
|
||||
_shutdown_process(self.process, timeout=2.0)
|
||||
self.process = None
|
||||
except KeyboardInterrupt:
|
||||
log.warning("Keyboard interrupt during shutdown - forcing termination")
|
||||
if self.process and self.process.is_alive():
|
||||
self.process.kill()
|
||||
self.process.join(timeout=1.0)
|
||||
except Exception as e:
|
||||
log.error(f"Error stopping worker process: {e}")
|
||||
finally:
|
||||
self.process = None
|
||||
self.conn = None
|
||||
|
||||
log.info("SerenaAgent stopped")
|
||||
|
||||
def _make_request(self, method: SerenaAgentWorker.RequestMethod, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""Make a request to the worker process."""
|
||||
if self.process is None or not self.process.is_alive():
|
||||
raise RuntimeError("Worker process is not running")
|
||||
|
||||
if self.conn is None:
|
||||
raise RuntimeError("Connection is not initialized")
|
||||
|
||||
request = {"method": method, "params": params or {}}
|
||||
|
||||
# Send request
|
||||
try:
|
||||
self.conn.send(request)
|
||||
except (EOFError, BrokenPipeError) as e:
|
||||
raise RuntimeError("Failed to send request: worker process may have crashed") from e
|
||||
|
||||
# Wait for response with timeout
|
||||
timeout = self.serena_config.tool_timeout
|
||||
if self.conn.poll(timeout):
|
||||
try:
|
||||
return self.conn.recv()
|
||||
except (EOFError, BrokenPipeError) as e:
|
||||
raise RuntimeError("Failed to receive response: worker process may have crashed") from e
|
||||
else:
|
||||
raise TimeoutError(f"Request {method} timed out after {timeout} seconds")
|
||||
|
||||
def _make_request_with_result(self, method: SerenaAgentWorker.RequestMethod, params: dict[str, Any] | None = None) -> Any:
|
||||
"""Make a request and return the result, raising an exception if there's an error."""
|
||||
response = self._make_request(method, params)
|
||||
if "error" in response:
|
||||
raise RuntimeError(f"Request {method} failed: {response['error']}")
|
||||
return response["result"]
|
||||
|
||||
def tool_call(self, tool_name: str, **tool_params: Any) -> str:
|
||||
"""Call a tool in the worker process."""
|
||||
return self._make_request_with_result(
|
||||
SerenaAgentWorker.RequestMethod.TOOL_CALL, {"tool_name": tool_name, "tool_params": tool_params}
|
||||
)
|
||||
|
||||
def get_tool(self, tool_cls: type[Tool]) -> "ProcessIsolatedTool":
|
||||
"""Get a process-isolated tool that delegates to this agent."""
|
||||
tool_name = tool_cls.get_name_from_cls()
|
||||
return ProcessIsolatedTool(self, tool_name)
|
||||
|
||||
def get_active_tool_names(self) -> list[str]:
|
||||
"""Get list of active tool names."""
|
||||
return self._make_request_with_result(SerenaAgentWorker.RequestMethod.GET_ACTIVE_TOOL_NAMES)
|
||||
|
||||
def is_language_server_running(self) -> bool:
|
||||
"""Check if language server is running."""
|
||||
return self._make_request_with_result(SerenaAgentWorker.RequestMethod.IS_LANGUAGE_SERVER_RUNNING)
|
||||
|
||||
def reset_language_server(self) -> None:
|
||||
"""Reset the language server."""
|
||||
self._make_request_with_result(SerenaAgentWorker.RequestMethod.RESET_LANGUAGE_SERVER)
|
||||
|
||||
def shutdown_from_dashboard(self) -> None:
|
||||
"""Request shutdown from dashboard."""
|
||||
self._make_request_with_result(SerenaAgentWorker.RequestMethod.SHUTDOWN)
|
||||
|
||||
def get_exposed_tool_names(self) -> list[str]:
|
||||
"""Get tool names for MCP tool creation."""
|
||||
return self._make_request_with_result(SerenaAgentWorker.RequestMethod.GET_EXPOSED_TOOL_NAMES)
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
self.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
self.stop()
|
||||
|
||||
|
||||
class ProcessIsolatedTool(ToolInterface):
|
||||
"""A clean tool wrapper that delegates to ProcessIsolatedSerenaAgent."""
|
||||
|
||||
def __init__(self, process_agent: ProcessIsolatedSerenaAgent, tool_name: str):
|
||||
self.process_agent = process_agent
|
||||
self._tool_name = tool_name
|
||||
|
||||
@property
|
||||
def _tool_class(self) -> type[Tool]:
|
||||
return ToolRegistry.get_tool_class_by_name(self._tool_name)
|
||||
|
||||
def get_name(self) -> str:
|
||||
"""Get the tool name for this process-isolated tool."""
|
||||
return self._tool_name
|
||||
|
||||
def get_apply_docstring(self) -> str:
|
||||
"""Get the docstring for the apply method."""
|
||||
# in the actual tool, this is a classmethod
|
||||
return self._tool_class.get_apply_docstring_from_cls()
|
||||
|
||||
def get_apply_fn_metadata(self) -> FuncMetadata:
|
||||
"""Get the metadata for the apply method."""
|
||||
# in the actual tool, this is a classmethod
|
||||
return self._tool_class.get_apply_fn_metadata_from_cls()
|
||||
|
||||
def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, **kwargs: Any) -> str:
|
||||
"""Apply the tool with logging and exception handling."""
|
||||
try:
|
||||
return self.process_agent.tool_call(self._tool_name, **kwargs)
|
||||
except Exception as e:
|
||||
if catch_exceptions:
|
||||
return f"Error executing tool {self._tool_name}: {e!s}"
|
||||
raise
|
||||
@@ -20,6 +20,12 @@ web_dashboard: True
|
||||
# shows Serena's current session logs - as an alternative to the GUI log window which
|
||||
# is supported on all platforms.
|
||||
|
||||
web_dashboard_open_on_launch: True
|
||||
# whether to open a browser window with the web dashboard when Serena starts (provided that web_dashboard
|
||||
# is enabled). If set to False, you can still open the dashboard manually by navigating to
|
||||
# http://localhost:24282/dashboard/ in your web browser.
|
||||
# If you have multiple instances running, a higher port will be used; try port 24283, 24284, etc.
|
||||
|
||||
log_level: 20
|
||||
# the minimum log level for the GUI log window and the dashboard (10 = debug, 20 = info, 30 = warning, 40 = error)
|
||||
|
||||
|
||||
+17
-4
@@ -189,8 +189,11 @@ class SymbolLocation:
|
||||
if self.relative_path is not None:
|
||||
self.relative_path = self.relative_path.replace("/", os.path.sep)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
def to_dict(self, include_relative_path: bool = True) -> dict[str, Any]:
|
||||
result = asdict(self)
|
||||
if not include_relative_path:
|
||||
result.pop("relative_path", None)
|
||||
return result
|
||||
|
||||
def has_position_in_file(self) -> bool:
|
||||
return self.relative_path is not None and self.line is not None and self.column is not None
|
||||
@@ -426,7 +429,13 @@ class Symbol(ToStringMixin):
|
||||
return result
|
||||
|
||||
def to_dict(
|
||||
self, kind: bool = False, location: bool = False, depth: int = 0, include_body: bool = False, include_children_body: bool = False
|
||||
self,
|
||||
kind: bool = False,
|
||||
location: bool = False,
|
||||
depth: int = 0,
|
||||
include_body: bool = False,
|
||||
include_children_body: bool = False,
|
||||
include_relative_path=True,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Converts the symbol to a dictionary.
|
||||
@@ -439,6 +448,8 @@ class Symbol(ToStringMixin):
|
||||
Note that the body of the children is part of the body of the parent symbol,
|
||||
so there is usually no need to set this to True unless you want process the output
|
||||
and pass the children without passing the parent body to the LM.
|
||||
:param include_relative_path: whether to include the relative path of the symbol in the location
|
||||
entry. Relative paths of the symbol's children are always excluded.
|
||||
:return: a dictionary representation of the symbol
|
||||
"""
|
||||
result: dict[str, Any] = {"name": self.name, "name_path": self.get_name_path()}
|
||||
@@ -447,7 +458,7 @@ class Symbol(ToStringMixin):
|
||||
result["kind"] = self.kind
|
||||
|
||||
if location:
|
||||
result["location"] = self.location.to_dict()
|
||||
result["location"] = self.location.to_dict(include_relative_path=include_relative_path)
|
||||
body_start_line, body_end_line = self.get_body_line_numbers()
|
||||
result["body_location"] = {"start_line": body_start_line, "end_line": body_end_line}
|
||||
|
||||
@@ -466,6 +477,8 @@ class Symbol(ToStringMixin):
|
||||
depth=depth - 1,
|
||||
include_body=include_children_body,
|
||||
include_children_body=include_children_body,
|
||||
# all children have the same relative path as the parent
|
||||
include_relative_path=False,
|
||||
)
|
||||
)
|
||||
return children
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# ruff: noqa
|
||||
from .tools_base import *
|
||||
from .file_tools import *
|
||||
from .ls_tools import *
|
||||
from .memory_tools import *
|
||||
from .cmd_tools import *
|
||||
from .config_tools import *
|
||||
from .workflow_tools import *
|
||||
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Tools supporting the execution of (external) commands
|
||||
"""
|
||||
|
||||
from serena.tools import TOOL_DEFAULT_MAX_ANSWER_LENGTH, Tool, ToolMarkerCanEdit
|
||||
from serena.util.shell import execute_shell_command
|
||||
|
||||
|
||||
class ExecuteShellCommandTool(Tool, ToolMarkerCanEdit):
|
||||
"""
|
||||
Executes a shell command.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
command: str,
|
||||
cwd: str | None = None,
|
||||
capture_stderr: bool = True,
|
||||
max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH,
|
||||
) -> str:
|
||||
"""
|
||||
Execute a shell command and return its output.
|
||||
|
||||
IMPORTANT: you should always consider the memory about suggested shell commands before using this tool.
|
||||
If this memory was not loaded in the current conversation, you should load it using the `read_memory` tool
|
||||
before using this tool.
|
||||
|
||||
You should have at least once looked at the suggested shell commands from the corresponding memory
|
||||
created during the onboarding process before using this tool.
|
||||
Never execute unsafe shell commands like `rm -rf /` or similar! Generally be very careful with deletions.
|
||||
|
||||
:param command: the shell command to execute
|
||||
:param cwd: the working directory to execute the command in. If None, the project root will be used.
|
||||
:param capture_stderr: whether to capture and return stderr output
|
||||
:param max_answer_chars: if the output is longer than this number of characters,
|
||||
no content will be returned. Don't adjust unless there is really no other way to get the content
|
||||
required for the task.
|
||||
:return: a JSON object containing the command's stdout and optionally stderr output
|
||||
"""
|
||||
_cwd = cwd or self.get_project_root()
|
||||
result = execute_shell_command(command, cwd=_cwd, capture_stderr=capture_stderr)
|
||||
result = result.json()
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
@@ -0,0 +1,89 @@
|
||||
import json
|
||||
|
||||
from serena.config.context_mode import SerenaAgentMode
|
||||
from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject
|
||||
|
||||
|
||||
class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
|
||||
"""
|
||||
Activates a project by name.
|
||||
"""
|
||||
|
||||
def apply(self, project: str) -> str:
|
||||
"""
|
||||
Activates the project with the given name.
|
||||
|
||||
:param project: the name of a registered project to activate or a path to a project directory
|
||||
"""
|
||||
from ..config.serena_config import ProjectConfig
|
||||
|
||||
active_project, new_project_generated, new_project_config_generated = self.agent.activate_project_from_path_or_name(project)
|
||||
if new_project_generated:
|
||||
result_str = (
|
||||
f"Created and activated a new project with name {active_project.project_name} at {active_project.project_root}, language: {active_project.project_config.language.value}. "
|
||||
+ "You can activate this project later by name."
|
||||
)
|
||||
else:
|
||||
result_str = f"Activated existing project with name {active_project.project_name} at {active_project.project_root}, language: {active_project.project_config.language.value}"
|
||||
if new_project_config_generated:
|
||||
result_str += (
|
||||
f"\nNote: A new project configuration was autogenerated because the given path did not contain a {ProjectConfig.SERENA_DEFAULT_PROJECT_FILE} file."
|
||||
+ f"You can now edit the project configuration in the file {active_project.path_to_project_yml()}. In particular, you may want to edit the project name and the initial prompt."
|
||||
)
|
||||
|
||||
if active_project.project_config.initial_prompt:
|
||||
result_str += f"\nAdditional project information:\n {active_project.project_config.initial_prompt}"
|
||||
result_str += (
|
||||
f"\nAvailable memories:\n {json.dumps(list(self.memories_manager.list_memories()))}"
|
||||
+ "You should not read these memories directly, but rather use the `read_memory` tool to read them later if needed for the task."
|
||||
)
|
||||
result_str += f"\nAvailable tools:\n {json.dumps(self.agent.get_active_tool_names())}"
|
||||
return result_str
|
||||
|
||||
|
||||
class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
|
||||
"""
|
||||
Removes a project from the Serena configuration.
|
||||
"""
|
||||
|
||||
def apply(self, project_name: str) -> str:
|
||||
"""
|
||||
Removes a project from the Serena configuration.
|
||||
|
||||
:param project_name: Name of the project to remove
|
||||
"""
|
||||
self.agent.serena_config.remove_project(project_name)
|
||||
return f"Successfully removed project '{project_name}' from configuration."
|
||||
|
||||
|
||||
class SwitchModesTool(Tool):
|
||||
"""
|
||||
Activates modes by providing a list of their names
|
||||
"""
|
||||
|
||||
def apply(self, modes: list[str]) -> str:
|
||||
"""
|
||||
Activates the desired modes, like ["editing", "interactive"] or ["planning", "one-shot"]
|
||||
|
||||
:param modes: the names of the modes to activate
|
||||
"""
|
||||
mode_instances = [SerenaAgentMode.load(mode) for mode in modes]
|
||||
self.agent.set_modes(mode_instances)
|
||||
|
||||
# Inform the Agent about the activated modes and the currently active tools
|
||||
result_str = f"Successfully activated modes: {', '.join([mode.name for mode in mode_instances])}" + "\n"
|
||||
result_str += "\n".join([mode_instance.prompt for mode_instance in mode_instances]) + "\n"
|
||||
result_str += f"Currently active tools: {', '.join(self.agent.get_active_tool_names())}"
|
||||
return result_str
|
||||
|
||||
|
||||
class GetCurrentConfigTool(Tool):
|
||||
"""
|
||||
Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
"""
|
||||
|
||||
def apply(self) -> str:
|
||||
"""
|
||||
Print the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
"""
|
||||
return self.agent.get_current_config_overview()
|
||||
@@ -0,0 +1,393 @@
|
||||
"""
|
||||
File and file system-related tools, specifically for
|
||||
* listing directory contents
|
||||
* reading files
|
||||
* creating files
|
||||
* editing at the file level
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from fnmatch import fnmatch
|
||||
from pathlib import Path
|
||||
|
||||
from serena.text_utils import search_files
|
||||
from serena.tools import SUCCESS_RESULT, TOOL_DEFAULT_MAX_ANSWER_LENGTH, EditedFileContext, Tool, ToolMarkerCanEdit
|
||||
from serena.util.file_system import scan_directory
|
||||
|
||||
|
||||
class ReadFileTool(Tool):
|
||||
"""
|
||||
Reads a file within the project directory.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self, relative_path: str, start_line: int = 0, end_line: int | None = None, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH
|
||||
) -> str:
|
||||
"""
|
||||
Reads the given file or a chunk of it. Generally, symbolic operations
|
||||
like find_symbol or find_referencing_symbols should be preferred if you know which symbols you are looking for.
|
||||
Reading the entire file is only recommended if there is no other way to get the content required for the task.
|
||||
|
||||
:param relative_path: the relative path to the file to read
|
||||
:param start_line: the 0-based index of the first line to be retrieved.
|
||||
:param end_line: the 0-based index of the last line to be retrieved (inclusive). If None, read until the end of the file.
|
||||
:param max_answer_chars: if the file (chunk) is longer than this number of characters,
|
||||
no content will be returned. Don't adjust unless there is really no other way to get the content
|
||||
required for the task.
|
||||
:return: the full text of the file at the given relative path
|
||||
"""
|
||||
self.agent.validate_relative_path(relative_path)
|
||||
|
||||
result = self.language_server.retrieve_full_file_content(relative_path)
|
||||
result_lines = result.splitlines()
|
||||
if end_line is None:
|
||||
result_lines = result_lines[start_line:]
|
||||
else:
|
||||
self.lines_read.add_lines_read(relative_path, (start_line, end_line))
|
||||
result_lines = result_lines[start_line : end_line + 1]
|
||||
result = "\n".join(result_lines)
|
||||
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
|
||||
|
||||
class CreateTextFileTool(Tool, ToolMarkerCanEdit):
|
||||
"""
|
||||
Creates/overwrites a file in the project directory.
|
||||
"""
|
||||
|
||||
def apply(self, relative_path: str, content: str) -> str:
|
||||
"""
|
||||
Write a new file (or overwrite an existing file). For existing files, it is strongly recommended
|
||||
to use symbolic operations like replace_symbol_body or insert_after_symbol/insert_before_symbol, if possible.
|
||||
You can also use insert_at_line to insert content at a specific line for existing files if the symbolic operations
|
||||
are not the right choice for what you want to do.
|
||||
|
||||
If ever used on an existing file, the content has to be the complete content of that file (so it
|
||||
may never end with something like "The remaining content of the file is left unchanged.").
|
||||
For operations that just replace a part of a file, use the replace_lines or the symbolic editing tools instead.
|
||||
|
||||
:param relative_path: the relative path to the file to create
|
||||
:param content: the (utf-8-encoded) content to write to the file
|
||||
:return: a message indicating success or failure
|
||||
"""
|
||||
self.agent.validate_relative_path(relative_path)
|
||||
|
||||
abs_path = (Path(self.get_project_root()) / relative_path).resolve()
|
||||
will_overwrite_existing = abs_path.exists()
|
||||
|
||||
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
abs_path.write_text(content, encoding="utf-8")
|
||||
answer = f"File created: {relative_path}."
|
||||
if will_overwrite_existing:
|
||||
answer += " Overwrote existing file."
|
||||
return answer
|
||||
|
||||
|
||||
class ListDirTool(Tool):
|
||||
"""
|
||||
Lists files and directories in the given directory (optionally with recursion).
|
||||
"""
|
||||
|
||||
def apply(self, relative_path: str, recursive: bool, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH) -> str:
|
||||
"""
|
||||
Lists all non-gitignored files and directories in the given directory (optionally with recursion).
|
||||
|
||||
:param relative_path: the relative path to the directory to list; pass "." to scan the project root
|
||||
:param recursive: whether to scan subdirectories recursively
|
||||
:param max_answer_chars: if the output is longer than this number of characters,
|
||||
no content will be returned. Don't adjust unless there is really no other way to get the content
|
||||
required for the task.
|
||||
:return: a JSON object with the names of directories and files within the given directory
|
||||
"""
|
||||
self.agent.validate_relative_path(relative_path)
|
||||
|
||||
dirs, files = scan_directory(
|
||||
os.path.join(self.get_project_root(), relative_path),
|
||||
relative_to=self.get_project_root(),
|
||||
recursive=recursive,
|
||||
is_ignored_dir=self.agent.path_is_gitignored,
|
||||
is_ignored_file=self.agent.path_is_gitignored,
|
||||
)
|
||||
|
||||
result = json.dumps({"dirs": dirs, "files": files})
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
|
||||
|
||||
class FindFileTool(Tool):
|
||||
"""
|
||||
Finds files in the given relative paths
|
||||
"""
|
||||
|
||||
def apply(self, file_mask: str, relative_path: str) -> str:
|
||||
"""
|
||||
Finds non-gitignored files matching the given file mask within the given relative path
|
||||
|
||||
:param file_mask: the filename or file mask (using the wildcards * or ?) to search for
|
||||
:param relative_path: the relative path to the directory to search in; pass "." to scan the project root
|
||||
:return: a JSON object with the list of matching files
|
||||
"""
|
||||
self.agent.validate_relative_path(relative_path)
|
||||
|
||||
dir_to_scan = os.path.join(self.get_project_root(), relative_path)
|
||||
|
||||
# find the files by ignoring everything that doesn't match
|
||||
def is_ignored_file(abs_path: str) -> bool:
|
||||
if self.agent.path_is_gitignored(abs_path):
|
||||
return True
|
||||
filename = os.path.basename(abs_path)
|
||||
return not fnmatch(filename, file_mask)
|
||||
|
||||
dirs, files = scan_directory(
|
||||
path=dir_to_scan,
|
||||
recursive=True,
|
||||
is_ignored_dir=self.agent.path_is_gitignored,
|
||||
is_ignored_file=is_ignored_file,
|
||||
relative_to=self.get_project_root(),
|
||||
)
|
||||
|
||||
result = json.dumps({"files": files})
|
||||
return result
|
||||
|
||||
|
||||
class ReplaceRegexTool(Tool, ToolMarkerCanEdit):
|
||||
"""
|
||||
Replaces content in a file by using regular expressions.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
relative_path: str,
|
||||
regex: str,
|
||||
repl: str,
|
||||
allow_multiple_occurrences: bool = False,
|
||||
) -> str:
|
||||
r"""
|
||||
Replaces one or more occurrences of the given regular expression.
|
||||
This is the preferred way to replace content in a file whenever the symbol-level
|
||||
tools are not appropriate.
|
||||
Even large sections of code can be replaced by providing a concise regular expression of
|
||||
the form "beginning.*?end-of-text-to-be-replaced".
|
||||
Always try to use wildcards to avoid specifying the exact content of the code to be replaced,
|
||||
especially if it spans several lines.
|
||||
|
||||
IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD!
|
||||
|
||||
:param relative_path: the relative path to the file
|
||||
:param regex: a Python-style regular expression, matches of which will be replaced.
|
||||
Dot matches all characters, multi-line matching is enabled.
|
||||
:param repl: the string to replace the matched content with, which may contain
|
||||
backreferences like \1, \2, etc.
|
||||
:param allow_multiple_occurrences: if True, the regex may match multiple occurrences in the file
|
||||
and all of them will be replaced.
|
||||
If this is set to False and the regex matches multiple occurrences, an error will be returned
|
||||
(and you may retry with a revised, more specific regex).
|
||||
"""
|
||||
self.agent.validate_relative_path(relative_path)
|
||||
with EditedFileContext(relative_path, self.agent) as context:
|
||||
original_content = context.get_original_content()
|
||||
updated_content, n = re.subn(regex, repl, original_content, flags=re.DOTALL | re.MULTILINE)
|
||||
if n == 0:
|
||||
return f"Error: No matches found for regex '{regex}' in file '{relative_path}'."
|
||||
if not allow_multiple_occurrences and n > 1:
|
||||
return (
|
||||
f"Error: Regex '{regex}' matches {n} occurrences in file '{relative_path}'. "
|
||||
"Please revise the regex to be more specific or enable allow_multiple_occurrences if this is expected."
|
||||
)
|
||||
context.set_updated_content(updated_content)
|
||||
return SUCCESS_RESULT
|
||||
|
||||
|
||||
class DeleteLinesTool(Tool, ToolMarkerCanEdit):
|
||||
"""
|
||||
Deletes a range of lines within a file.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
relative_path: str,
|
||||
start_line: int,
|
||||
end_line: int,
|
||||
) -> str:
|
||||
"""
|
||||
Deletes the given lines in the file.
|
||||
Requires that the same range of lines was previously read using the `read_file` tool to verify correctness
|
||||
of the operation.
|
||||
|
||||
:param relative_path: the relative path to the file
|
||||
:param start_line: the 0-based index of the first line to be deleted
|
||||
:param end_line: the 0-based index of the last line to be deleted
|
||||
"""
|
||||
if not self.lines_read.were_lines_read(relative_path, (start_line, end_line)):
|
||||
read_lines_tool = self.agent.get_tool(ReadFileTool)
|
||||
return f"Error: Must call `{read_lines_tool.get_name_from_cls()}` first to read exactly the affected lines."
|
||||
self.symbol_manager.delete_lines(relative_path, start_line, end_line)
|
||||
return SUCCESS_RESULT
|
||||
|
||||
|
||||
class ReplaceLinesTool(Tool, ToolMarkerCanEdit):
|
||||
"""
|
||||
Replaces a range of lines within a file with new content.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
relative_path: str,
|
||||
start_line: int,
|
||||
end_line: int,
|
||||
content: str,
|
||||
) -> str:
|
||||
"""
|
||||
Replaces the given range of lines in the given file.
|
||||
Requires that the same range of lines was previously read using the `read_file` tool to verify correctness
|
||||
of the operation.
|
||||
|
||||
:param relative_path: the relative path to the file
|
||||
:param start_line: the 0-based index of the first line to be deleted
|
||||
:param end_line: the 0-based index of the last line to be deleted
|
||||
:param content: the content to insert
|
||||
"""
|
||||
if not content.endswith("\n"):
|
||||
content += "\n"
|
||||
result = self.agent.get_tool(DeleteLinesTool).apply(relative_path, start_line, end_line)
|
||||
if result != SUCCESS_RESULT:
|
||||
return result
|
||||
self.agent.get_tool(InsertAtLineTool).apply(relative_path, start_line, content)
|
||||
return SUCCESS_RESULT
|
||||
|
||||
|
||||
class InsertAtLineTool(Tool, ToolMarkerCanEdit):
|
||||
"""
|
||||
Inserts content at a given line in a file.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
relative_path: str,
|
||||
line: int,
|
||||
content: str,
|
||||
) -> str:
|
||||
"""
|
||||
Inserts the given content at the given line in the file, pushing existing content of the line down.
|
||||
In general, symbolic insert operations like insert_after_symbol or insert_before_symbol should be preferred if you know which
|
||||
symbol you are looking for.
|
||||
However, this can also be useful for small targeted edits of the body of a longer symbol (without replacing the entire body).
|
||||
|
||||
:param relative_path: the relative path to the file
|
||||
:param line: the 0-based index of the line to insert content at
|
||||
:param content: the content to be inserted
|
||||
"""
|
||||
if not content.endswith("\n"):
|
||||
content += "\n"
|
||||
self.symbol_manager.insert_at_line(relative_path, line, content)
|
||||
return SUCCESS_RESULT
|
||||
|
||||
|
||||
class SearchForPatternTool(Tool):
|
||||
"""
|
||||
Performs a search for a pattern in the project.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
substring_pattern: str,
|
||||
context_lines_before: int = 0,
|
||||
context_lines_after: int = 0,
|
||||
paths_include_glob: str | None = None,
|
||||
paths_exclude_glob: str | None = None,
|
||||
relative_path: str = "",
|
||||
restrict_search_to_code_files: bool = False,
|
||||
max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH,
|
||||
) -> str:
|
||||
"""
|
||||
Offers a flexible search for arbitrary patterns in the codebase, including the
|
||||
possibility to search in non-code files.
|
||||
Generally, symbolic operations like find_symbol or find_referencing_symbols
|
||||
should be preferred if you know which symbols you are looking for.
|
||||
|
||||
Pattern Matching Logic:
|
||||
For each match, the returned result will contain the full lines where the
|
||||
substring pattern is found, as well as optionally some lines before and after it. The pattern will be compiled with
|
||||
DOTALL, meaning that the dot will match all characters including newlines.
|
||||
This also means that it never makes sense to have .* at the beginning or end of the pattern,
|
||||
but it may make sense to have it in the middle for complex patterns.
|
||||
If a pattern matches multiple lines, all those lines will be part of the match.
|
||||
Be careful to not use greedy quantifiers unnecessarily, it is usually better to use non-greedy quantifiers like .*? to avoid
|
||||
matching too much content.
|
||||
|
||||
File Selection Logic:
|
||||
The files in which the search is performed can be restricted very flexibly.
|
||||
Using `restrict_search_to_code_files` is useful if you are only interested in code symbols (i.e., those
|
||||
symbols that can be manipulated with symbolic tools like find_symbol).
|
||||
You can also restrict the search to a specific file or directory,
|
||||
and provide glob patterns to include or exclude certain files on top of that.
|
||||
The globs are matched against relative file paths from the project root (not to the `relative_path` parameter that
|
||||
is used to further restrict the search).
|
||||
Smartly combining the various restrictions allows you to perform very targeted searches.
|
||||
|
||||
|
||||
:param substring_pattern: Regular expression for a substring pattern to search for
|
||||
:param context_lines_before: Number of lines of context to include before each match
|
||||
:param context_lines_after: Number of lines of context to include after each match
|
||||
:param paths_include_glob: optional glob pattern specifying files to include in the search.
|
||||
Matches against relative file paths from the project root (e.g., "*.py", "src/**/*.ts").
|
||||
Only matches files, not directories.
|
||||
:param paths_exclude_glob: optional glob pattern specifying files to exclude from the search.
|
||||
Matches against relative file paths from the project root (e.g., "*test*", "**/*_generated.py").
|
||||
Takes precedence over paths_include_glob. Only matches files, not directories.
|
||||
:param relative_path: only subpaths of this path (relative to the repo root) will be analyzed. If a path to a single
|
||||
file is passed, only that will be searched. The path must exist, otherwise a `FileNotFoundError` is raised.
|
||||
:param max_answer_chars: if the output is longer than this number of characters,
|
||||
no content will be returned. Don't adjust unless there is really no other way to get the content
|
||||
required for the task. Instead, if the output is too long, you should
|
||||
make a stricter query.
|
||||
:param restrict_search_to_code_files: whether to restrict the search to only those files where
|
||||
analyzed code symbols can be found. Otherwise, will search all non-ignored files.
|
||||
Set this to True if your search is only meant to discover code that can be manipulated with symbolic tools.
|
||||
For example, for finding classes or methods from a name pattern.
|
||||
Setting to False is a better choice if you also want to search in non-code files, like in html or yaml files,
|
||||
which is why it is the default.
|
||||
:return: A JSON object mapping file paths to lists of matched consecutive lines (with context, if requested).
|
||||
"""
|
||||
abs_path = os.path.join(self.get_project_root(), relative_path)
|
||||
if not os.path.exists(abs_path):
|
||||
raise FileNotFoundError(f"Relative path {relative_path} does not exist.")
|
||||
|
||||
if restrict_search_to_code_files:
|
||||
matches = self.language_server.search_files_for_pattern(
|
||||
pattern=substring_pattern,
|
||||
relative_path=relative_path,
|
||||
context_lines_before=context_lines_before,
|
||||
context_lines_after=context_lines_after,
|
||||
paths_include_glob=paths_include_glob,
|
||||
paths_exclude_glob=paths_exclude_glob,
|
||||
)
|
||||
else:
|
||||
if os.path.isfile(abs_path):
|
||||
rel_paths_to_search = [relative_path]
|
||||
else:
|
||||
dirs, rel_paths_to_search = scan_directory(
|
||||
path=abs_path,
|
||||
recursive=True,
|
||||
is_ignored_dir=self.agent.path_is_gitignored,
|
||||
is_ignored_file=self.agent.path_is_gitignored,
|
||||
relative_to=self.get_project_root(),
|
||||
)
|
||||
# TODO (maybe): not super efficient to walk through the files again and filter if glob patterns are provided
|
||||
# but it probably never matters and this version required no further refactoring
|
||||
matches = search_files(
|
||||
rel_paths_to_search,
|
||||
substring_pattern,
|
||||
root_path=self.get_project_root(),
|
||||
paths_include_glob=paths_include_glob,
|
||||
paths_exclude_glob=paths_exclude_glob,
|
||||
)
|
||||
# group matches by file
|
||||
file_to_matches: dict[str, list[str]] = defaultdict(list)
|
||||
for match in matches:
|
||||
assert match.source_file_path is not None
|
||||
file_to_matches[match.source_file_path].append(match.to_display_string())
|
||||
result = json.dumps(file_to_matches)
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
Language server-related tools
|
||||
"""
|
||||
|
||||
import json
|
||||
from collections.abc import Sequence
|
||||
from copy import copy
|
||||
from typing import Any
|
||||
|
||||
from serena.tools import SUCCESS_RESULT, TOOL_DEFAULT_MAX_ANSWER_LENGTH, Tool, ToolMarkerCanEdit
|
||||
from solidlsp.ls_types import SymbolKind
|
||||
|
||||
|
||||
def _sanitize_symbol_dict(symbol_dict: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
Sanitize a symbol dictionary inplace by removing unnecessary information.
|
||||
"""
|
||||
# We replace the location entry, which repeats line information already included in body_location
|
||||
# and has unnecessary information on column, by just the relative path.
|
||||
symbol_dict = copy(symbol_dict)
|
||||
s_relative_path = symbol_dict.get("location", {}).get("relative_path")
|
||||
if s_relative_path is not None:
|
||||
symbol_dict["relative_path"] = s_relative_path
|
||||
symbol_dict.pop("location", None)
|
||||
# also remove name, name_path should be enough
|
||||
symbol_dict.pop("name")
|
||||
return symbol_dict
|
||||
|
||||
|
||||
class RestartLanguageServerTool(Tool):
|
||||
"""Restarts the language server, may be necessary when edits not through Serena happen."""
|
||||
|
||||
def apply(self) -> str:
|
||||
"""Use this tool only on explicit user request or after confirmation.
|
||||
It may be necessary to restart the language server if the user performs edits
|
||||
not through Serena, so the language server state becomes outdated and further editing attempts lead to errors.
|
||||
|
||||
If such editing errors happen, you should suggest using this tool.
|
||||
"""
|
||||
self.agent.reset_language_server()
|
||||
return SUCCESS_RESULT
|
||||
|
||||
|
||||
class GetSymbolsOverviewTool(Tool):
|
||||
"""
|
||||
Gets an overview of the top-level symbols defined in a given file or directory.
|
||||
"""
|
||||
|
||||
def apply(self, relative_path: str, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH) -> str:
|
||||
"""
|
||||
Gets an overview of the given file or directory.
|
||||
For each analyzed file, we list the top-level symbols in the file (name_path, kind).
|
||||
Use this tool to get a high-level understanding of the code symbols.
|
||||
Calling this is often a good idea before more targeted reading, searching or editing operations on the code symbols.
|
||||
|
||||
:param relative_path: the relative path to the file or directory to get the overview of
|
||||
:param max_answer_chars: if the overview is longer than this number of characters,
|
||||
no content will be returned. Don't adjust unless there is really no other way to get the content
|
||||
required for the task. If the overview is too long, you should use a smaller directory instead,
|
||||
(e.g. a subdirectory).
|
||||
:return: a JSON object mapping relative paths of all contained files to info about top-level symbols in the file (name_path, kind).
|
||||
"""
|
||||
path_to_symbol_infos = self.language_server.request_overview(relative_path)
|
||||
result = {}
|
||||
for file_path, symbols in path_to_symbol_infos.items():
|
||||
# TODO: maybe include not just top-level symbols? We could filter by kind to exclude variables
|
||||
# The language server methods would need to be adjusted for this.
|
||||
result[file_path] = [{"name_path": symbol[0], "kind": int(symbol[1])} for symbol in symbols]
|
||||
|
||||
result_json_str = json.dumps(result)
|
||||
return self._limit_length(result_json_str, max_answer_chars)
|
||||
|
||||
|
||||
class FindSymbolTool(Tool):
|
||||
"""
|
||||
Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
name_path: str,
|
||||
depth: int = 0,
|
||||
relative_path: str | None = None,
|
||||
include_body: bool = False,
|
||||
include_kinds: list[int] | None = None,
|
||||
exclude_kinds: list[int] | None = None,
|
||||
substring_matching: bool = False,
|
||||
max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH,
|
||||
) -> str:
|
||||
"""
|
||||
Retrieves information on all symbols/code entities (classes, methods, etc.) based on the given `name_path`,
|
||||
which represents a pattern for the symbol's path within the symbol tree of a single file.
|
||||
The returned symbol location can be used for edits or further queries.
|
||||
Specify `depth > 0` to retrieve children (e.g., methods of a class).
|
||||
|
||||
The matching behavior is determined by the structure of `name_path`, which can
|
||||
either be a simple name (e.g. "method") or a name path like "class/method" (relative name path)
|
||||
or "/class/method" (absolute name path). Note that the name path is not a path in the file system
|
||||
but rather a path in the symbol tree **within a single file**. Thus, file or directory names should never
|
||||
be included in the `name_path`. For restricting the search to a single file or directory,
|
||||
the `within_relative_path` parameter should be used instead. The retrieved symbols' `name_path` attribute
|
||||
will always be composed of symbol names, never file or directory names.
|
||||
|
||||
Key aspects of the name path matching behavior:
|
||||
- Trailing slashes in `name_path` play no role and are ignored.
|
||||
- The name of the retrieved symbols will match (either exactly or as a substring)
|
||||
the last segment of `name_path`, while other segments will restrict the search to symbols that
|
||||
have a desired sequence of ancestors.
|
||||
- If there is no starting or intermediate slash in `name_path`, there is no
|
||||
restriction on the ancestor symbols. For example, passing `method` will match
|
||||
against symbols with name paths like `method`, `class/method`, `class/nested_class/method`, etc.
|
||||
- If `name_path` contains a `/` but doesn't start with a `/`, the matching is restricted to symbols
|
||||
with the same ancestors as the last segment of `name_path`. For example, passing `class/method` will match against
|
||||
`class/method` as well as `nested_class/class/method` but not `method`.
|
||||
- If `name_path` starts with a `/`, it will be treated as an absolute name path pattern, meaning
|
||||
that the first segment of it must match the first segment of the symbol's name path.
|
||||
For example, passing `/class` will match only against top-level symbols like `class` but not against `nested_class/class`.
|
||||
Passing `/class/method` will match against `class/method` but not `nested_class/class/method` or `method`.
|
||||
|
||||
|
||||
:param name_path: The name path pattern to search for, see above for details.
|
||||
:param depth: Depth to retrieve descendants (e.g., 1 for class methods/attributes).
|
||||
:param relative_path: Optional. Restrict search to this file or directory. If None, searches entire codebase.
|
||||
If a directory is passed, the search will be restricted to the files in that directory.
|
||||
If a file is passed, the search will be restricted to that file.
|
||||
If you have some knowledge about the codebase, you should use this parameter, as it will significantly
|
||||
speed up the search as well as reduce the number of results.
|
||||
:param include_body: If True, include the symbol's source code. Use judiciously.
|
||||
:param include_kinds: Optional. List of LSP symbol kind integers to include. (e.g., 5 for Class, 12 for Function).
|
||||
Valid kinds: 1=file, 2=module, 3=namespace, 4=package, 5=class, 6=method, 7=property, 8=field, 9=constructor, 10=enum,
|
||||
11=interface, 12=function, 13=variable, 14=constant, 15=string, 16=number, 17=boolean, 18=array, 19=object,
|
||||
20=key, 21=null, 22=enum member, 23=struct, 24=event, 25=operator, 26=type parameter
|
||||
:param exclude_kinds: Optional. List of LSP symbol kind integers to exclude. Takes precedence over `include_kinds`.
|
||||
:param substring_matching: If True, use substring matching for the last segment of `name`.
|
||||
:param max_answer_chars: Max characters for the JSON result. If exceeded, no content is returned.
|
||||
:return: JSON string: a list of symbols (with locations) matching the name.
|
||||
"""
|
||||
parsed_include_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in include_kinds] if include_kinds else None
|
||||
parsed_exclude_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in exclude_kinds] if exclude_kinds else None
|
||||
symbols = self.symbol_manager.find_by_name(
|
||||
name_path,
|
||||
include_body=include_body,
|
||||
include_kinds=parsed_include_kinds,
|
||||
exclude_kinds=parsed_exclude_kinds,
|
||||
substring_matching=substring_matching,
|
||||
within_relative_path=relative_path,
|
||||
)
|
||||
symbol_dicts = [_sanitize_symbol_dict(s.to_dict(kind=True, location=True, depth=depth, include_body=include_body)) for s in symbols]
|
||||
result = json.dumps(symbol_dicts)
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
|
||||
|
||||
class FindReferencingSymbolsTool(Tool):
|
||||
"""
|
||||
Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
name_path: str,
|
||||
relative_path: str,
|
||||
include_kinds: list[int] | None = None,
|
||||
exclude_kinds: list[int] | None = None,
|
||||
max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH,
|
||||
) -> str:
|
||||
"""
|
||||
Finds symbols that reference the symbol at the given `name_path`. The result will contain metadata about the referencing symbols
|
||||
as well as a short code snippet around the reference (unless `include_body` is True, then the short snippet will be omitted).
|
||||
Note that among other kinds of references, this function can be used to find (direct) subclasses of a class,
|
||||
as subclasses are referencing symbols that have the kind class.
|
||||
|
||||
:param name_path: for finding the symbol to find references for, same logic as in the `find_symbol` tool.
|
||||
:param relative_path: the relative path to the file containing the symbol for which to find references.
|
||||
Note that here you can't pass a directory but must pass a file.
|
||||
:param include_kinds: same as in the `find_symbol` tool.
|
||||
:param exclude_kinds: same as in the `find_symbol` tool.
|
||||
:param max_answer_chars: same as in the `find_symbol` tool.
|
||||
:return: a list of JSON objects with the symbols referencing the requested symbol
|
||||
"""
|
||||
include_body = False # It is probably never a good idea to include the body of the referencing symbols
|
||||
parsed_include_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in include_kinds] if include_kinds else None
|
||||
parsed_exclude_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in exclude_kinds] if exclude_kinds else None
|
||||
references_in_symbols = self.symbol_manager.find_referencing_symbols(
|
||||
name_path,
|
||||
relative_file_path=relative_path,
|
||||
include_body=include_body,
|
||||
include_kinds=parsed_include_kinds,
|
||||
exclude_kinds=parsed_exclude_kinds,
|
||||
)
|
||||
reference_dicts = []
|
||||
for ref in references_in_symbols:
|
||||
ref_dict = ref.symbol.to_dict(kind=True, location=True, depth=0, include_body=include_body)
|
||||
ref_dict = _sanitize_symbol_dict(ref_dict)
|
||||
if not include_body:
|
||||
ref_relative_path = ref.symbol.location.relative_path
|
||||
assert ref_relative_path is not None, f"Referencing symbol {ref.symbol.name} has no relative path, this is likely a bug."
|
||||
content_around_ref = self.language_server.retrieve_content_around_line(
|
||||
relative_file_path=ref_relative_path, line=ref.line, context_lines_before=1, context_lines_after=1
|
||||
)
|
||||
ref_dict["content_around_reference"] = content_around_ref.to_display_string()
|
||||
reference_dicts.append(ref_dict)
|
||||
result = json.dumps(reference_dicts)
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
|
||||
|
||||
class ReplaceSymbolBodyTool(Tool, ToolMarkerCanEdit):
|
||||
"""
|
||||
Replaces the full definition of a symbol.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
name_path: str,
|
||||
relative_path: str,
|
||||
body: str,
|
||||
) -> str:
|
||||
r"""
|
||||
Replaces the body of the symbol with the given `name_path`.
|
||||
|
||||
:param name_path: for finding the symbol to replace, same logic as in the `find_symbol` tool.
|
||||
:param relative_path: the relative path to the file containing the symbol
|
||||
:param body: the new symbol body. Important: Begin directly with the symbol definition and provide no
|
||||
leading indentation for the first line (but do indent the rest of the body according to the context).
|
||||
"""
|
||||
self.symbol_manager.replace_body(
|
||||
name_path,
|
||||
relative_file_path=relative_path,
|
||||
body=body,
|
||||
use_same_indentation=False,
|
||||
)
|
||||
return SUCCESS_RESULT
|
||||
|
||||
|
||||
class InsertAfterSymbolTool(Tool, ToolMarkerCanEdit):
|
||||
"""
|
||||
Inserts content after the end of the definition of a given symbol.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
name_path: str,
|
||||
relative_path: str,
|
||||
body: str,
|
||||
) -> str:
|
||||
"""
|
||||
Inserts the given body/content after the end of the definition of the given symbol (via the symbol's location).
|
||||
A typical use case is to insert a new class, function, method, field or variable assignment.
|
||||
|
||||
:param name_path: name path of the symbol after which to insert content (definitions in the `find_symbol` tool apply)
|
||||
:param relative_path: the relative path to the file containing the symbol
|
||||
:param body: the body/content to be inserted. The inserted code shall begin with the next line after
|
||||
the symbol.
|
||||
"""
|
||||
self.symbol_manager.insert_after_symbol(name_path, relative_file_path=relative_path, body=body, use_same_indentation=False)
|
||||
return SUCCESS_RESULT
|
||||
|
||||
|
||||
class InsertBeforeSymbolTool(Tool, ToolMarkerCanEdit):
|
||||
"""
|
||||
Inserts content before the beginning of the definition of a given symbol.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
name_path: str,
|
||||
relative_path: str,
|
||||
body: str,
|
||||
) -> str:
|
||||
"""
|
||||
Inserts the given body/content before the beginning of the definition of the given symbol (via the symbol's location).
|
||||
A typical use case is to insert a new class, function, method, field or variable assignment.
|
||||
It also can be used to insert a new import statement before the first symbol in the file.
|
||||
|
||||
:param name_path: name path of the symbol before which to insert content (definitions in the `find_symbol` tool apply)
|
||||
:param relative_path: the relative path to the file containing the symbol
|
||||
:param body: the body/content to be inserted before the line in which the referenced symbol is defined
|
||||
"""
|
||||
self.symbol_manager.insert_before_symbol(name_path, relative_file_path=relative_path, body=body, use_same_indentation=False)
|
||||
return SUCCESS_RESULT
|
||||
@@ -0,0 +1,69 @@
|
||||
import json
|
||||
|
||||
from serena.tools import TOOL_DEFAULT_MAX_ANSWER_LENGTH, Tool
|
||||
|
||||
|
||||
class WriteMemoryTool(Tool):
|
||||
"""
|
||||
Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
"""
|
||||
|
||||
def apply(self, memory_name: str, content: str, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH) -> str:
|
||||
"""
|
||||
Write some information about this project that can be useful for future tasks to a memory.
|
||||
Use markdown formatting for the content.
|
||||
The information should be short and to the point.
|
||||
The memory name should be meaningful, such that from the name you can infer what the information is about.
|
||||
It is better to have multiple small memories than to have a single large one because
|
||||
memories will be read one by one and we only ever want to read relevant memories.
|
||||
|
||||
This tool is either called during the onboarding process or when you have identified
|
||||
something worth remembering about the project from the past conversation.
|
||||
"""
|
||||
if len(content) > max_answer_chars:
|
||||
raise ValueError(
|
||||
f"Content for {memory_name} is too long. Max length is {max_answer_chars} characters. " + "Please make the content shorter."
|
||||
)
|
||||
|
||||
return self.memories_manager.save_memory(memory_name, content)
|
||||
|
||||
|
||||
class ReadMemoryTool(Tool):
|
||||
"""
|
||||
Reads the memory with the given name from Serena's project-specific memory store.
|
||||
"""
|
||||
|
||||
def apply(self, memory_file_name: str, max_answer_chars: int = TOOL_DEFAULT_MAX_ANSWER_LENGTH) -> str:
|
||||
"""
|
||||
Read the content of a memory file. This tool should only be used if the information
|
||||
is relevant to the current task. You can infer whether the information
|
||||
is relevant from the memory file name.
|
||||
You should not read the same memory file multiple times in the same conversation.
|
||||
"""
|
||||
return self.memories_manager.load_memory(memory_file_name)
|
||||
|
||||
|
||||
class ListMemoriesTool(Tool):
|
||||
"""
|
||||
Lists memories in Serena's project-specific memory store.
|
||||
"""
|
||||
|
||||
def apply(self) -> str:
|
||||
"""
|
||||
List available memories. Any memory can be read using the `read_memory` tool.
|
||||
"""
|
||||
return json.dumps(self.memories_manager.list_memories())
|
||||
|
||||
|
||||
class DeleteMemoryTool(Tool):
|
||||
"""
|
||||
Deletes a memory from Serena's project-specific memory store.
|
||||
"""
|
||||
|
||||
def apply(self, memory_file_name: str) -> str:
|
||||
"""
|
||||
Delete a memory file. Should only happen if a user asks for it explicitly,
|
||||
for example by saying that the information retrieved from a memory file is no longer correct
|
||||
or no longer relevant for the project.
|
||||
"""
|
||||
return self.memories_manager.delete_memory(memory_file_name)
|
||||
@@ -0,0 +1,372 @@
|
||||
import inspect
|
||||
import os
|
||||
import traceback
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable, Generator, Iterable
|
||||
from copy import copy
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Any, Self, TypeVar
|
||||
|
||||
from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
|
||||
from sensai.util import logging
|
||||
from sensai.util.string import dict_string
|
||||
|
||||
from serena.prompt_factory import PromptFactory
|
||||
from serena.symbol import SymbolManager
|
||||
from serena.util.inspection import iter_subclasses
|
||||
from solidlsp import SolidLanguageServer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.agent import LinesRead, MemoriesManager, SerenaAgent
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
T = TypeVar("T")
|
||||
SUCCESS_RESULT = "OK"
|
||||
|
||||
|
||||
class Component(ABC):
|
||||
def __init__(self, agent: "SerenaAgent"):
|
||||
self.agent = agent
|
||||
|
||||
@property
|
||||
def language_server(self) -> SolidLanguageServer:
|
||||
assert self.agent.language_server is not None
|
||||
return self.agent.language_server
|
||||
|
||||
def get_project_root(self) -> str:
|
||||
"""
|
||||
:return: the root directory of the active project, raises a ValueError if no active project configuration is set
|
||||
"""
|
||||
return self.agent.get_project_root()
|
||||
|
||||
@property
|
||||
def prompt_factory(self) -> PromptFactory:
|
||||
return self.agent.prompt_factory
|
||||
|
||||
@property
|
||||
def memories_manager(self) -> "MemoriesManager":
|
||||
assert self.agent.memories_manager is not None
|
||||
return self.agent.memories_manager
|
||||
|
||||
@property
|
||||
def symbol_manager(self) -> SymbolManager:
|
||||
assert self.agent.symbol_manager is not None
|
||||
return self.agent.symbol_manager
|
||||
|
||||
@property
|
||||
def lines_read(self) -> "LinesRead":
|
||||
assert self.agent.lines_read is not None
|
||||
return self.agent.lines_read
|
||||
|
||||
|
||||
TOOL_DEFAULT_MAX_ANSWER_LENGTH = int(2e5)
|
||||
|
||||
|
||||
class ToolMarkerCanEdit:
|
||||
"""
|
||||
Marker class for all tools that can perform editing operations on files.
|
||||
"""
|
||||
|
||||
|
||||
class ToolMarkerDoesNotRequireActiveProject:
|
||||
pass
|
||||
|
||||
|
||||
class ToolInterface(ABC):
|
||||
"""Protocol defining the complete interface that make_tool() expects from a tool."""
|
||||
|
||||
@abstractmethod
|
||||
def get_name(self) -> str:
|
||||
"""Get the tool name."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_apply_docstring(self) -> str:
|
||||
"""Get the docstring for the tool application, used by the MCP server."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_apply_fn_metadata(self) -> FuncMetadata:
|
||||
"""Get the metadata for the tool application function, used by the MCP server."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, **kwargs: Any) -> str:
|
||||
"""Apply the tool with logging and exception handling."""
|
||||
...
|
||||
|
||||
|
||||
class Tool(Component, ToolInterface):
|
||||
# NOTE: each tool should implement the apply method, which is then used in
|
||||
# the central method of the Tool class `apply_ex`.
|
||||
# Failure to do so will result in a RuntimeError at tool execution time.
|
||||
# The apply method is not declared as part of the base Tool interface since we cannot
|
||||
# know the signature of the (input parameters of the) method in advance.
|
||||
#
|
||||
# The docstring and types of the apply method are used to generate the tool description
|
||||
# (which is use by the LLM, so a good description is important)
|
||||
# and to validate the tool call arguments.
|
||||
|
||||
@classmethod
|
||||
def get_name_from_cls(cls) -> str:
|
||||
name = cls.__name__
|
||||
if name.endswith("Tool"):
|
||||
name = name[:-4]
|
||||
# convert to snake_case
|
||||
name = "".join(["_" + c.lower() if c.isupper() else c for c in name]).lstrip("_")
|
||||
return name
|
||||
|
||||
def get_name(self) -> str:
|
||||
return self.get_name_from_cls()
|
||||
|
||||
def get_apply_fn(self) -> Callable:
|
||||
apply_fn = getattr(self, "apply")
|
||||
if apply_fn is None:
|
||||
raise RuntimeError(f"apply not defined in {self}. Did you forget to implement it?")
|
||||
return apply_fn
|
||||
|
||||
@classmethod
|
||||
def can_edit(cls) -> bool:
|
||||
"""
|
||||
Returns whether this tool can perform editing operations on code.
|
||||
|
||||
:return: True if the tool can edit code, False otherwise
|
||||
"""
|
||||
return issubclass(cls, ToolMarkerCanEdit)
|
||||
|
||||
@classmethod
|
||||
def get_tool_description(cls) -> str:
|
||||
docstring = cls.__doc__
|
||||
if docstring is None:
|
||||
return ""
|
||||
return docstring.strip()
|
||||
|
||||
@classmethod
|
||||
def get_apply_docstring_from_cls(cls) -> str:
|
||||
"""Get the docstring for the apply method from the class (static metadata).
|
||||
Needed for creating MCP tools in a separate process without running into serialization issues.
|
||||
"""
|
||||
# First try to get from __dict__ to handle dynamic docstring changes
|
||||
if "apply" in cls.__dict__:
|
||||
apply_fn = cls.__dict__["apply"]
|
||||
else:
|
||||
# Fall back to getattr for inherited methods
|
||||
apply_fn = getattr(cls, "apply", None)
|
||||
if apply_fn is None:
|
||||
raise AttributeError(f"apply method not defined in {cls}. Did you forget to implement it?")
|
||||
|
||||
docstring = apply_fn.__doc__
|
||||
if not docstring:
|
||||
raise AttributeError(f"apply method has no (or empty) docstring in {cls}. Did you forget to implement it?")
|
||||
return docstring.strip()
|
||||
|
||||
def get_apply_docstring(self) -> str:
|
||||
"""Get the docstring for the apply method (instance method implementing ToolProtocol)."""
|
||||
return self.get_apply_docstring_from_cls()
|
||||
|
||||
def get_apply_fn_metadata(self) -> FuncMetadata:
|
||||
"""Get the metadata for the apply method (instance method implementing ToolProtocol)."""
|
||||
return self.get_apply_fn_metadata_from_cls()
|
||||
|
||||
@classmethod
|
||||
def get_apply_fn_metadata_from_cls(cls) -> FuncMetadata:
|
||||
"""Get the metadata for the apply method from the class (static metadata).
|
||||
Needed for creating MCP tools in a separate process without running into serialization issues.
|
||||
"""
|
||||
# First try to get from __dict__ to handle dynamic docstring changes
|
||||
if "apply" in cls.__dict__:
|
||||
apply_fn = cls.__dict__["apply"]
|
||||
else:
|
||||
# Fall back to getattr for inherited methods
|
||||
apply_fn = getattr(cls, "apply", None)
|
||||
if apply_fn is None:
|
||||
raise AttributeError(f"apply method not defined in {cls}. Did you forget to implement it?")
|
||||
|
||||
return func_metadata(apply_fn, skip_names=["self", "cls"])
|
||||
|
||||
def _log_tool_application(self, frame: Any) -> None:
|
||||
params = {}
|
||||
ignored_params = {"self", "log_call", "catch_exceptions", "args", "apply_fn"}
|
||||
for param, value in frame.f_locals.items():
|
||||
if param in ignored_params:
|
||||
continue
|
||||
if param == "kwargs":
|
||||
params.update(value)
|
||||
else:
|
||||
params[param] = value
|
||||
log.info(f"{self.get_name_from_cls()}: {dict_string(params)}")
|
||||
|
||||
@staticmethod
|
||||
def _limit_length(result: str, max_answer_chars: int) -> str:
|
||||
if (n_chars := len(result)) > max_answer_chars:
|
||||
result = (
|
||||
f"The answer is too long ({n_chars} characters). "
|
||||
+ "Please try a more specific tool query or raise the max_answer_chars parameter."
|
||||
)
|
||||
return result
|
||||
|
||||
def is_active(self) -> bool:
|
||||
return self.agent.tool_is_active(self.__class__)
|
||||
|
||||
def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, **kwargs) -> str: # type: ignore
|
||||
"""
|
||||
Applies the tool with the given arguments
|
||||
"""
|
||||
|
||||
def task() -> str:
|
||||
apply_fn = self.get_apply_fn()
|
||||
|
||||
try:
|
||||
if not self.is_active():
|
||||
return f"Error: Tool '{self.get_name_from_cls()}' is not active. Active tools: {self.agent.get_active_tool_names()}"
|
||||
except Exception as e:
|
||||
return f"RuntimeError while checking if tool {self.get_name_from_cls()} is active: {e}"
|
||||
|
||||
if log_call:
|
||||
self._log_tool_application(inspect.currentframe())
|
||||
try:
|
||||
# check whether the tool requires an active project and language server
|
||||
if not isinstance(self, ToolMarkerDoesNotRequireActiveProject):
|
||||
if self.agent._active_project is None:
|
||||
return (
|
||||
"Error: No active project. Ask to user to select a project from this list: "
|
||||
+ f"{self.agent.serena_config.project_names}"
|
||||
)
|
||||
if not self.agent.is_language_server_running():
|
||||
log.info("Language server is not running. Starting it ...")
|
||||
self.agent.reset_language_server()
|
||||
|
||||
# apply the actual tool
|
||||
result = apply_fn(**kwargs)
|
||||
|
||||
except Exception as e:
|
||||
if not catch_exceptions:
|
||||
raise
|
||||
msg = f"Error executing tool: {e}\n{traceback.format_exc()}"
|
||||
log.error(
|
||||
f"Error executing tool: {e}. "
|
||||
f"Consider restarting the language server to solve this (especially, if it's a timeout of a symbolic operation)",
|
||||
exc_info=e,
|
||||
)
|
||||
result = msg
|
||||
|
||||
if log_call:
|
||||
log.info(f"Result: {result}")
|
||||
|
||||
try:
|
||||
self.language_server.save_cache()
|
||||
except Exception as e:
|
||||
log.error(f"Error saving language server cache: {e}")
|
||||
|
||||
return result
|
||||
|
||||
future = self.agent.issue_task(task, name=self.__class__.__name__)
|
||||
return future.result(timeout=self.agent.serena_config.tool_timeout)
|
||||
|
||||
|
||||
class EditedFileContext:
|
||||
"""
|
||||
Context manager for file editing.
|
||||
|
||||
Create the context, then use `set_updated_content` to set the new content, the original content
|
||||
being provided in `original_content`.
|
||||
When exiting the context without an exception, the updated content will be written back to the file.
|
||||
"""
|
||||
|
||||
def __init__(self, relative_path: str, agent: "SerenaAgent"):
|
||||
self._project = agent.get_active_project()
|
||||
assert self._project is not None
|
||||
self._abs_path = os.path.join(self._project.project_root, relative_path)
|
||||
if not os.path.isfile(self._abs_path):
|
||||
raise FileNotFoundError(f"File {self._abs_path} does not exist.")
|
||||
with open(self._abs_path, encoding=self._project.project_config.encoding) as f:
|
||||
self._original_content = f.read()
|
||||
self._updated_content: str | None = None
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
return self
|
||||
|
||||
def get_original_content(self) -> str:
|
||||
"""
|
||||
:return: the original content of the file before any modifications.
|
||||
"""
|
||||
return self._original_content
|
||||
|
||||
def set_updated_content(self, content: str) -> None:
|
||||
"""
|
||||
Sets the updated content of the file, which will be written back to the file
|
||||
when the context is exited without an exception.
|
||||
|
||||
:param content: the updated content of the file
|
||||
"""
|
||||
self._updated_content = content
|
||||
|
||||
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
|
||||
if self._updated_content is not None and exc_type is None:
|
||||
assert self._project is not None
|
||||
with open(self._abs_path, "w", encoding=self._project.project_config.encoding) as f:
|
||||
f.write(self._updated_content)
|
||||
log.info(f"Updated content written to {self._abs_path}")
|
||||
# Language servers should automatically detect the change and update its state accordingly.
|
||||
# If they do not, we may have to add a call to notify it.
|
||||
|
||||
|
||||
class ToolRegistry:
|
||||
_tool_dict: dict[str, type[Tool]] | None = None
|
||||
"""maps tool name to the corresponding tool class"""
|
||||
|
||||
@staticmethod
|
||||
def _iter_tool_classes() -> Generator[type[Tool], None, None]:
|
||||
"""
|
||||
Iterate over Tool subclasses.
|
||||
"""
|
||||
for cls in iter_subclasses(Tool):
|
||||
if cls.__module__.startswith("serena.tools"):
|
||||
yield cls
|
||||
|
||||
@classmethod
|
||||
def _get_tool_dict(cls) -> dict[str, type[Tool]]:
|
||||
if cls._tool_dict is None:
|
||||
cls._tool_dict = {}
|
||||
for tool_class in cls._iter_tool_classes():
|
||||
name = tool_class.get_name_from_cls()
|
||||
if name in cls._tool_dict:
|
||||
raise ValueError(f"Duplicate tool name found: {name}. Tool classes must have unique names.")
|
||||
cls._tool_dict[name] = tool_class
|
||||
return cls._tool_dict
|
||||
|
||||
@classmethod
|
||||
def get_tool_class_by_name(cls, tool_name: str) -> type[Tool]:
|
||||
try:
|
||||
return cls._get_tool_dict()[tool_name]
|
||||
except KeyError as e:
|
||||
available_tools = "\n".join(ToolRegistry.get_tool_names())
|
||||
raise ValueError(f"Tool with name {tool_name} not found. Available tools:\n{available_tools}") from e
|
||||
|
||||
@classmethod
|
||||
def get_all_tool_classes(cls) -> list[type[Tool]]:
|
||||
return list(cls._get_tool_dict().values())
|
||||
|
||||
@classmethod
|
||||
def get_tool_names(cls) -> list[str]:
|
||||
return list(cls._get_tool_dict().keys())
|
||||
|
||||
@classmethod
|
||||
def tool_dict(cls) -> dict[str, type[Tool]]:
|
||||
"""Maps tool name to the corresponding tool class"""
|
||||
return copy(cls._get_tool_dict())
|
||||
|
||||
@classmethod
|
||||
def print_tool_overview(cls, tools: Iterable[type[Tool] | Tool] | None = None) -> None:
|
||||
"""
|
||||
Print a summary of the tools. If no tools are passed, a summary of all tools is printed.
|
||||
"""
|
||||
if tools is None:
|
||||
tools = cls._get_tool_dict().values()
|
||||
|
||||
tool_dict: dict[str, type[Tool] | Tool] = {}
|
||||
for tool_class in tools:
|
||||
tool_dict[tool_class.get_name_from_cls()] = tool_class
|
||||
for tool_name in sorted(tool_dict.keys()):
|
||||
tool_class = tool_dict[tool_name]
|
||||
print(f" * `{tool_name}`: {tool_class.get_tool_description().strip()}")
|
||||
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
Tools supporting the general workflow of the agent
|
||||
"""
|
||||
|
||||
import json
|
||||
import platform
|
||||
|
||||
from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject
|
||||
|
||||
|
||||
class CheckOnboardingPerformedTool(Tool):
|
||||
"""
|
||||
Checks whether project onboarding was already performed.
|
||||
"""
|
||||
|
||||
def apply(self) -> str:
|
||||
"""
|
||||
Checks whether project onboarding was already performed.
|
||||
You should always call this tool before beginning to actually work on the project/after activating a project,
|
||||
but after calling the initial instructions tool.
|
||||
"""
|
||||
from .memory_tools import ListMemoriesTool
|
||||
|
||||
list_memories_tool = self.agent.get_tool(ListMemoriesTool)
|
||||
memories = json.loads(list_memories_tool.apply())
|
||||
if len(memories) == 0:
|
||||
return (
|
||||
"Onboarding not performed yet (no memories available). "
|
||||
+ "You should perform onboarding by calling the `onboarding` tool before proceeding with the task."
|
||||
)
|
||||
else:
|
||||
return f"""The onboarding was already performed, below is the list of available memories.
|
||||
Do not read them immediately, just remember that they exist and that you can read them later, if it is necessary
|
||||
for the current task.
|
||||
Some memories may be based on previous conversations, others may be general for the current project.
|
||||
You should be able to tell which one you need based on the name of the memory.
|
||||
|
||||
{memories}"""
|
||||
|
||||
|
||||
class OnboardingTool(Tool):
|
||||
"""
|
||||
Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
"""
|
||||
|
||||
def apply(self) -> str:
|
||||
"""
|
||||
Call this tool if onboarding was not performed yet.
|
||||
You will call this tool at most once per conversation.
|
||||
|
||||
:return: instructions on how to create the onboarding information
|
||||
"""
|
||||
system = platform.system()
|
||||
return self.prompt_factory.create_onboarding_prompt(system=system)
|
||||
|
||||
|
||||
class ThinkAboutCollectedInformationTool(Tool):
|
||||
"""
|
||||
Thinking tool for pondering the completeness of collected information.
|
||||
"""
|
||||
|
||||
def apply(self) -> str:
|
||||
"""
|
||||
Think about the collected information and whether it is sufficient and relevant.
|
||||
This tool should ALWAYS be called after you have completed a non-trivial sequence of searching steps like
|
||||
find_symbol, find_referencing_symbols, search_files_for_pattern, read_file, etc.
|
||||
"""
|
||||
return self.prompt_factory.create_think_about_collected_information()
|
||||
|
||||
|
||||
class ThinkAboutTaskAdherenceTool(Tool):
|
||||
"""
|
||||
Thinking tool for determining whether the agent is still on track with the current task.
|
||||
"""
|
||||
|
||||
def apply(self) -> str:
|
||||
"""
|
||||
Think about the task at hand and whether you are still on track.
|
||||
Especially important if the conversation has been going on for a while and there
|
||||
has been a lot of back and forth.
|
||||
|
||||
This tool should ALWAYS be called before you insert, replace, or delete code.
|
||||
"""
|
||||
return self.prompt_factory.create_think_about_task_adherence()
|
||||
|
||||
|
||||
class ThinkAboutWhetherYouAreDoneTool(Tool):
|
||||
"""
|
||||
Thinking tool for determining whether the task is truly completed.
|
||||
"""
|
||||
|
||||
def apply(self) -> str:
|
||||
"""
|
||||
Whenever you feel that you are done with what the user has asked for, it is important to call this tool.
|
||||
"""
|
||||
return self.prompt_factory.create_think_about_whether_you_are_done()
|
||||
|
||||
|
||||
class SummarizeChangesTool(Tool):
|
||||
"""
|
||||
Provides instructions for summarizing the changes made to the codebase.
|
||||
"""
|
||||
|
||||
def apply(self) -> str:
|
||||
"""
|
||||
Summarize the changes you have made to the codebase.
|
||||
This tool should always be called after you have fully completed any non-trivial coding task,
|
||||
but only after the think_about_whether_you_are_done call.
|
||||
"""
|
||||
return self.prompt_factory.create_summarize_changes()
|
||||
|
||||
|
||||
class PrepareForNewConversationTool(Tool):
|
||||
"""
|
||||
Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
"""
|
||||
|
||||
def apply(self) -> str:
|
||||
"""
|
||||
Instructions for preparing for a new conversation. This tool should only be called on explicit user request.
|
||||
"""
|
||||
return self.prompt_factory.create_prepare_for_new_conversation()
|
||||
|
||||
|
||||
class InitialInstructionsTool(Tool, ToolMarkerDoesNotRequireActiveProject):
|
||||
"""
|
||||
Gets the initial instructions for the current project.
|
||||
Should only be used in settings where the system prompt cannot be set,
|
||||
e.g. in clients you have no control over, like Claude Desktop.
|
||||
"""
|
||||
|
||||
def apply(self) -> str:
|
||||
"""
|
||||
Get the initial instructions for the current coding project.
|
||||
You should always call this tool before starting to work (including using any other tool) on any programming task!
|
||||
The only exception is when a user asks you to activate a project, in which case you should call the `activate_project` first
|
||||
instead and then call this tool.
|
||||
"""
|
||||
return self.agent.create_system_prompt()
|
||||
@@ -0,0 +1,65 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from serena.agent import log
|
||||
|
||||
|
||||
def is_headless_environment() -> bool:
|
||||
"""
|
||||
Detect if we're running in a headless environment where GUI operations would fail.
|
||||
|
||||
Returns True if:
|
||||
- No DISPLAY variable on Linux/Unix
|
||||
- Running in SSH session
|
||||
- Running in WSL without X server
|
||||
- Running in Docker container
|
||||
"""
|
||||
# Check if we're on Windows - GUI usually works there
|
||||
if sys.platform == "win32":
|
||||
return False
|
||||
|
||||
# Check for DISPLAY variable (required for X11)
|
||||
if not os.environ.get("DISPLAY"):
|
||||
return True
|
||||
|
||||
# Check for SSH session
|
||||
if os.environ.get("SSH_CONNECTION") or os.environ.get("SSH_CLIENT"):
|
||||
return True
|
||||
|
||||
# Check for common CI/container environments
|
||||
if os.environ.get("CI") or os.environ.get("CONTAINER") or os.path.exists("/.dockerenv"):
|
||||
return True
|
||||
|
||||
# Check for WSL (only on Unix-like systems where os.uname exists)
|
||||
if hasattr(os, "uname"):
|
||||
if "microsoft" in os.uname().release.lower():
|
||||
# In WSL, even with DISPLAY set, X server might not be running
|
||||
# This is a simplified check - could be improved
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def show_fatal_exception_safe(e: Exception) -> None:
|
||||
"""
|
||||
Shows the given exception in the GUI log viewer on the main thread and ensures that the exception is logged or at
|
||||
least printed to stderr.
|
||||
"""
|
||||
# Log the error and print it to stderr
|
||||
log.error(f"Fatal exception: {e}", exc_info=e)
|
||||
print(f"Fatal exception: {e}", file=sys.stderr)
|
||||
|
||||
# Don't attempt GUI in headless environments
|
||||
if is_headless_environment():
|
||||
log.debug("Skipping GUI error display in headless environment")
|
||||
return
|
||||
|
||||
# attempt to show the error in the GUI
|
||||
try:
|
||||
# NOTE: The import can fail on macOS if Tk is not available (depends on Python interpreter installation, which uv
|
||||
# used as a base); while tkinter as such is always available, its dependencies can be unavailable on macOS.
|
||||
from serena.gui_log_viewer import show_fatal_exception
|
||||
|
||||
show_fatal_exception(e)
|
||||
except Exception as gui_error:
|
||||
log.debug(f"Failed to show GUI error dialog: {gui_error}")
|
||||
@@ -0,0 +1 @@
|
||||
language_servers/static
|
||||
+64
-52
@@ -2,7 +2,6 @@
|
||||
Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
@@ -12,10 +11,11 @@ import threading
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.ls_utils import FileUtils, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
from .common import RuntimeDependency, RuntimeDependencyCollection
|
||||
|
||||
|
||||
class ClangdLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
@@ -28,7 +28,7 @@ class ClangdLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
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)
|
||||
clangd_executable_path = self._setup_runtime_dependencies(logger, config)
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
@@ -41,42 +41,49 @@ class ClangdLanguageServer(SolidLanguageServer):
|
||||
self.initialize_searcher_command_available = threading.Event()
|
||||
self.resolve_main_method_available = threading.Event()
|
||||
|
||||
def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
|
||||
@staticmethod
|
||||
def _setup_runtime_dependencies(logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
|
||||
"""
|
||||
Setup runtime dependencies for ClangdLanguageServer.
|
||||
Setup runtime dependencies for ClangdLanguageServer and return the command to start the server.
|
||||
"""
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json")) as f:
|
||||
d = json.load(f)
|
||||
del d["_description"]
|
||||
|
||||
assert platform_id.value in [
|
||||
"linux-x64",
|
||||
"win-x64",
|
||||
"osx-arm64",
|
||||
], (
|
||||
"Unsupported platform: " + platform_id.value
|
||||
deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="Clangd",
|
||||
description="Clangd for Linux (x64)",
|
||||
url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip",
|
||||
platform_id="linux-x64",
|
||||
archive_type="zip",
|
||||
binary_name="clangd_19.1.2/bin/clangd",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="Clangd",
|
||||
description="Clangd for Windows (x64)",
|
||||
url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-windows-19.1.2.zip",
|
||||
platform_id="win-x64",
|
||||
archive_type="zip",
|
||||
binary_name="clangd_19.1.2/bin/clangd.exe",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="Clangd",
|
||||
description="Clangd for macOS (Arm64)",
|
||||
url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip",
|
||||
platform_id="osx-arm64",
|
||||
archive_type="zip",
|
||||
binary_name="clangd_19.1.2/bin/clangd",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
runtime_dependencies = d["runtimeDependencies"]
|
||||
runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value]
|
||||
assert len(runtime_dependencies) == 1
|
||||
# Select dependency matching the current platform
|
||||
dependency = next((dep for dep in runtime_dependencies if dep["platformId"] == platform_id.value), None)
|
||||
if dependency is None:
|
||||
raise RuntimeError(f"No runtime dependency found for platform {platform_id.value}")
|
||||
|
||||
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"])
|
||||
dep = deps.single_for_current_platform()
|
||||
clangd_executable_path = deps.binary_path(clangd_ls_dir)
|
||||
if not os.path.exists(clangd_executable_path):
|
||||
clangd_url = dependency["url"]
|
||||
logger.log(f"Clangd executable not found at {clangd_executable_path}. Downloading from {clangd_url}", logging.INFO)
|
||||
os.makedirs(clangd_ls_dir, exist_ok=True)
|
||||
if dependency["archiveType"] == "zip":
|
||||
FileUtils.download_and_extract_archive(logger, clangd_url, clangd_ls_dir, dependency["archiveType"])
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported archive type: {dependency['archiveType']}")
|
||||
logger.log(
|
||||
f"Clangd executable not found at {clangd_executable_path}. Downloading from {dep.url}",
|
||||
logging.INFO,
|
||||
)
|
||||
deps.install(logger, clangd_ls_dir)
|
||||
if not os.path.exists(clangd_executable_path):
|
||||
raise FileNotFoundError(
|
||||
f"Clangd executable not found at {clangd_executable_path}.\n"
|
||||
@@ -86,29 +93,34 @@ class ClangdLanguageServer(SolidLanguageServer):
|
||||
|
||||
return clangd_executable_path
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
@staticmethod
|
||||
def _get_initialize_params(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")) as f:
|
||||
d = json.load(f)
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
initialize_params = {
|
||||
"locale": "en",
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {"didSave": True, "dynamicRegistration": True},
|
||||
"completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
|
||||
"definition": {"dynamicRegistration": True},
|
||||
},
|
||||
"workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
|
||||
},
|
||||
"processId": os.getpid(),
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": root_uri,
|
||||
"name": "$name",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
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
|
||||
return initialize_params
|
||||
|
||||
def _start_server(self):
|
||||
"""
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"_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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"_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"
|
||||
},
|
||||
{
|
||||
"id": "Clangd",
|
||||
"description": "Clangd for Windows (x64)",
|
||||
"url": "https://github.com/clangd/clangd/releases/download/19.1.2/clangd-windows-19.1.2.zip",
|
||||
"platformId": "win-x64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "clangd.exe"
|
||||
},
|
||||
{
|
||||
"id": "Clangd",
|
||||
"description": "Clangd for macOS (Arm64)",
|
||||
"url": "https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip",
|
||||
"platformId": "osx-arm64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "clangd"
|
||||
}
|
||||
]
|
||||
}
|
||||
+50
-50
@@ -13,10 +13,11 @@ import threading
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.ls_utils import FileUtils, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
from .common import RuntimeDependency, RuntimeDependencyCollection
|
||||
|
||||
|
||||
def run_command(cmd: list, capture_output: bool = True) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
@@ -44,38 +45,44 @@ class ClojureLSP(SolidLanguageServer):
|
||||
"""
|
||||
|
||||
clojure_lsp_releases = "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download"
|
||||
runtime_dependencies = [
|
||||
{
|
||||
"url": f"{clojure_lsp_releases}/clojure-lsp-native-macos-aarch64.zip",
|
||||
"platformId": "osx-arm64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "clojure-lsp",
|
||||
},
|
||||
{
|
||||
"url": f"{clojure_lsp_releases}/clojure-lsp-native-linux-aarch64.zip",
|
||||
"platformId": "linux-arm64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "clojure-lsp",
|
||||
},
|
||||
{
|
||||
"url": f"{clojure_lsp_releases}/clojure-lsp-native-linux-amd64.zip",
|
||||
"platformId": "linux-x64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "clojure-lsp",
|
||||
},
|
||||
{
|
||||
"url": f"{clojure_lsp_releases}/clojure-lsp-native-windows-amd64.zip",
|
||||
"platformId": "win-x64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "clojure-lsp.exe",
|
||||
},
|
||||
]
|
||||
runtime_dependencies = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="clojure-lsp",
|
||||
url=f"{clojure_lsp_releases}/clojure-lsp-native-macos-aarch64.zip",
|
||||
platform_id="osx-arm64",
|
||||
archive_type="zip",
|
||||
binary_name="clojure-lsp",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="clojure-lsp",
|
||||
url=f"{clojure_lsp_releases}/clojure-lsp-native-linux-aarch64.zip",
|
||||
platform_id="linux-arm64",
|
||||
archive_type="zip",
|
||||
binary_name="clojure-lsp",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="clojure-lsp",
|
||||
url=f"{clojure_lsp_releases}/clojure-lsp-native-linux-amd64.zip",
|
||||
platform_id="linux-x64",
|
||||
archive_type="zip",
|
||||
binary_name="clojure-lsp",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="clojure-lsp",
|
||||
url=f"{clojure_lsp_releases}/clojure-lsp-native-windows-amd64.zip",
|
||||
platform_id="win-x64",
|
||||
archive_type="zip",
|
||||
binary_name="clojure-lsp.exe",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
|
||||
"""
|
||||
Creates a ClojureLSP instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
clojure_lsp_executable_path = self.setup_runtime_dependencies(logger, config)
|
||||
clojure_lsp_executable_path = self._setup_runtime_dependencies(logger, config)
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
@@ -88,44 +95,40 @@ class ClojureLSP(SolidLanguageServer):
|
||||
self.resolve_main_method_available = threading.Event()
|
||||
self.service_ready_event = threading.Event()
|
||||
|
||||
def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
|
||||
"""Setup runtime dependencies for clojure-lsp."""
|
||||
@staticmethod
|
||||
def _setup_runtime_dependencies(logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
|
||||
"""Setup runtime dependencies for clojure-lsp and return the command to start the server."""
|
||||
verify_clojure_cli()
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
|
||||
runtime_dependencies = [dependency for dependency in self.runtime_dependencies if dependency["platformId"] == platform_id.value]
|
||||
if len(runtime_dependencies) != 1:
|
||||
raise RuntimeError(f"Could not find a suitable clojure-lsp runtime dependency for platform {platform_id.value}.")
|
||||
dependency = runtime_dependencies[0]
|
||||
deps = ClojureLSP.runtime_dependencies
|
||||
dependency = deps.single_for_current_platform()
|
||||
|
||||
clojurelsp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "clojure-lsp")
|
||||
clojurelsp_executable_path = os.path.join(clojurelsp_ls_dir, dependency["binaryName"])
|
||||
clojurelsp_executable_path = deps.binary_path(clojurelsp_ls_dir)
|
||||
if not os.path.exists(clojurelsp_executable_path):
|
||||
os.makedirs(clojurelsp_ls_dir, exist_ok=True)
|
||||
logger.log(f"Downloading and extracting clojure-lsp from {dependency['url']} to {clojurelsp_ls_dir}", logging.INFO)
|
||||
FileUtils.download_and_extract_archive(logger, dependency["url"], clojurelsp_ls_dir, dependency["archiveType"])
|
||||
logger.log(
|
||||
f"Downloading and extracting clojure-lsp from {dependency.url} to {clojurelsp_ls_dir}",
|
||||
logging.INFO,
|
||||
)
|
||||
deps.install(logger, clojurelsp_ls_dir)
|
||||
if not os.path.exists(clojurelsp_executable_path):
|
||||
raise FileNotFoundError(f"Download failed? Could not find clojure-lsp executable at {clojurelsp_executable_path}")
|
||||
os.chmod(clojurelsp_executable_path, stat.S_IEXEC)
|
||||
return clojurelsp_executable_path
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""Returns the init params for clojure-lsp."""
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
return { # type: ignore
|
||||
"processId": os.getpid(),
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"clientInfo": {"name": "solidlsp", "version": "0.1.0"},
|
||||
"capabilities": {
|
||||
"workspace": {
|
||||
"applyEdit": True,
|
||||
"workspaceEdit": {"documentChanges": True},
|
||||
"symbol": {
|
||||
"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]
|
||||
}
|
||||
},
|
||||
"symbol": {"symbolKind": {"valueSet": list(range(1, 27))}},
|
||||
"workspaceFolders": True,
|
||||
},
|
||||
"textDocument": {
|
||||
@@ -133,13 +136,10 @@ class ClojureLSP(SolidLanguageServer):
|
||||
"publishDiagnostics": {"relatedInformation": True, "tagSupport": {"valueSet": [1, 2]}},
|
||||
"definition": {"linkSupport": True},
|
||||
"references": {},
|
||||
"completion": {"completionItem": {"snippetSupport": True, "documentationFormat": ["markdown", "plaintext"]}},
|
||||
"hover": {"contentFormat": ["markdown", "plaintext"]},
|
||||
"documentSymbol": {
|
||||
"hierarchicalDocumentSymbolSupport": 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]
|
||||
},
|
||||
"symbolKind": {"valueSet": list(range(1, 27))}, #
|
||||
},
|
||||
},
|
||||
"general": {"positionEncodings": ["utf-16"]},
|
||||
@@ -0,0 +1,101 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.ls_utils import FileUtils, PlatformUtils
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class RuntimeDependency:
|
||||
"""Represents a runtime dependency for a language server."""
|
||||
|
||||
id: str
|
||||
platform_id: str | None = None
|
||||
url: str | None = None
|
||||
archive_type: str | None = None
|
||||
binary_name: str | None = None
|
||||
command: str | None = None
|
||||
package_name: str | None = None
|
||||
package_version: str | None = None
|
||||
extract_path: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class RuntimeDependencyCollection:
|
||||
"""Utility to handle installation of runtime dependencies."""
|
||||
|
||||
def __init__(self, dependencies: Sequence[RuntimeDependency]):
|
||||
self._dependencies = list(dependencies)
|
||||
|
||||
def for_platform(self, platform_id: str) -> list[RuntimeDependency]:
|
||||
return [d for d in self._dependencies if d.platform_id in (platform_id, "any", "platform-agnostic", None)]
|
||||
|
||||
def for_current_platform(self) -> list[RuntimeDependency]:
|
||||
return self.for_platform(PlatformUtils.get_platform_id().value)
|
||||
|
||||
def single_for_current_platform(self) -> RuntimeDependency:
|
||||
deps = self.for_current_platform()
|
||||
if len(deps) != 1:
|
||||
raise RuntimeError(f"Expected exactly one runtime dependency for {PlatformUtils.get_platform_id().value}, found {len(deps)}")
|
||||
return deps[0]
|
||||
|
||||
def binary_path(self, target_dir: str) -> str:
|
||||
dep = self.single_for_current_platform()
|
||||
if not dep.binary_name:
|
||||
return target_dir
|
||||
return os.path.join(target_dir, dep.binary_name)
|
||||
|
||||
def install(self, logger: LanguageServerLogger, target_dir: str) -> dict[str, str]:
|
||||
"""Install all dependencies for the current platform into *target_dir*.
|
||||
|
||||
Returns a mapping from dependency id to the resolved binary path.
|
||||
"""
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
results: dict[str, str] = {}
|
||||
for dep in self.for_current_platform():
|
||||
if dep.url:
|
||||
self._install_from_url(dep, logger, target_dir)
|
||||
if dep.command:
|
||||
self._run_command(dep.command, target_dir)
|
||||
if dep.binary_name:
|
||||
results[dep.id] = os.path.join(target_dir, dep.binary_name)
|
||||
else:
|
||||
results[dep.id] = target_dir
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def _run_command(command: str, cwd: str) -> None:
|
||||
if PlatformUtils.get_platform_id().value.startswith("win"):
|
||||
subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
check=True,
|
||||
cwd=cwd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
else:
|
||||
import pwd
|
||||
|
||||
user = pwd.getpwuid(os.getuid()).pw_name
|
||||
subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
check=True,
|
||||
user=user,
|
||||
cwd=cwd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _install_from_url(dep: RuntimeDependency, logger: LanguageServerLogger, target_dir: str) -> None:
|
||||
if dep.archive_type == "gz" and dep.binary_name:
|
||||
dest = os.path.join(target_dir, dep.binary_name)
|
||||
FileUtils.download_and_extract_archive(logger, dep.url, dest, dep.archive_type)
|
||||
else:
|
||||
FileUtils.download_and_extract_archive(logger, dep.url, target_dir, dep.archive_type or "zip")
|
||||
+36
-102
@@ -8,11 +8,11 @@ import os
|
||||
import platform
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import tarfile
|
||||
import threading
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
@@ -26,21 +26,7 @@ from solidlsp.ls_utils import PathUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
|
||||
@dataclass
|
||||
class RuntimeDependency:
|
||||
"""Represents a runtime dependency for the C# language server."""
|
||||
|
||||
id: str
|
||||
description: str
|
||||
platform_id: str
|
||||
archive_type: str
|
||||
binary_name: str
|
||||
package_name: str | None = None
|
||||
package_version: str | None = None
|
||||
extract_path: str | None = None
|
||||
url: str | None = None
|
||||
|
||||
from .common import RuntimeDependency
|
||||
|
||||
# Runtime dependencies configuration
|
||||
RUNTIME_DEPENDENCIES = [
|
||||
@@ -194,13 +180,13 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
Creates a CSharpLanguageServer instance. This class is not meant to be instantiated directly.
|
||||
Use LanguageServer.create() instead.
|
||||
"""
|
||||
dotnet_path, language_server_path, cache_dir = self._ensure_server_installed(logger, config)
|
||||
dotnet_path, language_server_path = self._ensure_server_installed(logger, config)
|
||||
|
||||
# Find solution or project file
|
||||
solution_or_project = find_solution_or_project_file(repository_root_path)
|
||||
|
||||
# Create log directory
|
||||
log_dir = cache_dir / "logs"
|
||||
log_dir = Path(self.ls_resources_dir()) / "logs"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Build command using dotnet directly
|
||||
@@ -228,30 +214,21 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj", "packages", ".vs"]
|
||||
|
||||
def _ensure_server_installed(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str, Path]:
|
||||
@classmethod
|
||||
def _ensure_server_installed(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str]:
|
||||
"""
|
||||
Ensure .NET runtime and Microsoft.CodeAnalysis.LanguageServer are available.
|
||||
Returns a tuple of (dotnet_path, language_server_dll_path, cache_dir).
|
||||
Returns a tuple of (dotnet_path, language_server_dll_path).
|
||||
"""
|
||||
# Determine platform runtime ID
|
||||
runtime_id = self._get_runtime_id()
|
||||
runtime_id = CSharpLanguageServer._get_runtime_id()
|
||||
lang_server_dep, dotnet_runtime_dep = CSharpLanguageServer._get_runtime_dependencies(runtime_id)
|
||||
dotnet_path = CSharpLanguageServer._ensure_dotnet_runtime(logger, dotnet_runtime_dep)
|
||||
server_dll_path = CSharpLanguageServer._ensure_language_server(logger, lang_server_dep)
|
||||
|
||||
# Set up cache directory
|
||||
cache_dir = Path.home() / ".cache" / "serena" / "language-servers" / "csharp"
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
return dotnet_path, server_dll_path
|
||||
|
||||
# Get runtime dependencies for this platform
|
||||
lang_server_dep, dotnet_runtime_dep = self._get_runtime_dependencies(runtime_id)
|
||||
|
||||
# Ensure .NET runtime is available
|
||||
dotnet_path = self._ensure_dotnet_runtime(logger, cache_dir, dotnet_runtime_dep)
|
||||
|
||||
# Ensure language server is available
|
||||
server_dll_path = self._ensure_language_server(logger, cache_dir, lang_server_dep)
|
||||
|
||||
return dotnet_path, server_dll_path, cache_dir
|
||||
|
||||
def _get_runtime_id(self) -> str:
|
||||
@staticmethod
|
||||
def _get_runtime_id() -> str:
|
||||
"""Determine the runtime ID based on the platform."""
|
||||
system = platform.system().lower()
|
||||
machine = platform.machine().lower()
|
||||
@@ -265,7 +242,8 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
else:
|
||||
raise LanguageServerException(f"Unsupported platform: {system} {machine}")
|
||||
|
||||
def _get_runtime_dependencies(self, runtime_id: str) -> tuple[RuntimeDependency, RuntimeDependency]:
|
||||
@staticmethod
|
||||
def _get_runtime_dependencies(runtime_id: str) -> tuple[RuntimeDependency, RuntimeDependency]:
|
||||
"""Get the language server and .NET runtime dependencies for the platform."""
|
||||
lang_server_dep = None
|
||||
dotnet_runtime_dep = None
|
||||
@@ -283,15 +261,14 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
|
||||
return lang_server_dep, dotnet_runtime_dep
|
||||
|
||||
def _ensure_dotnet_runtime(self, logger: LanguageServerLogger, cache_dir: Path, runtime_dep: RuntimeDependency) -> str:
|
||||
@staticmethod
|
||||
def _ensure_dotnet_runtime(logger: LanguageServerLogger, runtime_dep: RuntimeDependency) -> str:
|
||||
"""Ensure .NET runtime is available and return the dotnet executable path."""
|
||||
# Check if dotnet is already available in system
|
||||
# Check if dotnet is already available on the system
|
||||
system_dotnet = shutil.which("dotnet")
|
||||
if system_dotnet:
|
||||
# Check if it's .NET 9
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
|
||||
if "Microsoft.NETCore.App 9." in result.stdout:
|
||||
logger.log("Found system .NET 9 runtime", logging.INFO)
|
||||
@@ -300,14 +277,15 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
pass
|
||||
|
||||
# Download .NET 9 runtime using config
|
||||
return self._ensure_dotnet_runtime_from_config(logger, cache_dir, runtime_dep)
|
||||
return CSharpLanguageServer._ensure_dotnet_runtime_from_config(logger, runtime_dep)
|
||||
|
||||
def _ensure_language_server(self, logger: LanguageServerLogger, cache_dir: Path, lang_server_dep: RuntimeDependency) -> str:
|
||||
@classmethod
|
||||
def _ensure_language_server(cls, logger: LanguageServerLogger, lang_server_dep: RuntimeDependency) -> str:
|
||||
"""Ensure language server is available and return the DLL path."""
|
||||
package_name = lang_server_dep.package_name
|
||||
package_version = lang_server_dep.package_version
|
||||
|
||||
server_dir = cache_dir / f"{package_name}.{package_version}"
|
||||
server_dir = Path(cls.ls_resources_dir()) / f"{package_name}.{package_version}"
|
||||
server_dll = server_dir / lang_server_dep.binary_name
|
||||
|
||||
if server_dll.exists():
|
||||
@@ -316,10 +294,10 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
|
||||
# Download and install the language server
|
||||
logger.log(f"Downloading {package_name} version {package_version}...", logging.INFO)
|
||||
package_path = self._download_nuget_package_direct(logger, package_name, package_version, cache_dir)
|
||||
package_path = CSharpLanguageServer._download_nuget_package_direct(logger, package_name, package_version)
|
||||
|
||||
# Extract and install
|
||||
self._extract_language_server(lang_server_dep, package_path, server_dir)
|
||||
CSharpLanguageServer._extract_language_server(lang_server_dep, package_path, server_dir)
|
||||
|
||||
if not server_dll.exists():
|
||||
raise LanguageServerException("Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction")
|
||||
@@ -331,7 +309,8 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
logger.log(f"Successfully installed Microsoft.CodeAnalysis.LanguageServer to {server_dll}", logging.INFO)
|
||||
return str(server_dll)
|
||||
|
||||
def _extract_language_server(self, lang_server_dep: RuntimeDependency, package_path: Path, server_dir: Path) -> None:
|
||||
@staticmethod
|
||||
def _extract_language_server(lang_server_dep: RuntimeDependency, package_path: Path, server_dir: Path) -> None:
|
||||
"""Extract language server files from downloaded package."""
|
||||
extract_path = lang_server_dep.extract_path or "lib/net9.0"
|
||||
source_dir = package_path / extract_path
|
||||
@@ -353,9 +332,8 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
server_dir.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(source_dir, server_dir, dirs_exist_ok=True)
|
||||
|
||||
def _download_nuget_package_direct(
|
||||
self, logger: LanguageServerLogger, package_name: str, package_version: str, cache_dir: Path
|
||||
) -> Path:
|
||||
@classmethod
|
||||
def _download_nuget_package_direct(cls, logger: LanguageServerLogger, package_name: str, package_version: str) -> Path:
|
||||
"""
|
||||
Download a NuGet package directly from the Azure NuGet feed.
|
||||
Returns the path to the extracted package directory.
|
||||
@@ -363,7 +341,7 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
azure_feed_url = "https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json"
|
||||
|
||||
# Create temporary directory for package download
|
||||
temp_dir = cache_dir / "temp_downloads"
|
||||
temp_dir = Path(cls.ls_resources_dir()) / "temp_downloads"
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
@@ -411,18 +389,17 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
f"Failed to download package {package_name} version {package_version} from Azure NuGet feed: {e}"
|
||||
) from e
|
||||
|
||||
def _ensure_dotnet_runtime_from_config(self, logger: LanguageServerLogger, cache_dir: Path, runtime_dep: RuntimeDependency) -> str:
|
||||
@classmethod
|
||||
def _ensure_dotnet_runtime_from_config(cls, logger: LanguageServerLogger, runtime_dep: RuntimeDependency) -> str:
|
||||
"""
|
||||
Ensure .NET 9 runtime is available using runtime dependency configuration.
|
||||
Returns the path to the dotnet executable.
|
||||
"""
|
||||
# Check if dotnet is already available in system
|
||||
# Check if dotnet is already available on the system
|
||||
system_dotnet = shutil.which("dotnet")
|
||||
if system_dotnet:
|
||||
# Check if it's .NET 9
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
|
||||
if "Microsoft.NETCore.App 9." in result.stdout:
|
||||
logger.log("Found system .NET 9 runtime", logging.INFO)
|
||||
@@ -431,7 +408,7 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
pass
|
||||
|
||||
# Download .NET 9 runtime using config
|
||||
dotnet_dir = cache_dir / "dotnet-runtime-9.0"
|
||||
dotnet_dir = Path(cls.ls_resources_dir()) / "dotnet-runtime-9.0"
|
||||
dotnet_exe = dotnet_dir / runtime_dep.binary_name
|
||||
|
||||
if dotnet_exe.exists():
|
||||
@@ -499,9 +476,7 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
"didChangeWatchedFiles": {"dynamicRegistration": 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]
|
||||
},
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
},
|
||||
"executeCommand": {"dynamicRegistration": True},
|
||||
"configuration": True,
|
||||
@@ -510,20 +485,6 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
},
|
||||
"textDocument": {
|
||||
"synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
|
||||
"completion": {
|
||||
"dynamicRegistration": True,
|
||||
"contextSupport": True,
|
||||
"completionItem": {
|
||||
"snippetSupport": True,
|
||||
"commitCharactersSupport": True,
|
||||
"documentationFormat": ["markdown", "plaintext"],
|
||||
"deprecatedSupport": True,
|
||||
"preselectSupport": True,
|
||||
},
|
||||
"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]
|
||||
},
|
||||
},
|
||||
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
|
||||
"signatureHelp": {
|
||||
"dynamicRegistration": True,
|
||||
@@ -534,38 +495,11 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
},
|
||||
"definition": {"dynamicRegistration": 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]
|
||||
},
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
},
|
||||
"codeAction": {
|
||||
"dynamicRegistration": True,
|
||||
"codeActionLiteralSupport": {
|
||||
"codeActionKind": {
|
||||
"valueSet": [
|
||||
"",
|
||||
"quickfix",
|
||||
"refactor",
|
||||
"refactor.extract",
|
||||
"refactor.inline",
|
||||
"refactor.rewrite",
|
||||
"source",
|
||||
"source.organizeImports",
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
"codeLens": {"dynamicRegistration": True},
|
||||
"formatting": {"dynamicRegistration": True},
|
||||
"rangeFormatting": {"dynamicRegistration": True},
|
||||
"onTypeFormatting": {"dynamicRegistration": True},
|
||||
"rename": {"dynamicRegistration": True},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
"foldingRange": {"dynamicRegistration": True, "rangeLimit": 5000, "lineFoldingOnly": True},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -0,0 +1,158 @@
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import stat
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
from .common import RuntimeDependency, RuntimeDependencyCollection
|
||||
|
||||
|
||||
class DartLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
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",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _setup_runtime_dependencies(cls, logger: "LanguageServerLogger") -> str:
|
||||
deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
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",
|
||||
platform_id="linux-x64",
|
||||
archive_type="zip",
|
||||
binary_name="dart-sdk/bin/dart",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DartLanguageServer",
|
||||
description="Dart Language Server for Windows (x64)",
|
||||
url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-x64-release.zip",
|
||||
platform_id="win-x64",
|
||||
archive_type="zip",
|
||||
binary_name="dart-sdk/bin/dart.exe",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DartLanguageServer",
|
||||
description="Dart Language Server for Windows (arm64)",
|
||||
url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-arm64-release.zip",
|
||||
platform_id="win-arm64",
|
||||
archive_type="zip",
|
||||
binary_name="dart-sdk/bin/dart.exe",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DartLanguageServer",
|
||||
description="Dart Language Server for macOS (x64)",
|
||||
url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-x64-release.zip",
|
||||
platform_id="osx-x64",
|
||||
archive_type="zip",
|
||||
binary_name="dart-sdk/bin/dart",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DartLanguageServer",
|
||||
description="Dart Language Server for macOS (arm64)",
|
||||
url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-arm64-release.zip",
|
||||
platform_id="osx-arm64",
|
||||
archive_type="zip",
|
||||
binary_name="dart-sdk/bin/dart",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
dart_ls_dir = cls.ls_resources_dir()
|
||||
dart_executable_path = deps.binary_path(dart_ls_dir)
|
||||
|
||||
if not os.path.exists(dart_ls_dir):
|
||||
os.makedirs(dart_ls_dir)
|
||||
deps.install(logger, dart_ls_dir)
|
||||
|
||||
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"
|
||||
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str):
|
||||
"""
|
||||
Returns the initialize params for the Dart Language Server.
|
||||
"""
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
initialize_params = {
|
||||
"capabilities": {},
|
||||
"initializationOptions": {
|
||||
"onlyAnalyzeProjectsWithOpenFiles": False,
|
||||
"closingLabels": False,
|
||||
"outline": False,
|
||||
"flutterOutline": False,
|
||||
"allowOpenUri": False,
|
||||
},
|
||||
"trace": "verbose",
|
||||
"processId": os.getpid(),
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": pathlib.Path(repository_absolute_path).as_uri(),
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": root_uri,
|
||||
"name": os.path.basename(repository_absolute_path),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
return initialize_params
|
||||
|
||||
def _start_server(self):
|
||||
"""
|
||||
Start the language server and yield when the server is ready.
|
||||
"""
|
||||
|
||||
def execute_client_command_handler(params):
|
||||
return []
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
def check_experimental_status(params):
|
||||
pass
|
||||
|
||||
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)
|
||||
|
||||
self.logger.log("Starting dart-language-server server process", logging.INFO)
|
||||
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 = 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({})
|
||||
@@ -1,119 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import stat
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.ls_utils import FileUtils, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
|
||||
class DartLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
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: "LanguageServerLogger") -> str:
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json")) 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")) 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
|
||||
|
||||
def _start_server(self):
|
||||
"""
|
||||
Start the language server and yield when the server is ready.
|
||||
"""
|
||||
|
||||
def execute_client_command_handler(params):
|
||||
return []
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
def check_experimental_status(params):
|
||||
pass
|
||||
|
||||
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)
|
||||
|
||||
self.logger.log("Starting dart-language-server server process", logging.INFO)
|
||||
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 = 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({})
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"_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"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"_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"
|
||||
},
|
||||
{
|
||||
"id": "DartLanguageServer",
|
||||
"description": "Dart Language Server for Windows (x64)",
|
||||
"url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-x64-release.zip",
|
||||
"platformId": "win-x64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "dart-sdk/bin/dart.exe"
|
||||
},
|
||||
{
|
||||
"id": "DartLanguageServer",
|
||||
"description": "Dart Language Server for Windows (arm64)",
|
||||
"url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-arm64-release.zip",
|
||||
"platformId": "win-arm64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "dart-sdk/bin/dart.exe"
|
||||
},
|
||||
{
|
||||
"id": "DartLanguageServer",
|
||||
"description": "Dart Language Server for macOS (x64)",
|
||||
"url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-x64-release.zip",
|
||||
"platformId": "osx-x64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "dart-sdk/bin/dart"
|
||||
},
|
||||
{
|
||||
"id": "DartLanguageServer",
|
||||
"description": "Dart Language Server for macOS (arm64)",
|
||||
"url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-arm64-release.zip",
|
||||
"platformId": "osx-arm64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "dart-sdk/bin/dart"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,718 @@
|
||||
"""
|
||||
Provides Java specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Java.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import stat
|
||||
import threading
|
||||
import uuid
|
||||
from pathlib import PurePath
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.ls_utils import FileUtils, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class RuntimeDependencyPaths:
|
||||
"""
|
||||
Stores the paths to the runtime dependencies of EclipseJDTLS
|
||||
"""
|
||||
|
||||
gradle_path: str
|
||||
lombok_jar_path: str
|
||||
jre_path: str
|
||||
jre_home_path: str
|
||||
jdtls_launcher_jar_path: str
|
||||
jdtls_readonly_config_path: str
|
||||
intellicode_jar_path: str
|
||||
intellisense_members_path: str
|
||||
|
||||
|
||||
class EclipseJDTLS(SolidLanguageServer):
|
||||
"""
|
||||
The EclipseJDTLS class provides a Java specific implementation of the LanguageServer class
|
||||
"""
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
|
||||
"""
|
||||
Creates a new EclipseJDTLS instance initializing the language server settings appropriately.
|
||||
This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
runtime_dependency_paths = self._setupRuntimeDependencies(logger, config)
|
||||
self.runtime_dependency_paths = runtime_dependency_paths
|
||||
|
||||
# ws_dir is the workspace directory for the EclipseJDTLS server
|
||||
ws_dir = str(
|
||||
PurePath(
|
||||
SolidLSPSettings.get_language_server_directory(),
|
||||
"EclipseJDTLS",
|
||||
"workspaces",
|
||||
uuid.uuid4().hex,
|
||||
)
|
||||
)
|
||||
|
||||
# shared_cache_location is the global cache used by Eclipse JDTLS across all workspaces
|
||||
shared_cache_location = str(PurePath(SolidLSPSettings.get_global_cache_directory(), "lsp", "EclipseJDTLS", "sharedIndex"))
|
||||
|
||||
jre_path = self.runtime_dependency_paths.jre_path
|
||||
lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path
|
||||
|
||||
jdtls_launcher_jar = self.runtime_dependency_paths.jdtls_launcher_jar_path
|
||||
|
||||
os.makedirs(ws_dir, exist_ok=True)
|
||||
|
||||
data_dir = str(PurePath(ws_dir, "data_dir"))
|
||||
jdtls_config_path = str(PurePath(ws_dir, "config_path"))
|
||||
|
||||
jdtls_readonly_config_path = self.runtime_dependency_paths.jdtls_readonly_config_path
|
||||
|
||||
if not os.path.exists(jdtls_config_path):
|
||||
shutil.copytree(jdtls_readonly_config_path, jdtls_config_path)
|
||||
|
||||
for static_path in [
|
||||
jre_path,
|
||||
lombok_jar_path,
|
||||
jdtls_launcher_jar,
|
||||
jdtls_config_path,
|
||||
jdtls_readonly_config_path,
|
||||
]:
|
||||
assert os.path.exists(static_path), static_path
|
||||
|
||||
# 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(
|
||||
[
|
||||
jre_path,
|
||||
"--add-modules=ALL-SYSTEM",
|
||||
"--add-opens",
|
||||
"java.base/java.util=ALL-UNNAMED",
|
||||
"--add-opens",
|
||||
"java.base/java.lang=ALL-UNNAMED",
|
||||
"--add-opens",
|
||||
"java.base/sun.nio.fs=ALL-UNNAMED",
|
||||
"-Declipse.application=org.eclipse.jdt.ls.core.id1",
|
||||
"-Dosgi.bundles.defaultStartLevel=4",
|
||||
"-Declipse.product=org.eclipse.jdt.ls.core.product",
|
||||
"-Djava.import.generatesMetadataFilesAtProjectRoot=false",
|
||||
"-Dfile.encoding=utf8",
|
||||
"-noverify",
|
||||
"-XX:+UseParallelGC",
|
||||
"-XX:GCTimeRatio=4",
|
||||
"-XX:AdaptiveSizePolicyWeight=90",
|
||||
"-Dsun.zip.disableMemoryMapping=true",
|
||||
"-Djava.lsp.joinOnCompletion=true",
|
||||
"-Xmx3G",
|
||||
"-Xms100m",
|
||||
"-Xlog:disable",
|
||||
"-Dlog.level=ALL",
|
||||
f'"-javaagent:{lombok_jar_path}"',
|
||||
f'"-Djdt.core.sharedIndexLocation={shared_cache_location}"',
|
||||
"-jar",
|
||||
f'"{jdtls_launcher_jar}"',
|
||||
"-configuration",
|
||||
f'"{jdtls_config_path}"',
|
||||
"-data",
|
||||
f'"{data_dir}"',
|
||||
]
|
||||
)
|
||||
|
||||
self.service_ready_event = threading.Event()
|
||||
self.intellicode_enable_command_available = threading.Event()
|
||||
self.initialize_searcher_command_available = threading.Event()
|
||||
|
||||
super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java")
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
# Ignore common Java build directories from different build tools:
|
||||
# - Maven: target
|
||||
# - Gradle: build, .gradle
|
||||
# - Eclipse: bin, .settings
|
||||
# - IntelliJ IDEA: out, .idea
|
||||
# - General: classes, dist, lib
|
||||
return super().is_ignored_dirname(dirname) or dirname in [
|
||||
"target", # Maven
|
||||
"build", # Gradle
|
||||
"bin", # Eclipse
|
||||
"out", # IntelliJ IDEA
|
||||
"classes", # General
|
||||
"dist", # General
|
||||
"lib", # General
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _setupRuntimeDependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> RuntimeDependencyPaths:
|
||||
"""
|
||||
Setup runtime dependencies for EclipseJDTLS and return the paths.
|
||||
"""
|
||||
platformId = PlatformUtils.get_platform_id()
|
||||
|
||||
runtime_dependencies = {
|
||||
"gradle": {
|
||||
"platform-agnostic": {
|
||||
"url": "https://services.gradle.org/distributions/gradle-7.3.3-bin.zip",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": ".",
|
||||
}
|
||||
},
|
||||
"vscode-java": {
|
||||
"darwin-arm64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": "vscode-java",
|
||||
},
|
||||
"osx-arm64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": "vscode-java",
|
||||
"jre_home_path": "extension/jre/21.0.7-macosx-aarch64",
|
||||
"jre_path": "extension/jre/21.0.7-macosx-aarch64/bin/java",
|
||||
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
|
||||
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
|
||||
"jdtls_readonly_config_path": "extension/server/config_mac_arm",
|
||||
},
|
||||
"osx-x64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": "vscode-java",
|
||||
"jre_home_path": "extension/jre/21.0.7-macosx-x86_64",
|
||||
"jre_path": "extension/jre/21.0.7-macosx-x86_64/bin/java",
|
||||
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
|
||||
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
|
||||
"jdtls_readonly_config_path": "extension/server/config_mac",
|
||||
},
|
||||
"linux-arm64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": "vscode-java",
|
||||
},
|
||||
"linux-x64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": "vscode-java",
|
||||
"jre_home_path": "extension/jre/21.0.7-linux-x86_64",
|
||||
"jre_path": "extension/jre/21.0.7-linux-x86_64/bin/java",
|
||||
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
|
||||
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
|
||||
"jdtls_readonly_config_path": "extension/server/config_linux",
|
||||
},
|
||||
"win-x64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": "vscode-java",
|
||||
"jre_home_path": "extension/jre/21.0.7-win32-x86_64",
|
||||
"jre_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe",
|
||||
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
|
||||
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
|
||||
"jdtls_readonly_config_path": "extension/server/config_win",
|
||||
},
|
||||
},
|
||||
"intellicode": {
|
||||
"platform-agnostic": {
|
||||
"url": "https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/1.2.30/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage",
|
||||
"alternate_url": "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioExptTeam/vsextensions/vscodeintellicode/1.2.30/vspackage",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": "intellicode",
|
||||
"intellicode_jar_path": "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar",
|
||||
"intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# assert platformId.value in [
|
||||
# "linux-x64",
|
||||
# "win-x64",
|
||||
# ], "Only linux-x64 platform is supported for in multilspy at the moment"
|
||||
|
||||
gradle_path = str(
|
||||
PurePath(
|
||||
cls.ls_resources_dir(),
|
||||
"gradle-7.3.3",
|
||||
)
|
||||
)
|
||||
|
||||
if not os.path.exists(gradle_path):
|
||||
FileUtils.download_and_extract_archive(
|
||||
logger,
|
||||
runtime_dependencies["gradle"]["platform-agnostic"]["url"],
|
||||
str(PurePath(gradle_path).parent),
|
||||
runtime_dependencies["gradle"]["platform-agnostic"]["archiveType"],
|
||||
)
|
||||
|
||||
assert os.path.exists(gradle_path)
|
||||
|
||||
dependency = runtime_dependencies["vscode-java"][platformId.value]
|
||||
vscode_java_path = str(PurePath(cls.ls_resources_dir(), dependency["relative_extraction_path"]))
|
||||
os.makedirs(vscode_java_path, exist_ok=True)
|
||||
jre_home_path = str(PurePath(vscode_java_path, dependency["jre_home_path"]))
|
||||
jre_path = str(PurePath(vscode_java_path, dependency["jre_path"]))
|
||||
lombok_jar_path = str(PurePath(vscode_java_path, dependency["lombok_jar_path"]))
|
||||
jdtls_launcher_jar_path = str(PurePath(vscode_java_path, dependency["jdtls_launcher_jar_path"]))
|
||||
jdtls_readonly_config_path = str(PurePath(vscode_java_path, dependency["jdtls_readonly_config_path"]))
|
||||
if not all(
|
||||
[
|
||||
os.path.exists(vscode_java_path),
|
||||
os.path.exists(jre_home_path),
|
||||
os.path.exists(jre_path),
|
||||
os.path.exists(lombok_jar_path),
|
||||
os.path.exists(jdtls_launcher_jar_path),
|
||||
os.path.exists(jdtls_readonly_config_path),
|
||||
]
|
||||
):
|
||||
FileUtils.download_and_extract_archive(logger, dependency["url"], vscode_java_path, dependency["archiveType"])
|
||||
|
||||
os.chmod(jre_path, stat.S_IEXEC)
|
||||
|
||||
assert os.path.exists(vscode_java_path)
|
||||
assert os.path.exists(jre_home_path)
|
||||
assert os.path.exists(jre_path)
|
||||
assert os.path.exists(lombok_jar_path)
|
||||
assert os.path.exists(jdtls_launcher_jar_path)
|
||||
assert os.path.exists(jdtls_readonly_config_path)
|
||||
|
||||
dependency = runtime_dependencies["intellicode"]["platform-agnostic"]
|
||||
intellicode_directory_path = str(PurePath(cls.ls_resources_dir(), dependency["relative_extraction_path"]))
|
||||
os.makedirs(intellicode_directory_path, exist_ok=True)
|
||||
intellicode_jar_path = str(PurePath(intellicode_directory_path, dependency["intellicode_jar_path"]))
|
||||
intellisense_members_path = str(PurePath(intellicode_directory_path, dependency["intellisense_members_path"]))
|
||||
if not all(
|
||||
[
|
||||
os.path.exists(intellicode_directory_path),
|
||||
os.path.exists(intellicode_jar_path),
|
||||
os.path.exists(intellisense_members_path),
|
||||
]
|
||||
):
|
||||
FileUtils.download_and_extract_archive(logger, dependency["url"], intellicode_directory_path, dependency["archiveType"])
|
||||
|
||||
assert os.path.exists(intellicode_directory_path)
|
||||
assert os.path.exists(intellicode_jar_path)
|
||||
assert os.path.exists(intellisense_members_path)
|
||||
|
||||
return RuntimeDependencyPaths(
|
||||
gradle_path=gradle_path,
|
||||
lombok_jar_path=lombok_jar_path,
|
||||
jre_path=jre_path,
|
||||
jre_home_path=jre_home_path,
|
||||
jdtls_launcher_jar_path=jdtls_launcher_jar_path,
|
||||
jdtls_readonly_config_path=jdtls_readonly_config_path,
|
||||
intellicode_jar_path=intellicode_jar_path,
|
||||
intellisense_members_path=intellisense_members_path,
|
||||
)
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize parameters for the EclipseJDTLS server.
|
||||
"""
|
||||
# Look into https://github.com/eclipse/eclipse.jdt.ls/blob/master/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java to understand all the options available
|
||||
|
||||
if not os.path.isabs(repository_absolute_path):
|
||||
repository_absolute_path = os.path.abspath(repository_absolute_path)
|
||||
repo_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
|
||||
initialize_params = {
|
||||
"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": list(range(1, 27))},
|
||||
"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},
|
||||
# TODO: we have an assert that completion provider is not included in the capabilities at server startup
|
||||
# Removing this will cause the assert to fail. Investigate why this is the case, simplify config
|
||||
"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,
|
||||
},
|
||||
},
|
||||
"definition": {"dynamicRegistration": True, "linkSupport": True},
|
||||
"references": {"dynamicRegistration": True},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": True,
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
"tagSupport": {"valueSet": [1]},
|
||||
"labelSupport": 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},
|
||||
"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,
|
||||
},
|
||||
"typeHierarchy": {"dynamicRegistration": True},
|
||||
"inlineValue": {"dynamicRegistration": True},
|
||||
"diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": False},
|
||||
},
|
||||
"general": {
|
||||
"staleRequestSupport": {
|
||||
"cancel": True,
|
||||
"retryOnContentModified": [
|
||||
"textDocument/semanticTokens/full",
|
||||
"textDocument/semanticTokens/range",
|
||||
"textDocument/semanticTokens/full/delta",
|
||||
],
|
||||
},
|
||||
"regularExpressions": {"engine": "ECMAScript", "version": "ES2020"},
|
||||
"positionEncodings": ["utf-16"],
|
||||
},
|
||||
"notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}},
|
||||
},
|
||||
"initializationOptions": {
|
||||
"bundles": ["intellicode-core.jar"],
|
||||
"settings": {
|
||||
"java": {
|
||||
"home": None,
|
||||
"jdt": {
|
||||
"ls": {
|
||||
"java": {"home": None},
|
||||
"vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx1G -Xms100m -Xlog:disable",
|
||||
"lombokSupport": {"enabled": True},
|
||||
"protobufSupport": {"enabled": True},
|
||||
"androidSupport": {"enabled": True},
|
||||
}
|
||||
},
|
||||
"errors": {"incompleteClasspath": {"severity": "error"}},
|
||||
"configuration": {
|
||||
"checkProjectSettingsExclusions": False,
|
||||
"updateBuildConfiguration": "interactive",
|
||||
"maven": {
|
||||
"userSettings": None,
|
||||
"globalSettings": None,
|
||||
"notCoveredPluginExecutionSeverity": "warning",
|
||||
"defaultMojoExecutionAction": "ignore",
|
||||
},
|
||||
"workspaceCacheLimit": 90,
|
||||
"runtimes": [
|
||||
{"name": "JavaSE-21", "path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64", "default": True}
|
||||
],
|
||||
},
|
||||
"trace": {"server": "verbose"},
|
||||
"import": {
|
||||
"maven": {
|
||||
"enabled": True,
|
||||
"offline": {"enabled": False},
|
||||
"disableTestClasspathFlag": False,
|
||||
},
|
||||
"gradle": {
|
||||
"enabled": True,
|
||||
"wrapper": {"enabled": True},
|
||||
"version": None,
|
||||
"home": "abs(static/gradle-7.3.3)",
|
||||
"java": {"home": "abs(static/launch_jres/21.0.7-linux-x86_64)"},
|
||||
"offline": {"enabled": False},
|
||||
"arguments": None,
|
||||
"jvmArguments": None,
|
||||
"user": {"home": None},
|
||||
"annotationProcessing": {"enabled": True},
|
||||
},
|
||||
"exclusions": [
|
||||
"**/node_modules/**",
|
||||
"**/.metadata/**",
|
||||
"**/archetype-resources/**",
|
||||
"**/META-INF/maven/**",
|
||||
],
|
||||
"generatesMetadataFilesAtProjectRoot": False,
|
||||
},
|
||||
"maven": {"downloadSources": True, "updateSnapshots": True},
|
||||
"eclipse": {"downloadSources": True},
|
||||
"signatureHelp": {"enabled": True, "description": {"enabled": True}},
|
||||
"implementationsCodeLens": {"enabled": True},
|
||||
"format": {
|
||||
"enabled": True,
|
||||
"settings": {"url": None, "profile": None},
|
||||
"comments": {"enabled": True},
|
||||
"onType": {"enabled": True},
|
||||
"insertSpaces": True,
|
||||
"tabSize": 4,
|
||||
},
|
||||
"saveActions": {"organizeImports": False},
|
||||
"project": {
|
||||
"referencedLibraries": ["lib/**/*.jar"],
|
||||
"importOnFirstTimeStartup": "automatic",
|
||||
"importHint": True,
|
||||
"resourceFilters": ["node_modules", "\\.git"],
|
||||
"encoding": "ignore",
|
||||
"exportJar": {"targetPath": "${workspaceFolder}/${workspaceFolderBasename}.jar"},
|
||||
},
|
||||
"contentProvider": {"preferred": None},
|
||||
"autobuild": {"enabled": True},
|
||||
"maxConcurrentBuilds": 1,
|
||||
"selectionRange": {"enabled": True},
|
||||
"showBuildStatusOnStart": {"enabled": "notification"},
|
||||
"server": {"launchMode": "Standard"},
|
||||
"sources": {"organizeImports": {"starThreshold": 99, "staticStarThreshold": 99}},
|
||||
"imports": {"gradle": {"wrapper": {"checksums": []}}},
|
||||
"templates": {"fileHeader": [], "typeComment": []},
|
||||
"references": {"includeAccessors": True, "includeDecompiledSources": True},
|
||||
"typeHierarchy": {"lazyLoad": False},
|
||||
"settings": {"url": None},
|
||||
"symbols": {"includeSourceMethodDeclarations": False},
|
||||
"inlayHints": {"parameterNames": {"enabled": "literals", "exclusions": []}},
|
||||
"codeAction": {"sortMembers": {"avoidVolatileChanges": True}},
|
||||
"compile": {
|
||||
"nullAnalysis": {
|
||||
"nonnull": [
|
||||
"javax.annotation.Nonnull",
|
||||
"org.eclipse.jdt.annotation.NonNull",
|
||||
"org.springframework.lang.NonNull",
|
||||
],
|
||||
"nullable": [
|
||||
"javax.annotation.Nullable",
|
||||
"org.eclipse.jdt.annotation.Nullable",
|
||||
"org.springframework.lang.Nullable",
|
||||
],
|
||||
"mode": "automatic",
|
||||
}
|
||||
},
|
||||
"sharedIndexes": {"enabled": "auto", "location": ""},
|
||||
"silentNotification": False,
|
||||
"dependency": {
|
||||
"showMembers": False,
|
||||
"syncWithFolderExplorer": True,
|
||||
"autoRefresh": True,
|
||||
"refreshDelay": 2000,
|
||||
"packagePresentation": "flat",
|
||||
},
|
||||
"help": {"firstView": "auto", "showReleaseNotes": True, "collectErrorLog": False},
|
||||
"test": {"defaultConfig": "", "config": {}},
|
||||
}
|
||||
},
|
||||
},
|
||||
"trace": "verbose",
|
||||
"processId": os.getpid(),
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": repo_uri,
|
||||
"name": os.path.basename(repository_absolute_path),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
initialize_params["initializationOptions"]["workspaceFolders"] = [repo_uri]
|
||||
bundles = [self.runtime_dependency_paths.intellicode_jar_path]
|
||||
initialize_params["initializationOptions"]["bundles"] = bundles
|
||||
initialize_params["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] = [
|
||||
{"name": "JavaSE-21", "path": self.runtime_dependency_paths.jre_home_path, "default": True}
|
||||
]
|
||||
|
||||
for runtime in initialize_params["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"]:
|
||||
assert "name" in runtime
|
||||
assert "path" in runtime
|
||||
assert os.path.exists(runtime["path"]), f"Runtime required for eclipse_jdtls at path {runtime['path']} does not exist"
|
||||
|
||||
gradle_settings = initialize_params["initializationOptions"]["settings"]["java"]["import"]["gradle"]
|
||||
gradle_settings["home"] = self.runtime_dependency_paths.gradle_path
|
||||
gradle_settings["java"]["home"] = self.runtime_dependency_paths.jre_path
|
||||
return initialize_params
|
||||
|
||||
def _start_server(self):
|
||||
"""
|
||||
Starts the Eclipse JDTLS Language Server
|
||||
"""
|
||||
|
||||
def register_capability_handler(params):
|
||||
assert "registrations" in params
|
||||
for registration in params["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"]:
|
||||
self.intellicode_enable_command_available.set()
|
||||
return
|
||||
|
||||
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()
|
||||
|
||||
def execute_client_command_handler(params):
|
||||
assert params["command"] == "_java.reloadBundles.command"
|
||||
assert params["arguments"] == []
|
||||
return []
|
||||
|
||||
def window_log_message(msg):
|
||||
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
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.logger.log("Starting EclipseJDTLS server process", logging.INFO)
|
||||
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 = 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"]
|
||||
|
||||
self.server.notify.initialized({})
|
||||
|
||||
self.server.notify.workspace_did_change_configuration({"settings": initialize_params["initializationOptions"]["settings"]})
|
||||
|
||||
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 = 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
|
||||
self.service_ready_event.wait()
|
||||
@@ -1,388 +0,0 @@
|
||||
"""
|
||||
Provides Java specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Java.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import stat
|
||||
import threading
|
||||
import uuid
|
||||
from pathlib import PurePath
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.ls_utils import FileUtils, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class RuntimeDependencyPaths:
|
||||
"""
|
||||
Stores the paths to the runtime dependencies of EclipseJDTLS
|
||||
"""
|
||||
|
||||
gradle_path: str
|
||||
lombok_jar_path: str
|
||||
jre_path: str
|
||||
jre_home_path: str
|
||||
jdtls_launcher_jar_path: str
|
||||
jdtls_readonly_config_path: str
|
||||
intellicode_jar_path: str
|
||||
intellisense_members_path: str
|
||||
|
||||
|
||||
class EclipseJDTLS(SolidLanguageServer):
|
||||
"""
|
||||
The EclipseJDTLS class provides a Java specific implementation of the LanguageServer class
|
||||
"""
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
|
||||
"""
|
||||
Creates a new EclipseJDTLS instance initializing the language server settings appropriately.
|
||||
This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
runtime_dependency_paths = self.setupRuntimeDependencies(logger, config)
|
||||
self.runtime_dependency_paths = runtime_dependency_paths
|
||||
|
||||
# ws_dir is the workspace directory for the EclipseJDTLS server
|
||||
ws_dir = str(
|
||||
PurePath(
|
||||
SolidLSPSettings.get_language_server_directory(),
|
||||
"EclipseJDTLS",
|
||||
"workspaces",
|
||||
uuid.uuid4().hex,
|
||||
)
|
||||
)
|
||||
|
||||
# shared_cache_location is the global cache used by Eclipse JDTLS across all workspaces
|
||||
shared_cache_location = str(PurePath(SolidLSPSettings.get_global_cache_directory(), "lsp", "EclipseJDTLS", "sharedIndex"))
|
||||
|
||||
jre_path = self.runtime_dependency_paths.jre_path
|
||||
lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path
|
||||
|
||||
jdtls_launcher_jar = self.runtime_dependency_paths.jdtls_launcher_jar_path
|
||||
|
||||
os.makedirs(ws_dir, exist_ok=True)
|
||||
|
||||
data_dir = str(PurePath(ws_dir, "data_dir"))
|
||||
jdtls_config_path = str(PurePath(ws_dir, "config_path"))
|
||||
|
||||
jdtls_readonly_config_path = self.runtime_dependency_paths.jdtls_readonly_config_path
|
||||
|
||||
if not os.path.exists(jdtls_config_path):
|
||||
shutil.copytree(jdtls_readonly_config_path, jdtls_config_path)
|
||||
|
||||
for static_path in [
|
||||
jre_path,
|
||||
lombok_jar_path,
|
||||
jdtls_launcher_jar,
|
||||
jdtls_config_path,
|
||||
jdtls_readonly_config_path,
|
||||
]:
|
||||
assert os.path.exists(static_path), static_path
|
||||
|
||||
# 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(
|
||||
[
|
||||
jre_path,
|
||||
"--add-modules=ALL-SYSTEM",
|
||||
"--add-opens",
|
||||
"java.base/java.util=ALL-UNNAMED",
|
||||
"--add-opens",
|
||||
"java.base/java.lang=ALL-UNNAMED",
|
||||
"--add-opens",
|
||||
"java.base/sun.nio.fs=ALL-UNNAMED",
|
||||
"-Declipse.application=org.eclipse.jdt.ls.core.id1",
|
||||
"-Dosgi.bundles.defaultStartLevel=4",
|
||||
"-Declipse.product=org.eclipse.jdt.ls.core.product",
|
||||
"-Djava.import.generatesMetadataFilesAtProjectRoot=false",
|
||||
"-Dfile.encoding=utf8",
|
||||
"-noverify",
|
||||
"-XX:+UseParallelGC",
|
||||
"-XX:GCTimeRatio=4",
|
||||
"-XX:AdaptiveSizePolicyWeight=90",
|
||||
"-Dsun.zip.disableMemoryMapping=true",
|
||||
"-Djava.lsp.joinOnCompletion=true",
|
||||
"-Xmx3G",
|
||||
"-Xms100m",
|
||||
"-Xlog:disable",
|
||||
"-Dlog.level=ALL",
|
||||
f'"-javaagent:{lombok_jar_path}"',
|
||||
f'"-Djdt.core.sharedIndexLocation={shared_cache_location}"',
|
||||
"-jar",
|
||||
f'"{jdtls_launcher_jar}"',
|
||||
"-configuration",
|
||||
f'"{jdtls_config_path}"',
|
||||
"-data",
|
||||
f'"{data_dir}"',
|
||||
]
|
||||
)
|
||||
|
||||
self.service_ready_event = threading.Event()
|
||||
self.intellicode_enable_command_available = threading.Event()
|
||||
self.initialize_searcher_command_available = threading.Event()
|
||||
|
||||
super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java")
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
# Ignore common Java build directories from different build tools:
|
||||
# - Maven: target
|
||||
# - Gradle: build, .gradle
|
||||
# - Eclipse: bin, .settings
|
||||
# - IntelliJ IDEA: out, .idea
|
||||
# - General: classes, dist, lib
|
||||
return super().is_ignored_dirname(dirname) or dirname in [
|
||||
"target", # Maven
|
||||
"build", # Gradle
|
||||
"bin", # Eclipse
|
||||
"out", # IntelliJ IDEA
|
||||
"classes", # General
|
||||
"dist", # General
|
||||
"lib", # General
|
||||
]
|
||||
|
||||
def setupRuntimeDependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> RuntimeDependencyPaths:
|
||||
"""
|
||||
Setup runtime dependencies for EclipseJDTLS.
|
||||
"""
|
||||
platformId = PlatformUtils.get_platform_id()
|
||||
|
||||
with open(str(PurePath(os.path.dirname(__file__), "runtime_dependencies.json")), encoding="utf-8") as f:
|
||||
runtimeDependencies = json.load(f)
|
||||
del runtimeDependencies["_description"]
|
||||
|
||||
os.makedirs(str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static")), exist_ok=True)
|
||||
|
||||
# assert platformId.value in [
|
||||
# "linux-x64",
|
||||
# "win-x64",
|
||||
# ], "Only linux-x64 platform is supported for in multilspy at the moment"
|
||||
|
||||
gradle_path = str(
|
||||
PurePath(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
"static/gradle-7.3.3",
|
||||
)
|
||||
)
|
||||
|
||||
if not os.path.exists(gradle_path):
|
||||
FileUtils.download_and_extract_archive(
|
||||
logger,
|
||||
runtimeDependencies["gradle"]["platform-agnostic"]["url"],
|
||||
str(PurePath(gradle_path).parent),
|
||||
runtimeDependencies["gradle"]["platform-agnostic"]["archiveType"],
|
||||
)
|
||||
|
||||
assert os.path.exists(gradle_path)
|
||||
|
||||
dependency = runtimeDependencies["vscode-java"][platformId.value]
|
||||
vscode_java_path = str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"]))
|
||||
os.makedirs(vscode_java_path, exist_ok=True)
|
||||
jre_home_path = str(PurePath(vscode_java_path, dependency["jre_home_path"]))
|
||||
jre_path = str(PurePath(vscode_java_path, dependency["jre_path"]))
|
||||
lombok_jar_path = str(PurePath(vscode_java_path, dependency["lombok_jar_path"]))
|
||||
jdtls_launcher_jar_path = str(PurePath(vscode_java_path, dependency["jdtls_launcher_jar_path"]))
|
||||
jdtls_readonly_config_path = str(PurePath(vscode_java_path, dependency["jdtls_readonly_config_path"]))
|
||||
if not all(
|
||||
[
|
||||
os.path.exists(vscode_java_path),
|
||||
os.path.exists(jre_home_path),
|
||||
os.path.exists(jre_path),
|
||||
os.path.exists(lombok_jar_path),
|
||||
os.path.exists(jdtls_launcher_jar_path),
|
||||
os.path.exists(jdtls_readonly_config_path),
|
||||
]
|
||||
):
|
||||
FileUtils.download_and_extract_archive(logger, dependency["url"], vscode_java_path, dependency["archiveType"])
|
||||
|
||||
os.chmod(jre_path, stat.S_IEXEC)
|
||||
|
||||
assert os.path.exists(vscode_java_path)
|
||||
assert os.path.exists(jre_home_path)
|
||||
assert os.path.exists(jre_path)
|
||||
assert os.path.exists(lombok_jar_path)
|
||||
assert os.path.exists(jdtls_launcher_jar_path)
|
||||
assert os.path.exists(jdtls_readonly_config_path)
|
||||
|
||||
dependency = runtimeDependencies["intellicode"]["platform-agnostic"]
|
||||
intellicode_directory_path = str(
|
||||
PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"])
|
||||
)
|
||||
os.makedirs(intellicode_directory_path, exist_ok=True)
|
||||
intellicode_jar_path = str(PurePath(intellicode_directory_path, dependency["intellicode_jar_path"]))
|
||||
intellisense_members_path = str(PurePath(intellicode_directory_path, dependency["intellisense_members_path"]))
|
||||
if not all(
|
||||
[
|
||||
os.path.exists(intellicode_directory_path),
|
||||
os.path.exists(intellicode_jar_path),
|
||||
os.path.exists(intellisense_members_path),
|
||||
]
|
||||
):
|
||||
FileUtils.download_and_extract_archive(logger, dependency["url"], intellicode_directory_path, dependency["archiveType"])
|
||||
|
||||
assert os.path.exists(intellicode_directory_path)
|
||||
assert os.path.exists(intellicode_jar_path)
|
||||
assert os.path.exists(intellisense_members_path)
|
||||
|
||||
return RuntimeDependencyPaths(
|
||||
gradle_path=gradle_path,
|
||||
lombok_jar_path=lombok_jar_path,
|
||||
jre_path=jre_path,
|
||||
jre_home_path=jre_home_path,
|
||||
jdtls_launcher_jar_path=jdtls_launcher_jar_path,
|
||||
jdtls_readonly_config_path=jdtls_readonly_config_path,
|
||||
intellicode_jar_path=intellicode_jar_path,
|
||||
intellisense_members_path=intellisense_members_path,
|
||||
)
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize parameters for the EclipseJDTLS server.
|
||||
"""
|
||||
# Look into https://github.com/eclipse/eclipse.jdt.ls/blob/master/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java to understand all the options available
|
||||
with open(str(PurePath(os.path.dirname(__file__), "initialize_params.json")), encoding="utf-8") 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),
|
||||
}
|
||||
]
|
||||
|
||||
assert d["initializationOptions"]["bundles"] == ["intellicode-core.jar"]
|
||||
bundles = [self.runtime_dependency_paths.intellicode_jar_path]
|
||||
d["initializationOptions"]["bundles"] = bundles
|
||||
|
||||
assert d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] == [
|
||||
{"name": "JavaSE-21", "path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64", "default": True}
|
||||
]
|
||||
d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] = [
|
||||
{"name": "JavaSE-21", "path": self.runtime_dependency_paths.jre_home_path, "default": True}
|
||||
]
|
||||
|
||||
for runtime in d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"]:
|
||||
assert "name" in runtime
|
||||
assert "path" in runtime
|
||||
assert os.path.exists(runtime["path"]), f"Runtime required for eclipse_jdtls at path {runtime['path']} does not exist"
|
||||
|
||||
assert d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["home"] == "abs(static/gradle-7.3.3)"
|
||||
d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["home"] = self.runtime_dependency_paths.gradle_path
|
||||
|
||||
d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["java"]["home"] = self.runtime_dependency_paths.jre_path
|
||||
|
||||
return d
|
||||
|
||||
def _start_server(self):
|
||||
"""
|
||||
Starts the Eclipse JDTLS Language Server
|
||||
"""
|
||||
|
||||
def register_capability_handler(params):
|
||||
assert "registrations" in params
|
||||
for registration in params["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"]:
|
||||
self.intellicode_enable_command_available.set()
|
||||
return
|
||||
|
||||
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()
|
||||
|
||||
def execute_client_command_handler(params):
|
||||
assert params["command"] == "_java.reloadBundles.command"
|
||||
assert params["arguments"] == []
|
||||
return []
|
||||
|
||||
def window_log_message(msg):
|
||||
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
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.logger.log("Starting EclipseJDTLS server process", logging.INFO)
|
||||
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 = 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"]
|
||||
|
||||
self.server.notify.initialized({})
|
||||
|
||||
self.server.notify.workspace_did_change_configuration({"settings": initialize_params["initializationOptions"]["settings"]})
|
||||
|
||||
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 = 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
|
||||
self.service_ready_event.wait()
|
||||
@@ -1,849 +0,0 @@
|
||||
{
|
||||
"_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": "Visual Studio Code - Insiders",
|
||||
"version": "1.77.0-insider"
|
||||
},
|
||||
"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": {
|
||||
"bundles": [
|
||||
"intellicode-core.jar"
|
||||
],
|
||||
"workspaceFolders": "[pathlib.Path(repository_absolute_path).as_uri()]",
|
||||
"settings": {
|
||||
"java": {
|
||||
"home": null,
|
||||
"jdt": {
|
||||
"ls": {
|
||||
"java": {
|
||||
"home": null
|
||||
},
|
||||
"vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx1G -Xms100m -Xlog:disable",
|
||||
"lombokSupport": {
|
||||
"enabled": true
|
||||
},
|
||||
"protobufSupport": {
|
||||
"enabled": true
|
||||
},
|
||||
"androidSupport": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"incompleteClasspath": {
|
||||
"severity": "error"
|
||||
}
|
||||
},
|
||||
"configuration": {
|
||||
"checkProjectSettingsExclusions": false,
|
||||
"updateBuildConfiguration": "interactive",
|
||||
"maven": {
|
||||
"userSettings": null,
|
||||
"globalSettings": null,
|
||||
"notCoveredPluginExecutionSeverity": "warning",
|
||||
"defaultMojoExecutionAction": "ignore"
|
||||
},
|
||||
"workspaceCacheLimit": 90,
|
||||
"runtimes": [
|
||||
{
|
||||
"name": "JavaSE-21",
|
||||
"path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"trace": {
|
||||
"server": "verbose"
|
||||
},
|
||||
"import": {
|
||||
"maven": {
|
||||
"enabled": true,
|
||||
"offline": {
|
||||
"enabled": false
|
||||
},
|
||||
"disableTestClasspathFlag": false
|
||||
},
|
||||
"gradle": {
|
||||
"enabled": true,
|
||||
"wrapper": {
|
||||
"enabled": true
|
||||
},
|
||||
"version": null,
|
||||
"home": "abs(static/gradle-7.3.3)",
|
||||
"java": {
|
||||
"home": "abs(static/launch_jres/21.0.7-linux-x86_64)"
|
||||
},
|
||||
"offline": {
|
||||
"enabled": false
|
||||
},
|
||||
"arguments": null,
|
||||
"jvmArguments": null,
|
||||
"user": {
|
||||
"home": null
|
||||
},
|
||||
"annotationProcessing": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"exclusions": [
|
||||
"**/node_modules/**",
|
||||
"**/.metadata/**",
|
||||
"**/archetype-resources/**",
|
||||
"**/META-INF/maven/**"
|
||||
],
|
||||
"generatesMetadataFilesAtProjectRoot": false
|
||||
},
|
||||
"maven": {
|
||||
"downloadSources": true,
|
||||
"updateSnapshots": true
|
||||
},
|
||||
"eclipse": {
|
||||
"downloadSources": true
|
||||
},
|
||||
"referencesCodeLens": {
|
||||
"enabled": true
|
||||
},
|
||||
"signatureHelp": {
|
||||
"enabled": true,
|
||||
"description": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"implementationsCodeLens": {
|
||||
"enabled": true
|
||||
},
|
||||
"format": {
|
||||
"enabled": true,
|
||||
"settings": {
|
||||
"url": null,
|
||||
"profile": null
|
||||
},
|
||||
"comments": {
|
||||
"enabled": true
|
||||
},
|
||||
"onType": {
|
||||
"enabled": true
|
||||
},
|
||||
"insertSpaces": true,
|
||||
"tabSize": 4
|
||||
},
|
||||
"saveActions": {
|
||||
"organizeImports": false
|
||||
},
|
||||
"project": {
|
||||
"referencedLibraries": [
|
||||
"lib/**/*.jar"
|
||||
],
|
||||
"importOnFirstTimeStartup": "automatic",
|
||||
"importHint": true,
|
||||
"resourceFilters": [
|
||||
"node_modules",
|
||||
"\\.git"
|
||||
],
|
||||
"encoding": "ignore",
|
||||
"exportJar": {
|
||||
"targetPath": "${workspaceFolder}/${workspaceFolderBasename}.jar"
|
||||
}
|
||||
},
|
||||
"contentProvider": {
|
||||
"preferred": null
|
||||
},
|
||||
"autobuild": {
|
||||
"enabled": true
|
||||
},
|
||||
"maxConcurrentBuilds": 1,
|
||||
"recommendations": {
|
||||
"dependency": {
|
||||
"analytics": {
|
||||
"show": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"completion": {
|
||||
"maxResults": 0,
|
||||
"enabled": true,
|
||||
"guessMethodArguments": true,
|
||||
"favoriteStaticMembers": [
|
||||
"org.junit.Assert.*",
|
||||
"org.junit.Assume.*",
|
||||
"org.junit.jupiter.api.Assertions.*",
|
||||
"org.junit.jupiter.api.Assumptions.*",
|
||||
"org.junit.jupiter.api.DynamicContainer.*",
|
||||
"org.junit.jupiter.api.DynamicTest.*",
|
||||
"org.mockito.Mockito.*",
|
||||
"org.mockito.ArgumentMatchers.*",
|
||||
"org.mockito.Answers.*"
|
||||
],
|
||||
"filteredTypes": [
|
||||
"java.awt.*",
|
||||
"com.sun.*",
|
||||
"sun.*",
|
||||
"jdk.*",
|
||||
"org.graalvm.*",
|
||||
"io.micrometer.shaded.*"
|
||||
],
|
||||
"importOrder": [
|
||||
"#",
|
||||
"java",
|
||||
"javax",
|
||||
"org",
|
||||
"com",
|
||||
""
|
||||
],
|
||||
"postfix": {
|
||||
"enabled": false
|
||||
},
|
||||
"matchCase": "off"
|
||||
},
|
||||
"foldingRange": {
|
||||
"enabled": true
|
||||
},
|
||||
"progressReports": {
|
||||
"enabled": false
|
||||
},
|
||||
"codeGeneration": {
|
||||
"hashCodeEquals": {
|
||||
"useJava7Objects": false,
|
||||
"useInstanceof": false
|
||||
},
|
||||
"useBlocks": false,
|
||||
"generateComments": false,
|
||||
"toString": {
|
||||
"template": "${object.className} [${member.name()}=${member.value}, ${otherMembers}]",
|
||||
"codeStyle": "STRING_CONCATENATION",
|
||||
"skipNullValues": false,
|
||||
"listArrayContents": true,
|
||||
"limitElements": 0
|
||||
},
|
||||
"insertionLocation": "afterCursor"
|
||||
},
|
||||
"selectionRange": {
|
||||
"enabled": true
|
||||
},
|
||||
"showBuildStatusOnStart": {
|
||||
"enabled": "notification"
|
||||
},
|
||||
"server": {
|
||||
"launchMode": "Standard"
|
||||
},
|
||||
"sources": {
|
||||
"organizeImports": {
|
||||
"starThreshold": 99,
|
||||
"staticStarThreshold": 99
|
||||
}
|
||||
},
|
||||
"imports": {
|
||||
"gradle": {
|
||||
"wrapper": {
|
||||
"checksums": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"templates": {
|
||||
"fileHeader": [],
|
||||
"typeComment": []
|
||||
},
|
||||
"references": {
|
||||
"includeAccessors": true,
|
||||
"includeDecompiledSources": true
|
||||
},
|
||||
"typeHierarchy": {
|
||||
"lazyLoad": false
|
||||
},
|
||||
"settings": {
|
||||
"url": null
|
||||
},
|
||||
"symbols": {
|
||||
"includeSourceMethodDeclarations": false
|
||||
},
|
||||
"quickfix": {
|
||||
"showAt": "line"
|
||||
},
|
||||
"inlayHints": {
|
||||
"parameterNames": {
|
||||
"enabled": "literals",
|
||||
"exclusions": []
|
||||
}
|
||||
},
|
||||
"codeAction": {
|
||||
"sortMembers": {
|
||||
"avoidVolatileChanges": true
|
||||
}
|
||||
},
|
||||
"compile": {
|
||||
"nullAnalysis": {
|
||||
"nonnull": [
|
||||
"javax.annotation.Nonnull",
|
||||
"org.eclipse.jdt.annotation.NonNull",
|
||||
"org.springframework.lang.NonNull"
|
||||
],
|
||||
"nullable": [
|
||||
"javax.annotation.Nullable",
|
||||
"org.eclipse.jdt.annotation.Nullable",
|
||||
"org.springframework.lang.Nullable"
|
||||
],
|
||||
"mode": "automatic"
|
||||
}
|
||||
},
|
||||
"cleanup": {
|
||||
"actionsOnSave": []
|
||||
},
|
||||
"sharedIndexes": {
|
||||
"enabled": "auto",
|
||||
"location": ""
|
||||
},
|
||||
"refactoring": {
|
||||
"extract": {
|
||||
"interface": {
|
||||
"replace": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"logLevel": "verbose",
|
||||
"settings": {
|
||||
"showHex": false,
|
||||
"showStaticVariables": false,
|
||||
"showQualifiedNames": false,
|
||||
"showLogicalStructure": true,
|
||||
"showToString": true,
|
||||
"maxStringLength": 0,
|
||||
"numericPrecision": 0,
|
||||
"hotCodeReplace": "manual",
|
||||
"enableRunDebugCodeLens": true,
|
||||
"forceBuildBeforeLaunch": true,
|
||||
"onBuildFailureProceed": false,
|
||||
"console": "integratedTerminal",
|
||||
"exceptionBreakpoint": {
|
||||
"skipClasses": []
|
||||
},
|
||||
"stepping": {
|
||||
"skipClasses": [],
|
||||
"skipSynthetics": false,
|
||||
"skipStaticInitializers": false,
|
||||
"skipConstructors": false
|
||||
},
|
||||
"jdwp": {
|
||||
"limitOfVariablesPerJdwpRequest": 100,
|
||||
"requestTimeout": 3000,
|
||||
"async": "auto"
|
||||
},
|
||||
"vmArgs": ""
|
||||
}
|
||||
},
|
||||
"silentNotification": false,
|
||||
"dependency": {
|
||||
"showMembers": false,
|
||||
"syncWithFolderExplorer": true,
|
||||
"autoRefresh": true,
|
||||
"refreshDelay": 2000,
|
||||
"packagePresentation": "flat"
|
||||
},
|
||||
"help": {
|
||||
"firstView": "auto",
|
||||
"showReleaseNotes": true,
|
||||
"collectErrorLog": false
|
||||
},
|
||||
"test": {
|
||||
"defaultConfig": "",
|
||||
"config": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"extendedClientCapabilities": {
|
||||
"progressReportProvider": false,
|
||||
"classFileContentsSupport": true,
|
||||
"overrideMethodsPromptSupport": true,
|
||||
"hashCodeEqualsPromptSupport": true,
|
||||
"advancedOrganizeImportsSupport": true,
|
||||
"generateToStringPromptSupport": true,
|
||||
"advancedGenerateAccessorsSupport": true,
|
||||
"generateConstructorsPromptSupport": true,
|
||||
"generateDelegateMethodsPromptSupport": true,
|
||||
"advancedExtractRefactoringSupport": true,
|
||||
"inferSelectionSupport": [
|
||||
"extractMethod",
|
||||
"extractVariable",
|
||||
"extractField"
|
||||
],
|
||||
"moveRefactoringSupport": true,
|
||||
"clientHoverProvider": true,
|
||||
"clientDocumentSymbolProvider": true,
|
||||
"gradleChecksumWrapperPromptSupport": true,
|
||||
"resolveAdditionalTextEditsSupport": true,
|
||||
"advancedIntroduceParameterRefactoringSupport": true,
|
||||
"actionableRuntimeNotificationSupport": true,
|
||||
"shouldLanguageServerExitOnShutdown": true,
|
||||
"onCompletionItemSelectedCommand": "editor.action.triggerParameterHints",
|
||||
"extractInterfaceSupport": true,
|
||||
"advancedUpgradeGradleSupport": true
|
||||
},
|
||||
"triggerFiles": []
|
||||
},
|
||||
"trace": "verbose",
|
||||
"workspaceFolders": "[\n {\n \"uri\": pathlib.Path(repository_absolute_path).as_uri(),\n \"name\": os.path.basename(repository_absolute_path),\n }\n ]"
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"_description": "This file lists the runtime dependencies for the Java Language Server",
|
||||
"gradle": {
|
||||
"platform-agnostic": {
|
||||
"url": "https://services.gradle.org/distributions/gradle-7.3.3-bin.zip",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": "."
|
||||
}
|
||||
},
|
||||
"vscode-java": {
|
||||
"darwin-arm64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": "vscode-java"
|
||||
},
|
||||
"osx-arm64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": "vscode-java",
|
||||
"jre_home_path": "extension/jre/21.0.7-macosx-aarch64",
|
||||
"jre_path": "extension/jre/21.0.7-macosx-aarch64/bin/java",
|
||||
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
|
||||
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
|
||||
"jdtls_readonly_config_path": "extension/server/config_mac_arm"
|
||||
},
|
||||
"osx-x64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": "vscode-java",
|
||||
"jre_home_path": "extension/jre/21.0.7-macosx-x86_64",
|
||||
"jre_path": "extension/jre/21.0.7-macosx-x86_64/bin/java",
|
||||
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
|
||||
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
|
||||
"jdtls_readonly_config_path": "extension/server/config_mac"
|
||||
},
|
||||
"linux-arm64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": "vscode-java"
|
||||
},
|
||||
"linux-x64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": "vscode-java",
|
||||
"jre_home_path": "extension/jre/21.0.7-linux-x86_64",
|
||||
"jre_path": "extension/jre/21.0.7-linux-x86_64/bin/java",
|
||||
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
|
||||
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
|
||||
"jdtls_readonly_config_path": "extension/server/config_linux"
|
||||
},
|
||||
"win-x64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": "vscode-java",
|
||||
"jre_home_path": "extension/jre/21.0.7-win32-x86_64",
|
||||
"jre_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe",
|
||||
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
|
||||
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
|
||||
"jdtls_readonly_config_path": "extension/server/config_win"
|
||||
}
|
||||
},
|
||||
"intellicode": {
|
||||
"platform-agnostic": {
|
||||
"url": "https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/1.2.30/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage",
|
||||
"alternate_url": "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioExptTeam/vsextensions/vscodeintellicode/1.2.30/vspackage",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": "intellicode",
|
||||
"intellicode_jar_path": "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar",
|
||||
"intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
# Elixir Language Server Integration
|
||||
|
||||
This directory contains the integration for Elixir language support using [Next LS](https://github.com/elixir-tools/next-ls) from the elixir-tools project.
|
||||
|
||||
> **⚠️ Windows Not Supported**: Next LS does not provide Windows binaries, so Elixir language server integration is only available on Linux and macOS.
|
||||
|
||||
## Known Issues
|
||||
|
||||
### Next LS v0.23.3 Timeout Enumeration Bug
|
||||
There is a known intermittent bug in Next LS v0.23.3 where `textDocument/definition` requests can fail with:
|
||||
```
|
||||
Protocol.UndefinedError: protocol Enumerable not implemented for :timeout of type Atom
|
||||
```
|
||||
|
||||
This bug is tracked in [Next LS Issue #543](https://github.com/elixir-tools/next-ls/issues/543) and primarily occurs in CI environments. The affected test (`test_request_defining_symbol_none`) is marked as expected to fail until this upstream bug is resolved.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using the Elixir language server integration, you need to have:
|
||||
|
||||
1. **Elixir** installed and available in your PATH
|
||||
- Install from: https://elixir-lang.org/install.html
|
||||
- Verify with: `elixir --version`
|
||||
|
||||
2. **Next LS** installed and available in your PATH
|
||||
- Install from: https://github.com/elixir-tools/next-ls#installation
|
||||
- Verify with: `nextls --version`
|
||||
|
||||
## Features
|
||||
|
||||
The Elixir integration provides:
|
||||
|
||||
- **Language Server Protocol (LSP) support** via Next LS
|
||||
- **File extension recognition** for `.ex` and `.exs` files
|
||||
- **Project structure awareness** with proper handling of Elixir-specific directories:
|
||||
- `_build/` - Compiled artifacts (ignored)
|
||||
- `deps/` - Dependencies (ignored)
|
||||
- `.elixir_ls/` - ElixirLS artifacts (ignored)
|
||||
- `cover/` - Coverage reports (ignored)
|
||||
- `lib/` - Source code (not ignored)
|
||||
- `test/` - Test files (not ignored)
|
||||
|
||||
## Configuration
|
||||
|
||||
The integration uses the default Next LS configuration with:
|
||||
|
||||
- **MIX_ENV**: `dev`
|
||||
- **MIX_TARGET**: `host`
|
||||
- **Experimental completions**: Disabled by default
|
||||
- **Credo extension**: Enabled by default
|
||||
|
||||
## Usage
|
||||
|
||||
The Elixir language server is automatically selected when working with Elixir projects. It will be used for:
|
||||
|
||||
- Code completion
|
||||
- Go to definition
|
||||
- Find references
|
||||
- Document symbols
|
||||
- Hover information
|
||||
- Code formatting
|
||||
- Diagnostics (via Credo integration)
|
||||
|
||||
### Important: Project Compilation
|
||||
|
||||
Next LS requires your Elixir project to be **compiled** for optimal performance, especially for:
|
||||
- Cross-file reference resolution
|
||||
- Complete symbol information
|
||||
- Accurate go-to-definition
|
||||
|
||||
**For production use**: Ensure your project is compiled with `mix compile` before using the language server.
|
||||
|
||||
**For testing**: The test suite automatically compiles the test repositories before running tests to ensure optimal Next LS performance.
|
||||
|
||||
## Testing
|
||||
|
||||
Run the Elixir-specific tests with:
|
||||
|
||||
```bash
|
||||
pytest test/solidlsp/elixir/ -m elixir
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
- **Main class**: `ElixirTools` in `elixir_tools.py`
|
||||
- **Initialization parameters**: Defined in `initialize_params.json`
|
||||
- **Language identifier**: `"elixir"`
|
||||
- **Command**: `nextls --stdio`
|
||||
|
||||
The implementation follows the same patterns as other language servers in this project, inheriting from `SolidLanguageServer` and providing Elixir-specific configuration and behavior.
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import stat
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from pathlib import PurePath
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.ls_utils import FileUtils, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
|
||||
class ElixirTools(SolidLanguageServer):
|
||||
"""
|
||||
Provides Elixir specific instantiation of the LanguageServer class using Next LS from elixir-tools.
|
||||
"""
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
# For Elixir projects, we should ignore:
|
||||
# - _build: compiled artifacts
|
||||
# - deps: dependencies
|
||||
# - node_modules: if the project has JavaScript components
|
||||
# - .elixir_ls: ElixirLS artifacts (in case both are present)
|
||||
# - cover: coverage reports
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["_build", "deps", "node_modules", ".elixir_ls", "cover"]
|
||||
|
||||
def _is_next_ls_internal_file(self, abs_path: str) -> bool:
|
||||
"""Check if an absolute path is a Next LS internal file that should be ignored."""
|
||||
return any(
|
||||
pattern in abs_path
|
||||
for pattern in [
|
||||
".burrito", # Next LS runtime directory
|
||||
"next_ls_erts-", # Next LS Erlang runtime
|
||||
"_next_ls_private_", # Next LS private files
|
||||
"/priv/monkey/", # Next LS monkey patching directory
|
||||
]
|
||||
)
|
||||
|
||||
@override
|
||||
def _send_references_request(self, relative_file_path: str, line: int, column: int):
|
||||
"""Override to filter out Next LS internal files from references."""
|
||||
from solidlsp.ls_utils import PathUtils
|
||||
|
||||
# Get the raw response from the parent implementation
|
||||
raw_response = super()._send_references_request(relative_file_path, line, column)
|
||||
|
||||
if raw_response is None:
|
||||
return None
|
||||
|
||||
# Filter out Next LS internal files
|
||||
filtered_response = []
|
||||
for item in raw_response:
|
||||
if isinstance(item, dict) and "uri" in item:
|
||||
abs_path = PathUtils.uri_to_path(item["uri"])
|
||||
if self._is_next_ls_internal_file(abs_path):
|
||||
self.logger.log(f"Filtering out Next LS internal file: {abs_path}", logging.DEBUG)
|
||||
continue
|
||||
filtered_response.append(item)
|
||||
|
||||
return filtered_response
|
||||
|
||||
@staticmethod
|
||||
def _get_elixir_version():
|
||||
"""Get the installed Elixir version or None if not found."""
|
||||
try:
|
||||
result = subprocess.run(["elixir", "--version"], capture_output=True, text=True, check=False)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
return None
|
||||
|
||||
def setupRuntimeDependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
|
||||
"""
|
||||
Setup runtime dependencies for Next LS.
|
||||
Downloads the Next LS binary for the current platform and returns the path to the executable.
|
||||
"""
|
||||
# Check if Elixir is available first
|
||||
elixir_version = self._get_elixir_version()
|
||||
if not elixir_version:
|
||||
raise RuntimeError(
|
||||
"Elixir is not installed. Please install Elixir from https://elixir-lang.org/install.html and make sure it is added to your PATH."
|
||||
)
|
||||
|
||||
logger.log(f"Found Elixir: {elixir_version}", logging.INFO)
|
||||
|
||||
platformId = PlatformUtils.get_platform_id()
|
||||
|
||||
with open(str(PurePath(os.path.dirname(__file__), "runtime_dependencies.json")), encoding="utf-8") as f:
|
||||
runtimeDependencies = json.load(f)
|
||||
del runtimeDependencies["_description"]
|
||||
|
||||
os.makedirs(str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static")), exist_ok=True)
|
||||
|
||||
# Map platform IDs to runtime dependency keys
|
||||
platform_mapping = {
|
||||
"linux-x64": "linux-x64",
|
||||
"osx-x64": "darwin-x64",
|
||||
"osx-arm64": "darwin-arm64",
|
||||
"darwin-x64": "darwin-x64",
|
||||
"darwin-arm64": "darwin-arm64",
|
||||
"win-x64": "win-x64",
|
||||
}
|
||||
|
||||
platform_key = platform_mapping.get(platformId.value)
|
||||
if not platform_key:
|
||||
raise RuntimeError(f"Unsupported platform for Next LS: {platformId.value}")
|
||||
|
||||
# Check for Windows and provide a helpful error message
|
||||
if platformId.value.startswith("win"):
|
||||
raise RuntimeError(
|
||||
"Windows is not supported by Next LS. The Next LS project does not provide Windows binaries. "
|
||||
"Consider using Windows Subsystem for Linux (WSL) or a virtual machine with Linux/macOS."
|
||||
)
|
||||
|
||||
dependency = runtimeDependencies["next_ls"][platform_key]
|
||||
next_ls_dir = str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"]))
|
||||
os.makedirs(next_ls_dir, exist_ok=True)
|
||||
|
||||
executable_path = str(PurePath(next_ls_dir, dependency["executable_name"]))
|
||||
binary_path = str(PurePath(next_ls_dir, dependency["binary_name"]))
|
||||
|
||||
if not os.path.exists(executable_path):
|
||||
logger.log(f"Downloading Next LS binary from {dependency['url']}", logging.INFO)
|
||||
FileUtils.download_file(logger, dependency["url"], binary_path)
|
||||
|
||||
# Make the binary executable on Unix-like systems
|
||||
if not platformId.value.startswith("win"):
|
||||
os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
||||
|
||||
# Create a symlink or copy with the expected name
|
||||
if binary_path != executable_path:
|
||||
if os.path.exists(executable_path):
|
||||
os.remove(executable_path)
|
||||
if platformId.value.startswith("win"):
|
||||
# On Windows, copy the file
|
||||
import shutil
|
||||
|
||||
shutil.copy2(binary_path, executable_path)
|
||||
else:
|
||||
# On Unix-like systems, create a symlink
|
||||
os.symlink(os.path.basename(binary_path), executable_path)
|
||||
|
||||
assert os.path.exists(executable_path), f"Next LS executable not found at {executable_path}"
|
||||
|
||||
logger.log(f"Next LS binary ready at: {executable_path}", logging.INFO)
|
||||
return executable_path
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
|
||||
nextls_executable_path = self.setupRuntimeDependencies(logger, config)
|
||||
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=f'"{nextls_executable_path}" --stdio', cwd=repository_root_path),
|
||||
"elixir",
|
||||
)
|
||||
self.server_ready = threading.Event()
|
||||
self.request_id = 0
|
||||
|
||||
# Set generous timeout for Next LS which can be slow to initialize and respond
|
||||
self.set_request_timeout(180.0) # 60 seconds for all environments
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Next LS Language Server.
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") 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
|
||||
|
||||
def _start_server(self):
|
||||
"""Start Next LS server process"""
|
||||
|
||||
def register_capability_handler(params):
|
||||
return
|
||||
|
||||
def window_log_message(msg):
|
||||
"""Handle window/logMessage notifications from Next LS"""
|
||||
message_text = msg.get("message", "")
|
||||
self.logger.log(f"LSP: window/logMessage: {message_text}", logging.INFO)
|
||||
|
||||
# Check for the specific Next LS readiness signal
|
||||
# Based on Next LS source: "Runtime for folder #{name} is ready..."
|
||||
if "Runtime for folder" in message_text and "is ready..." in message_text:
|
||||
self.logger.log("Next LS runtime is ready based on official log message", logging.INFO)
|
||||
self.server_ready.set()
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
def check_server_ready(params):
|
||||
"""
|
||||
Handle $/progress notifications from Next LS.
|
||||
Keep as fallback for error detection, but primary readiness detection
|
||||
is now done via window/logMessage handler.
|
||||
"""
|
||||
value = params.get("value", {})
|
||||
|
||||
# Check for initialization completion progress (fallback signal)
|
||||
if value.get("kind") == "end":
|
||||
message = value.get("message", "")
|
||||
if "has initialized!" in message:
|
||||
self.logger.log("Next LS initialization progress completed", logging.INFO)
|
||||
# Note: We don't set server_ready here - we wait for the log message
|
||||
|
||||
def work_done_progress(params):
|
||||
"""
|
||||
Handle $/workDoneProgress notifications from Next LS.
|
||||
Keep for completeness but primary readiness detection is via window/logMessage.
|
||||
"""
|
||||
value = params.get("value", {})
|
||||
if value.get("kind") == "end":
|
||||
self.logger.log("Next LS work done progress completed", logging.INFO)
|
||||
# Note: We don't set server_ready here - we wait for the log message
|
||||
|
||||
self.server.on_request("client/registerCapability", register_capability_handler)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_notification("$/progress", check_server_ready)
|
||||
self.server.on_notification("window/workDoneProgress/create", do_nothing)
|
||||
self.server.on_notification("$/workDoneProgress", work_done_progress)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
|
||||
self.logger.log("Starting Next LS server process", logging.INFO)
|
||||
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 = self.server.send.initialize(initialize_params)
|
||||
|
||||
# Verify server capabilities - be more lenient with Next LS
|
||||
self.logger.log(f"Next LS capabilities: {list(init_response['capabilities'].keys())}", logging.INFO)
|
||||
|
||||
# Next LS may not provide all capabilities immediately, so we check for basic ones
|
||||
assert "textDocumentSync" in init_response["capabilities"], f"Missing textDocumentSync in {init_response['capabilities']}"
|
||||
|
||||
# Some capabilities might be optional or provided later
|
||||
if "completionProvider" not in init_response["capabilities"]:
|
||||
self.logger.log("Warning: completionProvider not available in initial capabilities", logging.WARNING)
|
||||
if "definitionProvider" not in init_response["capabilities"]:
|
||||
self.logger.log("Warning: definitionProvider not available in initial capabilities", logging.WARNING)
|
||||
|
||||
self.server.notify.initialized({})
|
||||
self.completions_available.set()
|
||||
|
||||
# Wait for Next LS to send the specific "Runtime for folder X is ready..." log message
|
||||
# This is the authoritative signal that Next LS is truly ready for requests
|
||||
ready_timeout = 180.0
|
||||
self.logger.log(f"Waiting up to {ready_timeout} seconds for Next LS runtime readiness...", logging.INFO)
|
||||
|
||||
if self.server_ready.wait(timeout=ready_timeout):
|
||||
self.logger.log("Next LS is ready and available for requests", logging.INFO)
|
||||
|
||||
# Add a small settling period to ensure background indexing is complete
|
||||
# Next LS often continues compilation/indexing in background after ready signal
|
||||
settling_time = 120.0
|
||||
self.logger.log(f"Allowing {settling_time} seconds for Next LS background indexing to complete...", logging.INFO)
|
||||
time.sleep(settling_time)
|
||||
self.logger.log("Next LS settling period complete", logging.INFO)
|
||||
else:
|
||||
error_msg = f"Next LS failed to initialize within {ready_timeout} seconds. This may indicate a problem with the Elixir installation, project compilation, or Next LS itself."
|
||||
self.logger.log(error_msg, logging.ERROR)
|
||||
raise RuntimeError(error_msg)
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"_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",
|
||||
"initializationOptions": {
|
||||
"mix_env": "dev",
|
||||
"mix_target": "host",
|
||||
"experimental": {
|
||||
"completions": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"credo": {
|
||||
"enable": true,
|
||||
"cli_options": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {
|
||||
"didSave": true,
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"completion": {
|
||||
"dynamicRegistration": true,
|
||||
"completionItem": {
|
||||
"snippetSupport": true,
|
||||
"documentationFormat": [
|
||||
"markdown",
|
||||
"plaintext"
|
||||
]
|
||||
}
|
||||
},
|
||||
"definition": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"references": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": true,
|
||||
"hierarchicalDocumentSymbolSupport": 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
|
||||
]
|
||||
}
|
||||
},
|
||||
"hover": {
|
||||
"dynamicRegistration": true,
|
||||
"contentFormat": [
|
||||
"markdown",
|
||||
"plaintext"
|
||||
]
|
||||
},
|
||||
"formatting": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"codeAction": {
|
||||
"dynamicRegistration": true,
|
||||
"codeActionLiteralSupport": {
|
||||
"codeActionKind": {
|
||||
"valueSet": [
|
||||
"quickfix",
|
||||
"refactor",
|
||||
"refactor.extract",
|
||||
"refactor.inline",
|
||||
"refactor.rewrite",
|
||||
"source",
|
||||
"source.organizeImports"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": true,
|
||||
"didChangeConfiguration": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"executeCommand": {
|
||||
"dynamicRegistration": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": "$uri",
|
||||
"name": "$name"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"_description": "This file lists the runtime dependencies for the Elixir Language Server (Next LS)",
|
||||
"next_ls": {
|
||||
"linux-x64": {
|
||||
"url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_linux_amd64",
|
||||
"archiveType": "binary",
|
||||
"relative_extraction_path": "next_ls",
|
||||
"binary_name": "next_ls_linux_amd64",
|
||||
"executable_name": "nextls"
|
||||
},
|
||||
"darwin-x64": {
|
||||
"url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_amd64",
|
||||
"archiveType": "binary",
|
||||
"relative_extraction_path": "next_ls",
|
||||
"binary_name": "next_ls_darwin_amd64",
|
||||
"executable_name": "nextls"
|
||||
},
|
||||
"darwin-arm64": {
|
||||
"url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_darwin_arm64",
|
||||
"archiveType": "binary",
|
||||
"relative_extraction_path": "next_ls",
|
||||
"binary_name": "next_ls_darwin_arm64",
|
||||
"executable_name": "nextls"
|
||||
},
|
||||
"win-x64": {
|
||||
"url": "https://github.com/elixir-tools/next-ls/releases/download/v0.23.3/next_ls_windows_amd64.exe",
|
||||
"archiveType": "binary",
|
||||
"relative_extraction_path": "next_ls",
|
||||
"binary_name": "next_ls_windows_amd64.exe",
|
||||
"executable_name": "nextls.exe"
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
-27
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
@@ -49,19 +48,19 @@ class Gopls(SolidLanguageServer):
|
||||
return None
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def setup_runtime_dependency(cls):
|
||||
@staticmethod
|
||||
def _setup_runtime_dependency():
|
||||
"""
|
||||
Check if required Go runtime dependencies are available.
|
||||
Raises RuntimeError with helpful message if dependencies are missing.
|
||||
"""
|
||||
go_version = cls._get_go_version()
|
||||
go_version = Gopls._get_go_version()
|
||||
if not go_version:
|
||||
raise RuntimeError(
|
||||
"Go is not installed. Please install Go from https://golang.org/doc/install and make sure it is added to your PATH."
|
||||
)
|
||||
|
||||
gopls_version = cls._get_gopls_version()
|
||||
gopls_version = Gopls._get_gopls_version()
|
||||
if not gopls_version:
|
||||
raise RuntimeError(
|
||||
"Found a Go version but gopls is not installed.\n"
|
||||
@@ -72,7 +71,7 @@ class Gopls(SolidLanguageServer):
|
||||
return True
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
|
||||
self.setup_runtime_dependency()
|
||||
self._setup_runtime_dependency()
|
||||
|
||||
super().__init__(
|
||||
config,
|
||||
@@ -84,29 +83,37 @@ class Gopls(SolidLanguageServer):
|
||||
self.server_ready = threading.Event()
|
||||
self.request_id = 0
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the TypeScript Language Server.
|
||||
Returns the initialize params for the Go Language Server.
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") 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
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
initialize_params = {
|
||||
"locale": "en",
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {"didSave": True, "dynamicRegistration": True},
|
||||
"definition": {"dynamicRegistration": True},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": True,
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
},
|
||||
},
|
||||
"workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
|
||||
},
|
||||
"processId": os.getpid(),
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": root_uri,
|
||||
"name": os.path.basename(repository_absolute_path),
|
||||
}
|
||||
],
|
||||
}
|
||||
return initialize_params
|
||||
|
||||
def _start_server(self):
|
||||
"""Start gopls server process"""
|
||||
+46
-60
@@ -2,12 +2,10 @@
|
||||
Provides PHP specific instantiation of the LanguageServer class using Intelephense.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
from time import sleep
|
||||
|
||||
from overrides import override
|
||||
@@ -19,6 +17,8 @@ from solidlsp.ls_utils import PlatformId, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
from .common import RuntimeDependency, RuntimeDependencyCollection
|
||||
|
||||
|
||||
class Intelephense(SolidLanguageServer):
|
||||
"""
|
||||
@@ -33,9 +33,10 @@ class Intelephense(SolidLanguageServer):
|
||||
# - cache: commonly used for caching
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "cache"]
|
||||
|
||||
def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
|
||||
@classmethod
|
||||
def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
|
||||
"""
|
||||
Setup runtime dependencies for Intelephense.
|
||||
Setup runtime dependencies for Intelephense and return the command to start the server.
|
||||
"""
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
|
||||
@@ -50,13 +51,6 @@ class Intelephense(SolidLanguageServer):
|
||||
]
|
||||
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy PHP at the moment"
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f:
|
||||
d = json.load(f)
|
||||
del d["_description"]
|
||||
|
||||
runtime_dependencies = d.get("runtimeDependencies", [])
|
||||
intelephense_ls_dir = os.path.join(os.path.dirname(__file__), "static", "php-lsp")
|
||||
|
||||
# Verify both node and npm are installed
|
||||
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."
|
||||
@@ -64,69 +58,61 @@ class Intelephense(SolidLanguageServer):
|
||||
assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
|
||||
|
||||
# Install intelephense if not already installed
|
||||
if not os.path.exists(intelephense_ls_dir):
|
||||
os.makedirs(intelephense_ls_dir, exist_ok=True)
|
||||
for dependency in runtime_dependencies:
|
||||
# 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=intelephense_ls_dir,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
else:
|
||||
# On Unix-like systems, run as non-root user
|
||||
import pwd
|
||||
|
||||
user = pwd.getpwuid(os.getuid()).pw_name
|
||||
subprocess.run(
|
||||
dependency["command"],
|
||||
shell=True,
|
||||
check=True,
|
||||
user=user,
|
||||
cwd=intelephense_ls_dir,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
intelephense_ls_dir = os.path.join(cls.ls_resources_dir(), "php-lsp")
|
||||
os.makedirs(intelephense_ls_dir, exist_ok=True)
|
||||
intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense")
|
||||
assert os.path.exists(intelephense_executable_path), "intelephense executable not found. Please install intelephense and try again."
|
||||
if not os.path.exists(intelephense_executable_path):
|
||||
deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="intelephense",
|
||||
command="npm install --prefix ./ intelephense@1.14.4",
|
||||
platform_id="any",
|
||||
)
|
||||
]
|
||||
)
|
||||
deps.install(logger, intelephense_ls_dir)
|
||||
|
||||
assert os.path.exists(
|
||||
intelephense_executable_path
|
||||
), f"intelephense executable not found at {intelephense_executable_path}, something went wrong."
|
||||
|
||||
return f"{intelephense_executable_path} --stdio"
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
|
||||
# Setup runtime dependencies before initializing
|
||||
intelephense_cmd = self.setup_runtime_dependencies(logger, config)
|
||||
intelephense_cmd = self._setup_runtime_dependencies(logger, config)
|
||||
|
||||
super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), "php")
|
||||
self.request_id = 0
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the TypeScript Language Server.
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f:
|
||||
d = json.load(f)
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
initialize_params = {
|
||||
"locale": "en",
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {"didSave": True, "dynamicRegistration": True},
|
||||
"definition": {"dynamicRegistration": True},
|
||||
},
|
||||
"workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
|
||||
},
|
||||
"processId": os.getpid(),
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": root_uri,
|
||||
"name": os.path.basename(repository_absolute_path),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
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
|
||||
return initialize_params
|
||||
|
||||
def _start_server(self):
|
||||
"""Start Intelephense server process"""
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"_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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"_description": "Used to download the runtime dependencies for running intelephense. Obtained from https://www.npmjs.com/package/intelephense",
|
||||
"runtimeDependencies": [
|
||||
{
|
||||
"id": "intelephense",
|
||||
"description": "Intelephense package for Linux, OSX, and Windows. Both x64 and arm64 are supported.",
|
||||
"command": "npm install --prefix ./ intelephense@1.14.4"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,905 +0,0 @@
|
||||
{
|
||||
"_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": "Visual Studio Code - Insiders",
|
||||
"version": "1.81.0-insider"
|
||||
},
|
||||
"locale": "en",
|
||||
"rootPath": "$rootPath",
|
||||
"rootUri": "$rootUri",
|
||||
"capabilities": {
|
||||
"workspace": {
|
||||
"applyEdit": true,
|
||||
"workspaceEdit": {
|
||||
"documentChanges": true,
|
||||
"resourceOperations": [
|
||||
"create",
|
||||
"rename",
|
||||
"delete"
|
||||
],
|
||||
"failureHandling": "textOnlyTransactional",
|
||||
"normalizesLineEndings": true,
|
||||
"changeAnnotationSupport": {
|
||||
"groupsOnLabel": true
|
||||
}
|
||||
},
|
||||
"configuration": 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
|
||||
},
|
||||
"didChangeConfiguration": {
|
||||
"dynamicRegistration": 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": true,
|
||||
"commitCharactersSupport": true,
|
||||
"documentationFormat": [
|
||||
"markdown",
|
||||
"plaintext"
|
||||
],
|
||||
"deprecatedSupport": true,
|
||||
"preselectSupport": true,
|
||||
"tagSupport": {
|
||||
"valueSet": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"insertReplaceSupport": true,
|
||||
"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,
|
||||
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": false
|
||||
},
|
||||
"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",
|
||||
"allowedTags": [
|
||||
"ul",
|
||||
"li",
|
||||
"p",
|
||||
"code",
|
||||
"blockquote",
|
||||
"ol",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"hr",
|
||||
"em",
|
||||
"pre",
|
||||
"table",
|
||||
"thead",
|
||||
"tbody",
|
||||
"tr",
|
||||
"th",
|
||||
"td",
|
||||
"div",
|
||||
"del",
|
||||
"a",
|
||||
"strong",
|
||||
"br",
|
||||
"img",
|
||||
"span"
|
||||
]
|
||||
},
|
||||
"positionEncodings": [
|
||||
"utf-16"
|
||||
]
|
||||
},
|
||||
"notebookDocument": {
|
||||
"synchronization": {
|
||||
"dynamicRegistration": true,
|
||||
"executionSummarySupport": true
|
||||
}
|
||||
},
|
||||
"experimental": {
|
||||
"snippetTextEdit": true,
|
||||
"codeActionGroup": true,
|
||||
"hoverActions": true,
|
||||
"serverStatusNotification": true,
|
||||
"colorDiagnosticOutput": true,
|
||||
"openServerLogs": true,
|
||||
"commands": {
|
||||
"commands": [
|
||||
"rust-analyzer.runSingle",
|
||||
"rust-analyzer.debugSingle",
|
||||
"rust-analyzer.showReferences",
|
||||
"rust-analyzer.gotoLocation",
|
||||
"editor.action.triggerParameterHints"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"initializationOptions": {
|
||||
"cargoRunner": null,
|
||||
"runnables": {
|
||||
"extraEnv": null,
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
],
|
||||
"command": null,
|
||||
"extraArgs": []
|
||||
},
|
||||
"server": {
|
||||
"path": null,
|
||||
"extraEnv": null
|
||||
},
|
||||
"trace": {
|
||||
"server": "verbose",
|
||||
"extension": true
|
||||
},
|
||||
"debug": {
|
||||
"engine": "auto",
|
||||
"sourceFileMap": {
|
||||
"/rustc/<id>": "${env:USERPROFILE}/.rustup/toolchains/<toolchain-id>/lib/rustlib/src/rust"
|
||||
},
|
||||
"openDebugPane": false,
|
||||
"engineSettings": {}
|
||||
},
|
||||
"restartServerOnConfigChange": false,
|
||||
"typing": {
|
||||
"continueCommentsOnNewline": true,
|
||||
"autoClosingAngleBrackets": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"diagnostics": {
|
||||
"previewRustcOutput": false,
|
||||
"useRustcErrorCode": false,
|
||||
"disabled": [],
|
||||
"enable": true,
|
||||
"experimental": {
|
||||
"enable": false
|
||||
},
|
||||
"remapPrefix": {},
|
||||
"warningsAsHint": [],
|
||||
"warningsAsInfo": []
|
||||
},
|
||||
"discoverProjectCommand": null,
|
||||
"showUnlinkedFileNotification": true,
|
||||
"showDependenciesExplorer": true,
|
||||
"assist": {
|
||||
"emitMustUse": false,
|
||||
"expressionFillDefault": "todo"
|
||||
},
|
||||
"cachePriming": {
|
||||
"enable": true,
|
||||
"numThreads": 0
|
||||
},
|
||||
"cargo": {
|
||||
"autoreload": true,
|
||||
"buildScripts": {
|
||||
"enable": true,
|
||||
"invocationLocation": "workspace",
|
||||
"invocationStrategy": "per_workspace",
|
||||
"overrideCommand": null,
|
||||
"useRustcWrapper": true
|
||||
},
|
||||
"cfgs": {},
|
||||
"extraArgs": [],
|
||||
"extraEnv": {},
|
||||
"features": [],
|
||||
"noDefaultFeatures": false,
|
||||
"sysroot": "discover",
|
||||
"sysrootSrc": null,
|
||||
"target": null,
|
||||
"unsetTest": [
|
||||
"core"
|
||||
]
|
||||
},
|
||||
"checkOnSave": true,
|
||||
"check": {
|
||||
"allTargets": true,
|
||||
"command": "check",
|
||||
"extraArgs": [],
|
||||
"extraEnv": {},
|
||||
"features": null,
|
||||
"invocationLocation": "workspace",
|
||||
"invocationStrategy": "per_workspace",
|
||||
"noDefaultFeatures": null,
|
||||
"overrideCommand": null,
|
||||
"targets": null
|
||||
},
|
||||
"completion": {
|
||||
"autoimport": {
|
||||
"enable": true
|
||||
},
|
||||
"autoself": {
|
||||
"enable": true
|
||||
},
|
||||
"callable": {
|
||||
"snippets": "fill_arguments"
|
||||
},
|
||||
"limit": null,
|
||||
"postfix": {
|
||||
"enable": true
|
||||
},
|
||||
"privateEditable": {
|
||||
"enable": false
|
||||
},
|
||||
"snippets": {
|
||||
"custom": {
|
||||
"Arc::new": {
|
||||
"postfix": "arc",
|
||||
"body": "Arc::new(${receiver})",
|
||||
"requires": "std::sync::Arc",
|
||||
"description": "Put the expression into an `Arc`",
|
||||
"scope": "expr"
|
||||
},
|
||||
"Rc::new": {
|
||||
"postfix": "rc",
|
||||
"body": "Rc::new(${receiver})",
|
||||
"requires": "std::rc::Rc",
|
||||
"description": "Put the expression into an `Rc`",
|
||||
"scope": "expr"
|
||||
},
|
||||
"Box::pin": {
|
||||
"postfix": "pinbox",
|
||||
"body": "Box::pin(${receiver})",
|
||||
"requires": "std::boxed::Box",
|
||||
"description": "Put the expression into a pinned `Box`",
|
||||
"scope": "expr"
|
||||
},
|
||||
"Ok": {
|
||||
"postfix": "ok",
|
||||
"body": "Ok(${receiver})",
|
||||
"description": "Wrap the expression in a `Result::Ok`",
|
||||
"scope": "expr"
|
||||
},
|
||||
"Err": {
|
||||
"postfix": "err",
|
||||
"body": "Err(${receiver})",
|
||||
"description": "Wrap the expression in a `Result::Err`",
|
||||
"scope": "expr"
|
||||
},
|
||||
"Some": {
|
||||
"postfix": "some",
|
||||
"body": "Some(${receiver})",
|
||||
"description": "Wrap the expression in an `Option::Some`",
|
||||
"scope": "expr"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"excludeDirs": [],
|
||||
"watcher": "client"
|
||||
},
|
||||
"highlightRelated": {
|
||||
"breakPoints": {
|
||||
"enable": true
|
||||
},
|
||||
"closureCaptures": {
|
||||
"enable": true
|
||||
},
|
||||
"exitPoints": {
|
||||
"enable": true
|
||||
},
|
||||
"references": {
|
||||
"enable": true
|
||||
},
|
||||
"yieldPoints": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"hover": {
|
||||
"actions": {
|
||||
"debug": {
|
||||
"enable": true
|
||||
},
|
||||
"enable": true,
|
||||
"gotoTypeDef": {
|
||||
"enable": true
|
||||
},
|
||||
"implementations": {
|
||||
"enable": true
|
||||
},
|
||||
"references": {
|
||||
"enable": false
|
||||
},
|
||||
"run": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"documentation": {
|
||||
"enable": true,
|
||||
"keywords": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"enable": true
|
||||
},
|
||||
"memoryLayout": {
|
||||
"alignment": "hexadecimal",
|
||||
"enable": true,
|
||||
"niches": false,
|
||||
"offset": "hexadecimal",
|
||||
"size": "both"
|
||||
}
|
||||
},
|
||||
"imports": {
|
||||
"granularity": {
|
||||
"enforce": false,
|
||||
"group": "crate"
|
||||
},
|
||||
"group": {
|
||||
"enable": true
|
||||
},
|
||||
"merge": {
|
||||
"glob": true
|
||||
},
|
||||
"prefer": {
|
||||
"no": {
|
||||
"std": false
|
||||
}
|
||||
},
|
||||
"prefix": "plain"
|
||||
},
|
||||
"inlayHints": {
|
||||
"bindingModeHints": {
|
||||
"enable": false
|
||||
},
|
||||
"chainingHints": {
|
||||
"enable": true
|
||||
},
|
||||
"closingBraceHints": {
|
||||
"enable": true,
|
||||
"minLines": 25
|
||||
},
|
||||
"closureCaptureHints": {
|
||||
"enable": false
|
||||
},
|
||||
"closureReturnTypeHints": {
|
||||
"enable": "never"
|
||||
},
|
||||
"closureStyle": "impl_fn",
|
||||
"discriminantHints": {
|
||||
"enable": "never"
|
||||
},
|
||||
"expressionAdjustmentHints": {
|
||||
"enable": "never",
|
||||
"hideOutsideUnsafe": false,
|
||||
"mode": "prefix"
|
||||
},
|
||||
"lifetimeElisionHints": {
|
||||
"enable": "never",
|
||||
"useParameterNames": false
|
||||
},
|
||||
"maxLength": 25,
|
||||
"parameterHints": {
|
||||
"enable": true
|
||||
},
|
||||
"reborrowHints": {
|
||||
"enable": "never"
|
||||
},
|
||||
"renderColons": true,
|
||||
"typeHints": {
|
||||
"enable": true,
|
||||
"hideClosureInitialization": false,
|
||||
"hideNamedConstructor": false
|
||||
}
|
||||
},
|
||||
"interpret": {
|
||||
"tests": false
|
||||
},
|
||||
"joinLines": {
|
||||
"joinAssignments": true,
|
||||
"joinElseIf": true,
|
||||
"removeTrailingComma": true,
|
||||
"unwrapTrivialBlock": true
|
||||
},
|
||||
"lens": {
|
||||
"debug": {
|
||||
"enable": true
|
||||
},
|
||||
"enable": true,
|
||||
"forceCustomCommands": true,
|
||||
"implementations": {
|
||||
"enable": true
|
||||
},
|
||||
"location": "above_name",
|
||||
"references": {
|
||||
"adt": {
|
||||
"enable": false
|
||||
},
|
||||
"enumVariant": {
|
||||
"enable": false
|
||||
},
|
||||
"method": {
|
||||
"enable": false
|
||||
},
|
||||
"trait": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"run": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"linkedProjects": [],
|
||||
"lru": {
|
||||
"capacity": null,
|
||||
"query": {
|
||||
"capacities": {}
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"cargoTomlNotFound": true
|
||||
},
|
||||
"numThreads": null,
|
||||
"procMacro": {
|
||||
"attributes": {
|
||||
"enable": true
|
||||
},
|
||||
"enable": true,
|
||||
"ignored": {},
|
||||
"server": null
|
||||
},
|
||||
"references": {
|
||||
"excludeImports": false
|
||||
},
|
||||
"rustc": {
|
||||
"source": null
|
||||
},
|
||||
"rustfmt": {
|
||||
"extraArgs": [],
|
||||
"overrideCommand": null,
|
||||
"rangeFormatting": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"semanticHighlighting": {
|
||||
"doc": {
|
||||
"comment": {
|
||||
"inject": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"nonStandardTokens": true,
|
||||
"operator": {
|
||||
"enable": true,
|
||||
"specialization": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"punctuation": {
|
||||
"enable": false,
|
||||
"separate": {
|
||||
"macro": {
|
||||
"bang": false
|
||||
}
|
||||
},
|
||||
"specialization": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"strings": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"signatureInfo": {
|
||||
"detail": "full",
|
||||
"documentation": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"symbols": {
|
||||
"maxSymbols": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"trace": "verbose",
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": "$uri",
|
||||
"name": "$name"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
"""
|
||||
Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
|
||||
class JediServer(SolidLanguageServer):
|
||||
"""
|
||||
Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python.
|
||||
"""
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
|
||||
"""
|
||||
Creates a JediServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd="jedi-language-server", cwd=repository_root_path),
|
||||
"python",
|
||||
)
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"]
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Jedi Language Server.
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") 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
|
||||
|
||||
def _start_server(self):
|
||||
"""
|
||||
Starts the JEDI Language Server
|
||||
"""
|
||||
|
||||
def execute_client_command_handler(params):
|
||||
return []
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
def check_experimental_status(params):
|
||||
if params["quiescent"] == True:
|
||||
self.completions_available.set()
|
||||
|
||||
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)
|
||||
|
||||
self.logger.log("Starting jedi-language-server server process", logging.INFO)
|
||||
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 = 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": True,
|
||||
}
|
||||
|
||||
self.server.notify.initialized({})
|
||||
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
|
||||
class JediServer(SolidLanguageServer):
|
||||
"""
|
||||
Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python.
|
||||
"""
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
|
||||
"""
|
||||
Creates a JediServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd="jedi-language-server", cwd=repository_root_path),
|
||||
"python",
|
||||
)
|
||||
|
||||
@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:
|
||||
"""
|
||||
Returns the initialize params for the Jedi Language Server.
|
||||
"""
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
initialize_params = {
|
||||
"processId": os.getpid(),
|
||||
"clientInfo": {"name": "Serena", "version": "0.1.0"},
|
||||
"locale": "en",
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
# Note: this is not necessarily the minimal set of capabilities...
|
||||
"capabilities": {
|
||||
"workspace": {
|
||||
"applyEdit": True,
|
||||
"workspaceEdit": {
|
||||
"documentChanges": True,
|
||||
"resourceOperations": ["create", "rename", "delete"],
|
||||
"failureHandling": "textOnlyTransactional",
|
||||
"normalizesLineEndings": True,
|
||||
"changeAnnotationSupport": {"groupsOnLabel": True},
|
||||
},
|
||||
"configuration": True,
|
||||
"didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True},
|
||||
"symbol": {
|
||||
"dynamicRegistration": True,
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
"tagSupport": {"valueSet": [1]},
|
||||
"resolveSupport": {"properties": ["location.range"]},
|
||||
},
|
||||
"workspaceFolders": 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},
|
||||
"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": list(range(1, 27))},
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
"tagSupport": {"valueSet": [1]},
|
||||
"labelSupport": True,
|
||||
},
|
||||
"documentLink": {"dynamicRegistration": True, "tooltipSupport": True},
|
||||
"typeDefinition": {"dynamicRegistration": True, "linkSupport": True},
|
||||
"implementation": {"dynamicRegistration": True, "linkSupport": True},
|
||||
"declaration": {"dynamicRegistration": True, "linkSupport": True},
|
||||
"selectionRange": {"dynamicRegistration": True},
|
||||
"callHierarchy": {"dynamicRegistration": 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},
|
||||
},
|
||||
"notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}},
|
||||
"experimental": {
|
||||
"serverStatusNotification": True,
|
||||
"openServerLogs": True,
|
||||
},
|
||||
},
|
||||
# See https://github.com/pappasam/jedi-language-server?tab=readme-ov-file
|
||||
# We use the default options except for maxSymbols, where 0 means no limit
|
||||
"initializationOptions": {
|
||||
"workspace": {
|
||||
"symbols": {"ignoreFolders": [".nox", ".tox", ".venv", "__pycache__", "venv"], "maxSymbols": 0},
|
||||
},
|
||||
},
|
||||
"trace": "verbose",
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": root_uri,
|
||||
"name": os.path.basename(repository_absolute_path),
|
||||
}
|
||||
],
|
||||
}
|
||||
return initialize_params
|
||||
|
||||
def _start_server(self):
|
||||
"""
|
||||
Starts the JEDI Language Server
|
||||
"""
|
||||
|
||||
def execute_client_command_handler(params):
|
||||
return []
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
def check_experimental_status(params):
|
||||
if params["quiescent"] == True:
|
||||
self.completions_available.set()
|
||||
|
||||
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)
|
||||
|
||||
self.logger.log("Starting jedi-language-server server process", logging.INFO)
|
||||
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 = 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": True,
|
||||
}
|
||||
|
||||
self.server.notify.initialized({})
|
||||
@@ -0,0 +1,463 @@
|
||||
"""
|
||||
Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import stat
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.ls_utils import FileUtils, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
|
||||
@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(SolidLanguageServer):
|
||||
"""
|
||||
Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin.
|
||||
"""
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, 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",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> KotlinRuntimeDependencyPaths:
|
||||
"""
|
||||
Setup runtime dependencies for Kotlin Language Server and return the paths.
|
||||
"""
|
||||
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"
|
||||
|
||||
# Runtime dependency information
|
||||
runtime_dependencies = {
|
||||
"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.42.0/java-win32-x64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"java_home_path": "extension/jre/21.0.7-win32-x86_64",
|
||||
"java_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe",
|
||||
},
|
||||
"linux-x64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"java_home_path": "extension/jre/21.0.7-linux-x86_64",
|
||||
"java_path": "extension/jre/21.0.7-linux-x86_64/bin/java",
|
||||
},
|
||||
"linux-arm64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"java_home_path": "extension/jre/21.0.7-linux-aarch64",
|
||||
"java_path": "extension/jre/21.0.7-linux-aarch64/bin/java",
|
||||
},
|
||||
"osx-x64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"java_home_path": "extension/jre/21.0.7-macosx-x86_64",
|
||||
"java_path": "extension/jre/21.0.7-macosx-x86_64/bin/java",
|
||||
},
|
||||
"osx-arm64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"java_home_path": "extension/jre/21.0.7-macosx-aarch64",
|
||||
"java_path": "extension/jre/21.0.7-macosx-aarch64/bin/java",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
kotlin_dependency = runtime_dependencies["runtimeDependency"]
|
||||
java_dependency = runtime_dependencies["java"][platform_id.value]
|
||||
|
||||
# Setup paths for dependencies
|
||||
static_dir = os.path.join(cls.ls_resources_dir(), "kotlin_language_server")
|
||||
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
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Kotlin Language Server.
|
||||
"""
|
||||
if not os.path.isabs(repository_absolute_path):
|
||||
repository_absolute_path = os.path.abspath(repository_absolute_path)
|
||||
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
initialize_params = {
|
||||
"clientInfo": {"name": "Multilspy Kotlin Client", "version": "1.0.0"},
|
||||
"locale": "en",
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_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": list(range(1, 27))},
|
||||
"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": list(range(1, 27))},
|
||||
"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": [root_uri],
|
||||
"storagePath": None,
|
||||
"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",
|
||||
"processId": os.getpid(),
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": root_uri,
|
||||
"name": os.path.basename(repository_absolute_path),
|
||||
}
|
||||
],
|
||||
}
|
||||
return initialize_params
|
||||
|
||||
def _start_server(self):
|
||||
"""
|
||||
Starts the Kotlin Language Server
|
||||
"""
|
||||
|
||||
def execute_client_command_handler(params):
|
||||
return []
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
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.logger.log("Starting Kotlin server process", logging.INFO)
|
||||
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 = 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()
|
||||
@@ -1,521 +0,0 @@
|
||||
{
|
||||
"_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 ]"
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
"""
|
||||
Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import stat
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.ls_utils import FileUtils, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
|
||||
@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(SolidLanguageServer):
|
||||
"""
|
||||
Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin.
|
||||
"""
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, 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: LanguageServerLogger, config: LanguageServerConfig) -> 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"), encoding="utf-8") 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")), encoding="utf-8") 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
|
||||
|
||||
def _start_server(self):
|
||||
"""
|
||||
Starts the Kotlin Language Server
|
||||
"""
|
||||
|
||||
def execute_client_command_handler(params):
|
||||
return []
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
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.logger.log("Starting Kotlin server process", logging.INFO)
|
||||
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 = 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()
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"_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.42.0/java-win32-x64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"java_home_path": "extension/jre/21.0.7-win32-x86_64",
|
||||
"java_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe"
|
||||
},
|
||||
"linux-x64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"java_home_path": "extension/jre/21.0.7-linux-x86_64",
|
||||
"java_path": "extension/jre/21.0.7-linux-x86_64/bin/java"
|
||||
},
|
||||
"linux-arm64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"java_home_path": "extension/jre/21.0.7-linux-aarch64",
|
||||
"java_path": "extension/jre/21.0.7-linux-aarch64/bin/java"
|
||||
},
|
||||
"osx-x64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"java_home_path": "extension/jre/21.0.7-macosx-x86_64",
|
||||
"java_path": "extension/jre/21.0.7-macosx-x86_64/bin/java"
|
||||
},
|
||||
"osx-arm64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix",
|
||||
"archiveType": "zip",
|
||||
"java_home_path": "extension/jre/21.0.7-macosx-aarch64",
|
||||
"java_path": "extension/jre/21.0.7-macosx-aarch64/bin/java"
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-11
@@ -63,7 +63,7 @@ class OmniSharp(SolidLanguageServer):
|
||||
"""
|
||||
Creates an OmniSharp instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
omnisharp_executable_path, dll_path = self.setupRuntimeDependencies(logger, config)
|
||||
omnisharp_executable_path, dll_path = self._setup_runtime_dependencies(logger, config)
|
||||
|
||||
slnfilename = find_least_depth_sln_file(repository_root_path)
|
||||
if slnfilename is None:
|
||||
@@ -112,11 +112,12 @@ class OmniSharp(SolidLanguageServer):
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj"]
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Omnisharp Language Server.
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f:
|
||||
with open(os.path.join(os.path.dirname(__file__), "omnisharp", "initialize_params.json"), encoding="utf-8") as f:
|
||||
d = json.load(f)
|
||||
|
||||
del d["_description"]
|
||||
@@ -136,30 +137,32 @@ class OmniSharp(SolidLanguageServer):
|
||||
|
||||
return d
|
||||
|
||||
def setupRuntimeDependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str]:
|
||||
@classmethod
|
||||
def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str]:
|
||||
"""
|
||||
Setup runtime dependencies for OmniSharp.
|
||||
"""
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
dotnet_version = PlatformUtils.get_dotnet_version()
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f:
|
||||
with open(os.path.join(os.path.dirname(__file__), "omnisharp", "runtime_dependencies.json"), encoding="utf-8") as f:
|
||||
d = json.load(f)
|
||||
del d["_description"]
|
||||
|
||||
assert platform_id in [
|
||||
PlatformId.LINUX_x64,
|
||||
PlatformId.WIN_x64,
|
||||
], "Only linux-x64 and win-x64 platform is supported for in multilspy at the moment"
|
||||
], f"Only linux-x64 and win-x64 platform is supported at the moment but got {platform_id=}"
|
||||
assert dotnet_version in [
|
||||
DotnetVersion.V6,
|
||||
DotnetVersion.V7,
|
||||
DotnetVersion.V8,
|
||||
], "Only dotnet version 6 and 7 are supported in multilspy at the moment"
|
||||
DotnetVersion.V9,
|
||||
], f"Only dotnet version 6-9 are supported at the moment but got {dotnet_version=}"
|
||||
|
||||
# TODO: Do away with this assumption
|
||||
# Currently, runtime binaries are not available for .Net 7 and .Net 8. Hence, we assume .Net 6 runtime binaries to be compatible with .Net 7, .Net 8
|
||||
if dotnet_version in [DotnetVersion.V7, DotnetVersion.V8]:
|
||||
if dotnet_version in [DotnetVersion.V7, DotnetVersion.V8, DotnetVersion.V9]:
|
||||
dotnet_version = DotnetVersion.V6
|
||||
|
||||
runtime_dependencies = d["runtimeDependencies"]
|
||||
@@ -178,7 +181,7 @@ class OmniSharp(SolidLanguageServer):
|
||||
assert "OmniSharp" in runtime_dependencies
|
||||
assert "RazorOmnisharp" in runtime_dependencies
|
||||
|
||||
omnisharp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "OmniSharp")
|
||||
omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(), "OmniSharp")
|
||||
if not os.path.exists(omnisharp_ls_dir):
|
||||
os.makedirs(omnisharp_ls_dir)
|
||||
FileUtils.download_and_extract_archive(logger, runtime_dependencies["OmniSharp"]["url"], omnisharp_ls_dir, "zip")
|
||||
@@ -186,7 +189,7 @@ class OmniSharp(SolidLanguageServer):
|
||||
assert os.path.exists(omnisharp_executable_path)
|
||||
os.chmod(omnisharp_executable_path, stat.S_IEXEC)
|
||||
|
||||
razor_omnisharp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "RazorOmnisharp")
|
||||
razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(), "RazorOmnisharp")
|
||||
if not os.path.exists(razor_omnisharp_ls_dir):
|
||||
os.makedirs(razor_omnisharp_ls_dir)
|
||||
FileUtils.download_and_extract_archive(logger, runtime_dependencies["RazorOmnisharp"]["url"], razor_omnisharp_ls_dir, "zip")
|
||||
@@ -363,7 +366,7 @@ class OmniSharp(SolidLanguageServer):
|
||||
)
|
||||
init_response = self.server.send.initialize(initialize_params)
|
||||
self.server.notify.initialized({})
|
||||
with open(os.path.join(os.path.dirname(__file__), "workspace_did_change_configuration.json"), encoding="utf-8") as f:
|
||||
with open(os.path.join(os.path.dirname(__file__), "omnisharp", "workspace_did_change_configuration.json"), encoding="utf-8") as f:
|
||||
self.server.notify.workspace_did_change_configuration({"settings": json.load(f)})
|
||||
assert "capabilities" in init_response
|
||||
if "definitionProvider" in init_response["capabilities"] and init_response["capabilities"]["definitionProvider"]:
|
||||
+4
-45
@@ -46,7 +46,8 @@ class PyrightServer(SolidLanguageServer):
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"]
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Pyright Language Server.
|
||||
"""
|
||||
@@ -68,34 +69,17 @@ class PyrightServer(SolidLanguageServer):
|
||||
},
|
||||
"capabilities": {
|
||||
"workspace": {
|
||||
"applyEdit": True,
|
||||
"workspaceEdit": {"documentChanges": True},
|
||||
"didChangeConfiguration": {"dynamicRegistration": True},
|
||||
"didChangeWatchedFiles": {"dynamicRegistration": 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]
|
||||
},
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
},
|
||||
"executeCommand": {"dynamicRegistration": True},
|
||||
},
|
||||
"textDocument": {
|
||||
"synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
|
||||
"completion": {
|
||||
"dynamicRegistration": True,
|
||||
"contextSupport": True,
|
||||
"completionItem": {
|
||||
"snippetSupport": True,
|
||||
"commitCharactersSupport": True,
|
||||
"documentationFormat": ["markdown", "plaintext"],
|
||||
"deprecatedSupport": True,
|
||||
"preselectSupport": True,
|
||||
},
|
||||
"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]
|
||||
},
|
||||
},
|
||||
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
|
||||
"signatureHelp": {
|
||||
"dynamicRegistration": True,
|
||||
@@ -106,36 +90,11 @@ class PyrightServer(SolidLanguageServer):
|
||||
},
|
||||
"definition": {"dynamicRegistration": 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]
|
||||
},
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
},
|
||||
"codeAction": {
|
||||
"dynamicRegistration": True,
|
||||
"codeActionLiteralSupport": {
|
||||
"codeActionKind": {
|
||||
"valueSet": [
|
||||
"",
|
||||
"quickfix",
|
||||
"refactor",
|
||||
"refactor.extract",
|
||||
"refactor.inline",
|
||||
"refactor.rewrite",
|
||||
"source",
|
||||
"source.organizeImports",
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
"codeLens": {"dynamicRegistration": True},
|
||||
"formatting": {"dynamicRegistration": True},
|
||||
"rangeFormatting": {"dynamicRegistration": True},
|
||||
"onTypeFormatting": {"dynamicRegistration": True},
|
||||
"rename": {"dynamicRegistration": True},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
},
|
||||
@@ -0,0 +1,632 @@
|
||||
"""
|
||||
Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import stat
|
||||
import threading
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
from .common import RuntimeDependency, RuntimeDependencyCollection
|
||||
|
||||
|
||||
class RustAnalyzer(SolidLanguageServer):
|
||||
"""
|
||||
Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust.
|
||||
"""
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
|
||||
"""
|
||||
Creates a RustAnalyzer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
rustanalyzer_executable_path = self._setup_runtime_dependencies(logger, config)
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=rustanalyzer_executable_path, cwd=repository_root_path),
|
||||
"rust",
|
||||
)
|
||||
self.server_ready = threading.Event()
|
||||
self.service_ready_event = threading.Event()
|
||||
self.initialize_searcher_command_available = threading.Event()
|
||||
self.resolve_main_method_available = threading.Event()
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["target"]
|
||||
|
||||
@classmethod
|
||||
def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
|
||||
"""
|
||||
Setup runtime dependencies for rust_analyzer and return the command to start the server.
|
||||
"""
|
||||
deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="RustAnalyzer",
|
||||
description="RustAnalyzer for macOS (arm64)",
|
||||
url="https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-aarch64-apple-darwin.gz",
|
||||
platform_id="osx-arm64",
|
||||
archive_type="gz",
|
||||
binary_name="rust_analyzer",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="RustAnalyzer",
|
||||
description="RustAnalyzer for Linux (x64)",
|
||||
url="https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-x86_64-unknown-linux-gnu.gz",
|
||||
platform_id="linux-x64",
|
||||
archive_type="gz",
|
||||
binary_name="rust_analyzer",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="RustAnalyzer",
|
||||
description="RustAnalyzer for Windows (x64)",
|
||||
url="https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-x86_64-pc-windows-msvc.zip",
|
||||
platform_id="win-x64",
|
||||
archive_type="zip",
|
||||
binary_name="rust-analyzer.exe",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# assert platform_id.value in [
|
||||
# "linux-x64",
|
||||
# "win-x64",
|
||||
# ], "Only linux-x64 and win-x64 platform is supported for in multilspy at the moment"
|
||||
|
||||
rustanalyzer_ls_dir = os.path.join(cls.ls_resources_dir(), "RustAnalyzer")
|
||||
rustanalyzer_executable_path = deps.binary_path(rustanalyzer_ls_dir)
|
||||
if not os.path.exists(rustanalyzer_ls_dir):
|
||||
os.makedirs(rustanalyzer_ls_dir)
|
||||
deps.install(logger, rustanalyzer_ls_dir)
|
||||
assert os.path.exists(rustanalyzer_executable_path)
|
||||
os.chmod(rustanalyzer_executable_path, stat.S_IEXEC)
|
||||
|
||||
return rustanalyzer_executable_path
|
||||
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Rust Analyzer Language Server.
|
||||
"""
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
initialize_params = {
|
||||
"clientInfo": {"name": "Visual Studio Code - Insiders", "version": "1.82.0-insider"},
|
||||
"locale": "en",
|
||||
"capabilities": {
|
||||
"workspace": {
|
||||
"applyEdit": True,
|
||||
"workspaceEdit": {
|
||||
"documentChanges": True,
|
||||
"resourceOperations": ["create", "rename", "delete"],
|
||||
"failureHandling": "textOnlyTransactional",
|
||||
"normalizesLineEndings": True,
|
||||
"changeAnnotationSupport": {"groupsOnLabel": True},
|
||||
},
|
||||
"configuration": True,
|
||||
"didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True},
|
||||
"symbol": {
|
||||
"dynamicRegistration": True,
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
"tagSupport": {"valueSet": [1]},
|
||||
"resolveSupport": {"properties": ["location.range"]},
|
||||
},
|
||||
"codeLens": {"refreshSupport": True},
|
||||
"executeCommand": {"dynamicRegistration": True},
|
||||
"didChangeConfiguration": {"dynamicRegistration": 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": True,
|
||||
"commitCharactersSupport": True,
|
||||
"documentationFormat": ["markdown", "plaintext"],
|
||||
"deprecatedSupport": True,
|
||||
"preselectSupport": True,
|
||||
"tagSupport": {"valueSet": [1]},
|
||||
"insertReplaceSupport": True,
|
||||
"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, 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": list(range(1, 27))},
|
||||
"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": False,
|
||||
},
|
||||
"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",
|
||||
"allowedTags": [
|
||||
"ul",
|
||||
"li",
|
||||
"p",
|
||||
"code",
|
||||
"blockquote",
|
||||
"ol",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"hr",
|
||||
"em",
|
||||
"pre",
|
||||
"table",
|
||||
"thead",
|
||||
"tbody",
|
||||
"tr",
|
||||
"th",
|
||||
"td",
|
||||
"div",
|
||||
"del",
|
||||
"a",
|
||||
"strong",
|
||||
"br",
|
||||
"img",
|
||||
"span",
|
||||
],
|
||||
},
|
||||
"positionEncodings": ["utf-16"],
|
||||
},
|
||||
"notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}},
|
||||
"experimental": {
|
||||
"snippetTextEdit": True,
|
||||
"codeActionGroup": True,
|
||||
"hoverActions": True,
|
||||
"serverStatusNotification": True,
|
||||
"colorDiagnosticOutput": True,
|
||||
"openServerLogs": True,
|
||||
"localDocs": True,
|
||||
"commands": {
|
||||
"commands": [
|
||||
"rust-analyzer.runSingle",
|
||||
"rust-analyzer.debugSingle",
|
||||
"rust-analyzer.showReferences",
|
||||
"rust-analyzer.gotoLocation",
|
||||
"editor.action.triggerParameterHints",
|
||||
]
|
||||
},
|
||||
},
|
||||
},
|
||||
"initializationOptions": {
|
||||
"cargoRunner": None,
|
||||
"runnables": {"extraEnv": None, "problemMatcher": ["$rustc"], "command": None, "extraArgs": []},
|
||||
"statusBar": {"clickAction": "openLogs"},
|
||||
"server": {"path": None, "extraEnv": None},
|
||||
"trace": {"server": "verbose", "extension": False},
|
||||
"debug": {
|
||||
"engine": "auto",
|
||||
"sourceFileMap": {"/rustc/<id>": "${env:USERPROFILE}/.rustup/toolchains/<toolchain-id>/lib/rustlib/src/rust"},
|
||||
"openDebugPane": False,
|
||||
"engineSettings": {},
|
||||
},
|
||||
"restartServerOnConfigChange": False,
|
||||
"typing": {"continueCommentsOnNewline": True, "autoClosingAngleBrackets": {"enable": False}},
|
||||
"diagnostics": {
|
||||
"previewRustcOutput": False,
|
||||
"useRustcErrorCode": False,
|
||||
"disabled": [],
|
||||
"enable": True,
|
||||
"experimental": {"enable": False},
|
||||
"remapPrefix": {},
|
||||
"warningsAsHint": [],
|
||||
"warningsAsInfo": [],
|
||||
},
|
||||
"discoverProjectRunner": None,
|
||||
"showUnlinkedFileNotification": True,
|
||||
"showDependenciesExplorer": True,
|
||||
"assist": {"emitMustUse": False, "expressionFillDefault": "todo"},
|
||||
"cachePriming": {"enable": True, "numThreads": 0},
|
||||
"cargo": {
|
||||
"autoreload": True,
|
||||
"buildScripts": {
|
||||
"enable": True,
|
||||
"invocationLocation": "workspace",
|
||||
"invocationStrategy": "per_workspace",
|
||||
"overrideCommand": None,
|
||||
"useRustcWrapper": True,
|
||||
},
|
||||
"cfgs": {},
|
||||
"extraArgs": [],
|
||||
"extraEnv": {},
|
||||
"features": [],
|
||||
"noDefaultFeatures": False,
|
||||
"sysroot": "discover",
|
||||
"sysrootSrc": None,
|
||||
"target": None,
|
||||
"unsetTest": ["core"],
|
||||
},
|
||||
"checkOnSave": True,
|
||||
"check": {
|
||||
"allTargets": True,
|
||||
"command": "check",
|
||||
"extraArgs": [],
|
||||
"extraEnv": {},
|
||||
"features": None,
|
||||
"ignore": [],
|
||||
"invocationLocation": "workspace",
|
||||
"invocationStrategy": "per_workspace",
|
||||
"noDefaultFeatures": None,
|
||||
"overrideCommand": None,
|
||||
"targets": None,
|
||||
},
|
||||
"completion": {
|
||||
"autoimport": {"enable": True},
|
||||
"autoself": {"enable": True},
|
||||
"callable": {"snippets": "fill_arguments"},
|
||||
"fullFunctionSignatures": {"enable": False},
|
||||
"limit": None,
|
||||
"postfix": {"enable": True},
|
||||
"privateEditable": {"enable": False},
|
||||
"snippets": {
|
||||
"custom": {
|
||||
"Arc::new": {
|
||||
"postfix": "arc",
|
||||
"body": "Arc::new(${receiver})",
|
||||
"requires": "std::sync::Arc",
|
||||
"description": "Put the expression into an `Arc`",
|
||||
"scope": "expr",
|
||||
},
|
||||
"Rc::new": {
|
||||
"postfix": "rc",
|
||||
"body": "Rc::new(${receiver})",
|
||||
"requires": "std::rc::Rc",
|
||||
"description": "Put the expression into an `Rc`",
|
||||
"scope": "expr",
|
||||
},
|
||||
"Box::pin": {
|
||||
"postfix": "pinbox",
|
||||
"body": "Box::pin(${receiver})",
|
||||
"requires": "std::boxed::Box",
|
||||
"description": "Put the expression into a pinned `Box`",
|
||||
"scope": "expr",
|
||||
},
|
||||
"Ok": {
|
||||
"postfix": "ok",
|
||||
"body": "Ok(${receiver})",
|
||||
"description": "Wrap the expression in a `Result::Ok`",
|
||||
"scope": "expr",
|
||||
},
|
||||
"Err": {
|
||||
"postfix": "err",
|
||||
"body": "Err(${receiver})",
|
||||
"description": "Wrap the expression in a `Result::Err`",
|
||||
"scope": "expr",
|
||||
},
|
||||
"Some": {
|
||||
"postfix": "some",
|
||||
"body": "Some(${receiver})",
|
||||
"description": "Wrap the expression in an `Option::Some`",
|
||||
"scope": "expr",
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"files": {"excludeDirs": [], "watcher": "client"},
|
||||
"highlightRelated": {
|
||||
"breakPoints": {"enable": True},
|
||||
"closureCaptures": {"enable": True},
|
||||
"exitPoints": {"enable": True},
|
||||
"references": {"enable": True},
|
||||
"yieldPoints": {"enable": True},
|
||||
},
|
||||
"hover": {
|
||||
"actions": {
|
||||
"debug": {"enable": True},
|
||||
"enable": True,
|
||||
"gotoTypeDef": {"enable": True},
|
||||
"implementations": {"enable": True},
|
||||
"references": {"enable": False},
|
||||
"run": {"enable": True},
|
||||
},
|
||||
"documentation": {"enable": True, "keywords": {"enable": True}},
|
||||
"links": {"enable": True},
|
||||
"memoryLayout": {"alignment": "hexadecimal", "enable": True, "niches": False, "offset": "hexadecimal", "size": "both"},
|
||||
},
|
||||
"imports": {
|
||||
"granularity": {"enforce": False, "group": "crate"},
|
||||
"group": {"enable": True},
|
||||
"merge": {"glob": True},
|
||||
"preferNoStd": False,
|
||||
"preferPrelude": False,
|
||||
"prefix": "plain",
|
||||
},
|
||||
"inlayHints": {
|
||||
"bindingModeHints": {"enable": False},
|
||||
"chainingHints": {"enable": True},
|
||||
"closingBraceHints": {"enable": True, "minLines": 25},
|
||||
"closureCaptureHints": {"enable": False},
|
||||
"closureReturnTypeHints": {"enable": "never"},
|
||||
"closureStyle": "impl_fn",
|
||||
"discriminantHints": {"enable": "never"},
|
||||
"expressionAdjustmentHints": {"enable": "never", "hideOutsideUnsafe": False, "mode": "prefix"},
|
||||
"lifetimeElisionHints": {"enable": "never", "useParameterNames": False},
|
||||
"maxLength": 25,
|
||||
"parameterHints": {"enable": True},
|
||||
"reborrowHints": {"enable": "never"},
|
||||
"renderColons": True,
|
||||
"typeHints": {"enable": True, "hideClosureInitialization": False, "hideNamedConstructor": False},
|
||||
},
|
||||
"interpret": {"tests": False},
|
||||
"joinLines": {"joinAssignments": True, "joinElseIf": True, "removeTrailingComma": True, "unwrapTrivialBlock": True},
|
||||
"lens": {
|
||||
"debug": {"enable": True},
|
||||
"enable": True,
|
||||
"forceCustomCommands": True,
|
||||
"implementations": {"enable": True},
|
||||
"location": "above_name",
|
||||
"references": {
|
||||
"adt": {"enable": False},
|
||||
"enumVariant": {"enable": False},
|
||||
"method": {"enable": False},
|
||||
"trait": {"enable": False},
|
||||
},
|
||||
"run": {"enable": True},
|
||||
},
|
||||
"linkedProjects": [],
|
||||
"lru": {"capacity": None, "query": {"capacities": {}}},
|
||||
"notifications": {"cargoTomlNotFound": True},
|
||||
"numThreads": None,
|
||||
"procMacro": {"attributes": {"enable": True}, "enable": True, "ignored": {}, "server": None},
|
||||
"references": {"excludeImports": False},
|
||||
"rust": {"analyzerTargetDir": None},
|
||||
"rustc": {"source": None},
|
||||
"rustfmt": {"extraArgs": [], "overrideCommand": None, "rangeFormatting": {"enable": False}},
|
||||
"semanticHighlighting": {
|
||||
"doc": {"comment": {"inject": {"enable": True}}},
|
||||
"nonStandardTokens": True,
|
||||
"operator": {"enable": True, "specialization": {"enable": False}},
|
||||
"punctuation": {"enable": False, "separate": {"macro": {"bang": False}}, "specialization": {"enable": False}},
|
||||
"strings": {"enable": True},
|
||||
},
|
||||
"signatureInfo": {"detail": "full", "documentation": {"enable": True}},
|
||||
"workspace": {"symbol": {"search": {"kind": "only_types", "limit": 128, "scope": "workspace"}}},
|
||||
},
|
||||
"trace": "verbose",
|
||||
"processId": os.getpid(),
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": root_uri,
|
||||
"name": os.path.basename(repository_absolute_path),
|
||||
}
|
||||
],
|
||||
}
|
||||
return initialize_params
|
||||
|
||||
def _start_server(self):
|
||||
"""
|
||||
Starts the Rust Analyzer Language Server
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
def execute_client_command_handler(params):
|
||||
return []
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
def check_experimental_status(params):
|
||||
if params["quiescent"] == True:
|
||||
self.server_ready.set()
|
||||
|
||||
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)
|
||||
|
||||
self.logger.log("Starting RustAnalyzer server process", logging.INFO)
|
||||
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 = self.server.send.initialize(initialize_params)
|
||||
assert init_response["capabilities"]["textDocumentSync"]["change"] == 2
|
||||
assert "completionProvider" in init_response["capabilities"]
|
||||
assert init_response["capabilities"]["completionProvider"] == {
|
||||
"resolveProvider": True,
|
||||
"triggerCharacters": [":", ".", "'", "("],
|
||||
"completionItem": {"labelDetailsSupport": True},
|
||||
}
|
||||
self.server.notify.initialized({})
|
||||
self.completions_available.set()
|
||||
|
||||
self.server_ready.wait()
|
||||
@@ -1,917 +0,0 @@
|
||||
{
|
||||
"_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": "Visual Studio Code - Insiders",
|
||||
"version": "1.82.0-insider"
|
||||
},
|
||||
"locale": "en",
|
||||
"rootPath": "$rootPath",
|
||||
"rootUri": "$rootUri",
|
||||
"capabilities": {
|
||||
"workspace": {
|
||||
"applyEdit": true,
|
||||
"workspaceEdit": {
|
||||
"documentChanges": true,
|
||||
"resourceOperations": [
|
||||
"create",
|
||||
"rename",
|
||||
"delete"
|
||||
],
|
||||
"failureHandling": "textOnlyTransactional",
|
||||
"normalizesLineEndings": true,
|
||||
"changeAnnotationSupport": {
|
||||
"groupsOnLabel": true
|
||||
}
|
||||
},
|
||||
"configuration": 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
|
||||
},
|
||||
"didChangeConfiguration": {
|
||||
"dynamicRegistration": 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": true,
|
||||
"commitCharactersSupport": true,
|
||||
"documentationFormat": [
|
||||
"markdown",
|
||||
"plaintext"
|
||||
],
|
||||
"deprecatedSupport": true,
|
||||
"preselectSupport": true,
|
||||
"tagSupport": {
|
||||
"valueSet": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"insertReplaceSupport": true,
|
||||
"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,
|
||||
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": false
|
||||
},
|
||||
"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",
|
||||
"allowedTags": [
|
||||
"ul",
|
||||
"li",
|
||||
"p",
|
||||
"code",
|
||||
"blockquote",
|
||||
"ol",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"hr",
|
||||
"em",
|
||||
"pre",
|
||||
"table",
|
||||
"thead",
|
||||
"tbody",
|
||||
"tr",
|
||||
"th",
|
||||
"td",
|
||||
"div",
|
||||
"del",
|
||||
"a",
|
||||
"strong",
|
||||
"br",
|
||||
"img",
|
||||
"span"
|
||||
]
|
||||
},
|
||||
"positionEncodings": [
|
||||
"utf-16"
|
||||
]
|
||||
},
|
||||
"notebookDocument": {
|
||||
"synchronization": {
|
||||
"dynamicRegistration": true,
|
||||
"executionSummarySupport": true
|
||||
}
|
||||
},
|
||||
"experimental": {
|
||||
"snippetTextEdit": true,
|
||||
"codeActionGroup": true,
|
||||
"hoverActions": true,
|
||||
"serverStatusNotification": true,
|
||||
"colorDiagnosticOutput": true,
|
||||
"openServerLogs": true,
|
||||
"localDocs": true,
|
||||
"commands": {
|
||||
"commands": [
|
||||
"rust-analyzer.runSingle",
|
||||
"rust-analyzer.debugSingle",
|
||||
"rust-analyzer.showReferences",
|
||||
"rust-analyzer.gotoLocation",
|
||||
"editor.action.triggerParameterHints"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"initializationOptions": {
|
||||
"cargoRunner": null,
|
||||
"runnables": {
|
||||
"extraEnv": null,
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
],
|
||||
"command": null,
|
||||
"extraArgs": []
|
||||
},
|
||||
"statusBar": {
|
||||
"clickAction": "openLogs"
|
||||
},
|
||||
"server": {
|
||||
"path": null,
|
||||
"extraEnv": null
|
||||
},
|
||||
"trace": {
|
||||
"server": "verbose",
|
||||
"extension": false
|
||||
},
|
||||
"debug": {
|
||||
"engine": "auto",
|
||||
"sourceFileMap": {
|
||||
"/rustc/<id>": "${env:USERPROFILE}/.rustup/toolchains/<toolchain-id>/lib/rustlib/src/rust"
|
||||
},
|
||||
"openDebugPane": false,
|
||||
"engineSettings": {}
|
||||
},
|
||||
"restartServerOnConfigChange": false,
|
||||
"typing": {
|
||||
"continueCommentsOnNewline": true,
|
||||
"autoClosingAngleBrackets": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"diagnostics": {
|
||||
"previewRustcOutput": false,
|
||||
"useRustcErrorCode": false,
|
||||
"disabled": [],
|
||||
"enable": true,
|
||||
"experimental": {
|
||||
"enable": false
|
||||
},
|
||||
"remapPrefix": {},
|
||||
"warningsAsHint": [],
|
||||
"warningsAsInfo": []
|
||||
},
|
||||
"discoverProjectRunner": null,
|
||||
"showUnlinkedFileNotification": true,
|
||||
"showDependenciesExplorer": true,
|
||||
"assist": {
|
||||
"emitMustUse": false,
|
||||
"expressionFillDefault": "todo"
|
||||
},
|
||||
"cachePriming": {
|
||||
"enable": true,
|
||||
"numThreads": 0
|
||||
},
|
||||
"cargo": {
|
||||
"autoreload": true,
|
||||
"buildScripts": {
|
||||
"enable": true,
|
||||
"invocationLocation": "workspace",
|
||||
"invocationStrategy": "per_workspace",
|
||||
"overrideCommand": null,
|
||||
"useRustcWrapper": true
|
||||
},
|
||||
"cfgs": {},
|
||||
"extraArgs": [],
|
||||
"extraEnv": {},
|
||||
"features": [],
|
||||
"noDefaultFeatures": false,
|
||||
"sysroot": "discover",
|
||||
"sysrootSrc": null,
|
||||
"target": null,
|
||||
"unsetTest": [
|
||||
"core"
|
||||
]
|
||||
},
|
||||
"checkOnSave": true,
|
||||
"check": {
|
||||
"allTargets": true,
|
||||
"command": "check",
|
||||
"extraArgs": [],
|
||||
"extraEnv": {},
|
||||
"features": null,
|
||||
"ignore": [],
|
||||
"invocationLocation": "workspace",
|
||||
"invocationStrategy": "per_workspace",
|
||||
"noDefaultFeatures": null,
|
||||
"overrideCommand": null,
|
||||
"targets": null
|
||||
},
|
||||
"completion": {
|
||||
"autoimport": {
|
||||
"enable": true
|
||||
},
|
||||
"autoself": {
|
||||
"enable": true
|
||||
},
|
||||
"callable": {
|
||||
"snippets": "fill_arguments"
|
||||
},
|
||||
"fullFunctionSignatures": {
|
||||
"enable": false
|
||||
},
|
||||
"limit": null,
|
||||
"postfix": {
|
||||
"enable": true
|
||||
},
|
||||
"privateEditable": {
|
||||
"enable": false
|
||||
},
|
||||
"snippets": {
|
||||
"custom": {
|
||||
"Arc::new": {
|
||||
"postfix": "arc",
|
||||
"body": "Arc::new(${receiver})",
|
||||
"requires": "std::sync::Arc",
|
||||
"description": "Put the expression into an `Arc`",
|
||||
"scope": "expr"
|
||||
},
|
||||
"Rc::new": {
|
||||
"postfix": "rc",
|
||||
"body": "Rc::new(${receiver})",
|
||||
"requires": "std::rc::Rc",
|
||||
"description": "Put the expression into an `Rc`",
|
||||
"scope": "expr"
|
||||
},
|
||||
"Box::pin": {
|
||||
"postfix": "pinbox",
|
||||
"body": "Box::pin(${receiver})",
|
||||
"requires": "std::boxed::Box",
|
||||
"description": "Put the expression into a pinned `Box`",
|
||||
"scope": "expr"
|
||||
},
|
||||
"Ok": {
|
||||
"postfix": "ok",
|
||||
"body": "Ok(${receiver})",
|
||||
"description": "Wrap the expression in a `Result::Ok`",
|
||||
"scope": "expr"
|
||||
},
|
||||
"Err": {
|
||||
"postfix": "err",
|
||||
"body": "Err(${receiver})",
|
||||
"description": "Wrap the expression in a `Result::Err`",
|
||||
"scope": "expr"
|
||||
},
|
||||
"Some": {
|
||||
"postfix": "some",
|
||||
"body": "Some(${receiver})",
|
||||
"description": "Wrap the expression in an `Option::Some`",
|
||||
"scope": "expr"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"excludeDirs": [],
|
||||
"watcher": "client"
|
||||
},
|
||||
"highlightRelated": {
|
||||
"breakPoints": {
|
||||
"enable": true
|
||||
},
|
||||
"closureCaptures": {
|
||||
"enable": true
|
||||
},
|
||||
"exitPoints": {
|
||||
"enable": true
|
||||
},
|
||||
"references": {
|
||||
"enable": true
|
||||
},
|
||||
"yieldPoints": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"hover": {
|
||||
"actions": {
|
||||
"debug": {
|
||||
"enable": true
|
||||
},
|
||||
"enable": true,
|
||||
"gotoTypeDef": {
|
||||
"enable": true
|
||||
},
|
||||
"implementations": {
|
||||
"enable": true
|
||||
},
|
||||
"references": {
|
||||
"enable": false
|
||||
},
|
||||
"run": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"documentation": {
|
||||
"enable": true,
|
||||
"keywords": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"enable": true
|
||||
},
|
||||
"memoryLayout": {
|
||||
"alignment": "hexadecimal",
|
||||
"enable": true,
|
||||
"niches": false,
|
||||
"offset": "hexadecimal",
|
||||
"size": "both"
|
||||
}
|
||||
},
|
||||
"imports": {
|
||||
"granularity": {
|
||||
"enforce": false,
|
||||
"group": "crate"
|
||||
},
|
||||
"group": {
|
||||
"enable": true
|
||||
},
|
||||
"merge": {
|
||||
"glob": true
|
||||
},
|
||||
"preferNoStd": false,
|
||||
"preferPrelude": false,
|
||||
"prefix": "plain"
|
||||
},
|
||||
"inlayHints": {
|
||||
"bindingModeHints": {
|
||||
"enable": false
|
||||
},
|
||||
"chainingHints": {
|
||||
"enable": true
|
||||
},
|
||||
"closingBraceHints": {
|
||||
"enable": true,
|
||||
"minLines": 25
|
||||
},
|
||||
"closureCaptureHints": {
|
||||
"enable": false
|
||||
},
|
||||
"closureReturnTypeHints": {
|
||||
"enable": "never"
|
||||
},
|
||||
"closureStyle": "impl_fn",
|
||||
"discriminantHints": {
|
||||
"enable": "never"
|
||||
},
|
||||
"expressionAdjustmentHints": {
|
||||
"enable": "never",
|
||||
"hideOutsideUnsafe": false,
|
||||
"mode": "prefix"
|
||||
},
|
||||
"lifetimeElisionHints": {
|
||||
"enable": "never",
|
||||
"useParameterNames": false
|
||||
},
|
||||
"maxLength": 25,
|
||||
"parameterHints": {
|
||||
"enable": true
|
||||
},
|
||||
"reborrowHints": {
|
||||
"enable": "never"
|
||||
},
|
||||
"renderColons": true,
|
||||
"typeHints": {
|
||||
"enable": true,
|
||||
"hideClosureInitialization": false,
|
||||
"hideNamedConstructor": false
|
||||
}
|
||||
},
|
||||
"interpret": {
|
||||
"tests": false
|
||||
},
|
||||
"joinLines": {
|
||||
"joinAssignments": true,
|
||||
"joinElseIf": true,
|
||||
"removeTrailingComma": true,
|
||||
"unwrapTrivialBlock": true
|
||||
},
|
||||
"lens": {
|
||||
"debug": {
|
||||
"enable": true
|
||||
},
|
||||
"enable": true,
|
||||
"forceCustomCommands": true,
|
||||
"implementations": {
|
||||
"enable": true
|
||||
},
|
||||
"location": "above_name",
|
||||
"references": {
|
||||
"adt": {
|
||||
"enable": false
|
||||
},
|
||||
"enumVariant": {
|
||||
"enable": false
|
||||
},
|
||||
"method": {
|
||||
"enable": false
|
||||
},
|
||||
"trait": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"run": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"linkedProjects": [],
|
||||
"lru": {
|
||||
"capacity": null,
|
||||
"query": {
|
||||
"capacities": {}
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"cargoTomlNotFound": true
|
||||
},
|
||||
"numThreads": null,
|
||||
"procMacro": {
|
||||
"attributes": {
|
||||
"enable": true
|
||||
},
|
||||
"enable": true,
|
||||
"ignored": {},
|
||||
"server": null
|
||||
},
|
||||
"references": {
|
||||
"excludeImports": false
|
||||
},
|
||||
"rust": {
|
||||
"analyzerTargetDir": null
|
||||
},
|
||||
"rustc": {
|
||||
"source": null
|
||||
},
|
||||
"rustfmt": {
|
||||
"extraArgs": [],
|
||||
"overrideCommand": null,
|
||||
"rangeFormatting": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"semanticHighlighting": {
|
||||
"doc": {
|
||||
"comment": {
|
||||
"inject": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"nonStandardTokens": true,
|
||||
"operator": {
|
||||
"enable": true,
|
||||
"specialization": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"punctuation": {
|
||||
"enable": false,
|
||||
"separate": {
|
||||
"macro": {
|
||||
"bang": false
|
||||
}
|
||||
},
|
||||
"specialization": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"strings": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"signatureInfo": {
|
||||
"detail": "full",
|
||||
"documentation": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"symbol": {
|
||||
"search": {
|
||||
"kind": "only_types",
|
||||
"limit": 128,
|
||||
"scope": "workspace"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"trace": "verbose",
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": "$uri",
|
||||
"name": "$name"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"_description": "Used to download the runtime dependencies for running RustAnalyzer. Obtained from https://github.com/rust-lang/rust-analyzer/releases",
|
||||
"runtimeDependencies": [
|
||||
{
|
||||
"id": "RustAnalyzer",
|
||||
"description": "RustAnalyzer for Linux (x64)",
|
||||
"url": "https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-aarch64-apple-darwin.gz",
|
||||
"platformId": "osx-arm64",
|
||||
"archiveType": "gz",
|
||||
"binaryName": "rust_analyzer"
|
||||
},
|
||||
{
|
||||
"id": "RustAnalyzer",
|
||||
"description": "RustAnalyzer for Linux (x64)",
|
||||
"url": "https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-x86_64-unknown-linux-gnu.gz",
|
||||
"platformId": "linux-x64",
|
||||
"archiveType": "gz",
|
||||
"binaryName": "rust_analyzer"
|
||||
},
|
||||
{
|
||||
"id": "RustAnalyzer",
|
||||
"description": "RustAnalyzer for Windows (x64)",
|
||||
"url": "https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-x86_64-pc-windows-msvc.zip",
|
||||
"platformId": "win-x64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "rust-analyzer.exe"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
"""
|
||||
Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import stat
|
||||
import threading
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.ls_utils import FileUtils, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
|
||||
class RustAnalyzer(SolidLanguageServer):
|
||||
"""
|
||||
Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust.
|
||||
"""
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
|
||||
"""
|
||||
Creates a RustAnalyzer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
rustanalyzer_executable_path = self.setup_runtime_dependencies(logger, config)
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=rustanalyzer_executable_path, cwd=repository_root_path),
|
||||
"rust",
|
||||
)
|
||||
self.server_ready = threading.Event()
|
||||
self.service_ready_event = threading.Event()
|
||||
self.initialize_searcher_command_available = threading.Event()
|
||||
self.resolve_main_method_available = threading.Event()
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["target"]
|
||||
|
||||
def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
|
||||
"""
|
||||
Setup runtime dependencies for rust_analyzer.
|
||||
"""
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f:
|
||||
d = json.load(f)
|
||||
del d["_description"]
|
||||
|
||||
# assert platform_id.value in [
|
||||
# "linux-x64",
|
||||
# "win-x64",
|
||||
# ], "Only linux-x64 and win-x64 platform 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]
|
||||
|
||||
rustanalyzer_ls_dir = os.path.join(os.path.dirname(__file__), "static", "RustAnalyzer")
|
||||
rustanalyzer_executable_path = os.path.join(rustanalyzer_ls_dir, dependency["binaryName"])
|
||||
if not os.path.exists(rustanalyzer_ls_dir):
|
||||
os.makedirs(rustanalyzer_ls_dir)
|
||||
if dependency["archiveType"] == "gz":
|
||||
FileUtils.download_and_extract_archive(logger, dependency["url"], rustanalyzer_executable_path, dependency["archiveType"])
|
||||
else:
|
||||
FileUtils.download_and_extract_archive(logger, dependency["url"], rustanalyzer_ls_dir, dependency["archiveType"])
|
||||
assert os.path.exists(rustanalyzer_executable_path)
|
||||
os.chmod(rustanalyzer_executable_path, stat.S_IEXEC)
|
||||
|
||||
return rustanalyzer_executable_path
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Rust Analyzer Language Server.
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") 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
|
||||
|
||||
def _start_server(self):
|
||||
"""
|
||||
Starts the Rust Analyzer Language Server
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
def execute_client_command_handler(params):
|
||||
return []
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
def check_experimental_status(params):
|
||||
if params["quiescent"] == True:
|
||||
self.server_ready.set()
|
||||
|
||||
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)
|
||||
|
||||
self.logger.log("Starting RustAnalyzer server process", logging.INFO)
|
||||
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 = self.server.send.initialize(initialize_params)
|
||||
assert init_response["capabilities"]["textDocumentSync"]["change"] == 2
|
||||
assert "completionProvider" in init_response["capabilities"]
|
||||
assert init_response["capabilities"]["completionProvider"] == {
|
||||
"resolveProvider": True,
|
||||
"triggerCharacters": [":", ".", "'", "("],
|
||||
"completionItem": {"labelDetailsSupport": True},
|
||||
}
|
||||
self.server.notify.initialized({})
|
||||
self.completions_available.set()
|
||||
|
||||
self.server_ready.wait()
|
||||
+30
-27
@@ -31,7 +31,7 @@ class Solargraph(SolidLanguageServer):
|
||||
Creates a Solargraph instance. This class is not meant to be instantiated directly.
|
||||
Use LanguageServer.create() instead.
|
||||
"""
|
||||
solargraph_executable_path = self.setup_runtime_dependencies(logger, config, repository_root_path)
|
||||
solargraph_executable_path = self._setup_runtime_dependencies(logger, config, repository_root_path)
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
@@ -48,15 +48,21 @@ class Solargraph(SolidLanguageServer):
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["vendor"]
|
||||
|
||||
def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig, repository_root_path: str) -> str:
|
||||
@staticmethod
|
||||
def _setup_runtime_dependencies(logger: LanguageServerLogger, config: LanguageServerConfig, repository_root_path: str) -> str:
|
||||
"""
|
||||
Setup runtime dependencies for Solargraph.
|
||||
Setup runtime dependencies for Solargraph and return the command to start the server.
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f:
|
||||
d = json.load(f)
|
||||
del d["_description"]
|
||||
runtime_dependencies = [
|
||||
{
|
||||
"url": "https://rubygems.org/downloads/solargraph-0.51.1.gem",
|
||||
"installCommand": "gem install solargraph -v 0.51.1",
|
||||
"binaryName": "solargraph",
|
||||
"archiveType": "gem",
|
||||
}
|
||||
]
|
||||
|
||||
dependency = d["runtimeDependencies"][0]
|
||||
dependency = runtime_dependencies[0]
|
||||
|
||||
# Check if Ruby is installed
|
||||
try:
|
||||
@@ -93,29 +99,26 @@ class Solargraph(SolidLanguageServer):
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise RuntimeError(f"Failed to check or install Solargraph. {e.stderr}") from e
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Solargraph Language Server.
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") 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
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
initialize_params = {
|
||||
"capabilities": {},
|
||||
"trace": "verbose",
|
||||
"processId": os.getpid(),
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": pathlib.Path(repository_absolute_path).as_uri(),
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": root_uri,
|
||||
"name": os.path.basename(repository_absolute_path),
|
||||
}
|
||||
],
|
||||
}
|
||||
return initialize_params
|
||||
|
||||
def _start_server(self):
|
||||
"""
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"_description": "This file contains the initialization parameters for the Solargraph Language Server.",
|
||||
"processId": "$processId",
|
||||
"rootPath": "$rootPath",
|
||||
"rootUri": "$rootUri",
|
||||
"capabilities": {
|
||||
},
|
||||
"trace": "verbose",
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": "$uri",
|
||||
"name": "$name"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"_description": "This file contains URLs and other metadata required for downloading and installing the Solargraph language server.",
|
||||
"runtimeDependencies": [
|
||||
{
|
||||
"url": "https://rubygems.org/downloads/solargraph-0.51.1.gem",
|
||||
"installCommand": "gem install solargraph -v 0.51.1",
|
||||
"binaryName": "solargraph",
|
||||
"archiveType": "gem"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import threading
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.ls_utils import PathUtils, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
from .common import RuntimeDependency, RuntimeDependencyCollection
|
||||
|
||||
|
||||
class TerraformLS(SolidLanguageServer):
|
||||
"""
|
||||
Provides Terraform specific instantiation of the LanguageServer class using terraform-ls.
|
||||
"""
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in [".terraform", "terraform.tfstate.d"]
|
||||
|
||||
@staticmethod
|
||||
def _ensure_tf_command_available(logger: LanguageServerLogger):
|
||||
logger.log("Starting terraform version detection...", logging.DEBUG)
|
||||
|
||||
# 1. Try to find terraform using shutil.which
|
||||
terraform_cmd = shutil.which("terraform")
|
||||
if terraform_cmd is not None:
|
||||
logger.log(f"Found terraform via shutil.which: {terraform_cmd}", logging.DEBUG)
|
||||
return
|
||||
|
||||
# TODO: is this needed?
|
||||
# 2. Fallback to TERRAFORM_CLI_PATH (set by hashicorp/setup-terraform action)
|
||||
if not terraform_cmd:
|
||||
terraform_cli_path = os.environ.get("TERRAFORM_CLI_PATH")
|
||||
if terraform_cli_path:
|
||||
logger.log(f"Trying TERRAFORM_CLI_PATH: {terraform_cli_path}", logging.DEBUG)
|
||||
# TODO: use binary name from runtime dependencies if we keep this code
|
||||
if os.name == "nt":
|
||||
terraform_binary = os.path.join(terraform_cli_path, "terraform.exe")
|
||||
else:
|
||||
terraform_binary = os.path.join(terraform_cli_path, "terraform")
|
||||
if os.path.exists(terraform_binary):
|
||||
terraform_cmd = terraform_binary
|
||||
logger.log(f"Found terraform via TERRAFORM_CLI_PATH: {terraform_cmd}", logging.DEBUG)
|
||||
return
|
||||
|
||||
raise RuntimeError(
|
||||
"Terraform executable not found, please ensure Terraform is installed."
|
||||
"See https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli for instructions."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _setup_runtime_dependencies(cls, logger: LanguageServerLogger) -> str:
|
||||
"""
|
||||
Setup runtime dependencies for terraform-ls.
|
||||
Downloads and installs terraform-ls if not already present.
|
||||
"""
|
||||
cls._ensure_tf_command_available(logger)
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="TerraformLS",
|
||||
description="terraform-ls for macOS (ARM64)",
|
||||
url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_arm64.zip",
|
||||
platform_id="osx-arm64",
|
||||
archive_type="zip",
|
||||
binary_name="terraform-ls",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="TerraformLS",
|
||||
description="terraform-ls for macOS (x64)",
|
||||
url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_amd64.zip",
|
||||
platform_id="osx-x64",
|
||||
archive_type="zip",
|
||||
binary_name="terraform-ls",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="TerraformLS",
|
||||
description="terraform-ls for Linux (x64)",
|
||||
url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_linux_amd64.zip",
|
||||
platform_id="linux-x64",
|
||||
archive_type="zip",
|
||||
binary_name="terraform-ls",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="TerraformLS",
|
||||
description="terraform-ls for Windows (x64)",
|
||||
url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_windows_amd64.zip",
|
||||
platform_id="win-x64",
|
||||
archive_type="zip",
|
||||
binary_name="terraform-ls.exe",
|
||||
),
|
||||
]
|
||||
)
|
||||
dependency = deps.single_for_current_platform()
|
||||
|
||||
terraform_ls_executable_path = deps.binary_path(cls.ls_resources_dir())
|
||||
if not os.path.exists(terraform_ls_executable_path):
|
||||
logger.log(f"Downloading terraform-ls from {dependency.url}", logging.INFO)
|
||||
deps.install(logger, cls.ls_resources_dir())
|
||||
|
||||
assert os.path.exists(terraform_ls_executable_path), f"terraform-ls executable not found at {terraform_ls_executable_path}"
|
||||
|
||||
# Make the executable file executable on Unix-like systems
|
||||
if platform_id.value != "win-x64":
|
||||
os.chmod(terraform_ls_executable_path, stat.S_IEXEC | stat.S_IREAD)
|
||||
|
||||
return terraform_ls_executable_path
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
|
||||
"""
|
||||
Creates a TerraformLS instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
terraform_ls_executable_path = self._setup_runtime_dependencies(logger)
|
||||
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=f"{terraform_ls_executable_path} serve", cwd=repository_root_path),
|
||||
"terraform",
|
||||
)
|
||||
self.server_ready = threading.Event()
|
||||
self.request_id = 0
|
||||
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Terraform Language Server.
|
||||
"""
|
||||
root_uri = PathUtils.path_to_uri(repository_absolute_path)
|
||||
return {
|
||||
"processId": os.getpid(),
|
||||
"locale": "en",
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {"didSave": True, "dynamicRegistration": True},
|
||||
"completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
|
||||
"definition": {"dynamicRegistration": True},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": True,
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
},
|
||||
},
|
||||
"workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
|
||||
},
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"name": os.path.basename(repository_absolute_path),
|
||||
"uri": root_uri,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
def _start_server(self):
|
||||
"""Start terraform-ls server process"""
|
||||
|
||||
def register_capability_handler(params):
|
||||
return
|
||||
|
||||
def window_log_message(msg):
|
||||
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
self.server.on_request("client/registerCapability", register_capability_handler)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
|
||||
self.logger.log("Starting terraform-ls server process", logging.INFO)
|
||||
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 = self.server.send.initialize(initialize_params)
|
||||
|
||||
# Verify server capabilities
|
||||
assert "textDocumentSync" in init_response["capabilities"]
|
||||
assert "completionProvider" in init_response["capabilities"]
|
||||
assert "definitionProvider" in init_response["capabilities"]
|
||||
|
||||
self.server.notify.initialized({})
|
||||
self.completions_available.set()
|
||||
|
||||
# terraform-ls server is typically ready immediately after initialization
|
||||
self.server_ready.set()
|
||||
self.server_ready.wait()
|
||||
File renamed without changes.
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"_description": "Used to download the runtime dependencies for running terraform-ls. Obtained from https://releases.hashicorp.com/terraform-ls/",
|
||||
"runtimeDependencies": [
|
||||
{
|
||||
"id": "TerraformLS",
|
||||
"description": "terraform-ls for macOS (ARM64)",
|
||||
"url": "https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_arm64.zip",
|
||||
"platformId": "osx-arm64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "terraform-ls"
|
||||
},
|
||||
{
|
||||
"id": "TerraformLS",
|
||||
"description": "terraform-ls for macOS (x64)",
|
||||
"url": "https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_amd64.zip",
|
||||
"platformId": "osx-x64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "terraform-ls"
|
||||
},
|
||||
{
|
||||
"id": "TerraformLS",
|
||||
"description": "terraform-ls for Linux (x64)",
|
||||
"url": "https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_linux_amd64.zip",
|
||||
"platformId": "linux-x64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "terraform-ls"
|
||||
},
|
||||
{
|
||||
"id": "TerraformLS",
|
||||
"description": "terraform-ls for Windows (x64)",
|
||||
"url": "https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_windows_amd64.zip",
|
||||
"platformId": "win-x64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "terraform-ls.exe"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.ls_utils import FileUtils, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
|
||||
class TerraformLS(SolidLanguageServer):
|
||||
"""
|
||||
Provides Terraform specific instantiation of the LanguageServer class using terraform-ls.
|
||||
"""
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in [".terraform", "terraform.tfstate.d"]
|
||||
|
||||
@staticmethod
|
||||
def _get_terraform_version(logger: LanguageServerLogger) -> str:
|
||||
logger.log("Starting terraform version detection...", logging.DEBUG)
|
||||
|
||||
# 1. Try to find terraform using shutil.which
|
||||
terraform_cmd = shutil.which("terraform")
|
||||
if terraform_cmd is not None:
|
||||
logger.log(f"Found terraform via shutil.which: {terraform_cmd}", logging.DEBUG)
|
||||
|
||||
# 2. Fallback to TERRAFORM_CLI_PATH (set by hashicorp/setup-terraform action)
|
||||
if not terraform_cmd:
|
||||
terraform_cli_path = os.environ.get("TERRAFORM_CLI_PATH")
|
||||
if terraform_cli_path:
|
||||
logger.log(f"Trying TERRAFORM_CLI_PATH: {terraform_cli_path}", logging.DEBUG)
|
||||
if os.name == "nt":
|
||||
terraform_binary = os.path.join(terraform_cli_path, "terraform.exe")
|
||||
else:
|
||||
terraform_binary = os.path.join(terraform_cli_path, "terraform")
|
||||
if os.path.exists(terraform_binary):
|
||||
terraform_cmd = terraform_binary
|
||||
logger.log(f"Found terraform via TERRAFORM_CLI_PATH: {terraform_cmd}", logging.DEBUG)
|
||||
|
||||
if not terraform_cmd:
|
||||
raise RuntimeError("Terraform executable not found. Please ensure Terraform is installed and accessible in your system's PATH.")
|
||||
|
||||
logger.log(f"Attempting to run: {terraform_cmd} version (with 15s timeout)", logging.DEBUG)
|
||||
result = subprocess.run(
|
||||
[terraform_cmd, "version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=15, # CRITICAL: 15 second timeout to prevent hangs
|
||||
)
|
||||
if result.returncode == 0:
|
||||
logger.log("terraform version command succeeded", logging.DEBUG)
|
||||
return result.stdout.strip()
|
||||
else:
|
||||
raise RuntimeError(f"terraform version command failed with return code {result.returncode}: {result.stderr}")
|
||||
|
||||
# Note: needs to remain static because it's called before init is complete
|
||||
@staticmethod
|
||||
def _setup_runtime_dependencies(logger: LanguageServerLogger) -> str:
|
||||
"""
|
||||
Setup runtime dependencies for terraform-ls.
|
||||
Downloads and installs terraform-ls if not already present.
|
||||
"""
|
||||
# First check if Terraform is available
|
||||
terraform_version = TerraformLS._get_terraform_version(logger)
|
||||
if not terraform_version:
|
||||
raise RuntimeError(
|
||||
"Terraform executable not found or failed to execute. "
|
||||
"Please ensure Terraform is installed and accessible in your system's PATH.\n"
|
||||
"If it's installed, check for permission issues or corrupted installation.\n"
|
||||
"Download from https://www.terraform.io/downloads"
|
||||
)
|
||||
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") 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
|
||||
), f"Expected exactly one runtime dependency for platform {platform_id.value}, found {len(runtime_dependencies)}"
|
||||
dependency = runtime_dependencies[0]
|
||||
|
||||
terraform_ls_dir = os.path.join(os.path.dirname(__file__), "static", "TerraformLS")
|
||||
terraform_ls_executable_path = os.path.join(terraform_ls_dir, dependency["binaryName"])
|
||||
|
||||
if not os.path.exists(terraform_ls_dir):
|
||||
os.makedirs(terraform_ls_dir)
|
||||
|
||||
if not os.path.exists(terraform_ls_executable_path):
|
||||
logger.log(f"Downloading terraform-ls from {dependency['url']}", logging.INFO)
|
||||
FileUtils.download_and_extract_archive(logger, dependency["url"], terraform_ls_dir, dependency["archiveType"])
|
||||
|
||||
assert os.path.exists(terraform_ls_executable_path), f"terraform-ls executable not found at {terraform_ls_executable_path}"
|
||||
|
||||
# Make the executable file executable on Unix-like systems
|
||||
if platform_id.value != "win-x64":
|
||||
os.chmod(terraform_ls_executable_path, stat.S_IEXEC | stat.S_IREAD)
|
||||
|
||||
return terraform_ls_executable_path
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
|
||||
"""
|
||||
Creates a TerraformLS instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
terraform_ls_executable_path = self._setup_runtime_dependencies(logger)
|
||||
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=f"{terraform_ls_executable_path} serve", cwd=repository_root_path),
|
||||
"terraform",
|
||||
)
|
||||
self.server_ready = threading.Event()
|
||||
self.request_id = 0
|
||||
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Terraform Language Server.
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") 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
|
||||
|
||||
def _start_server(self):
|
||||
"""Start terraform-ls server process"""
|
||||
|
||||
def register_capability_handler(params):
|
||||
return
|
||||
|
||||
def window_log_message(msg):
|
||||
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
self.server.on_request("client/registerCapability", register_capability_handler)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
|
||||
self.logger.log("Starting terraform-ls server process", logging.INFO)
|
||||
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 = self.server.send.initialize(initialize_params)
|
||||
|
||||
# Verify server capabilities
|
||||
assert "textDocumentSync" in init_response["capabilities"]
|
||||
assert "completionProvider" in init_response["capabilities"]
|
||||
assert "definitionProvider" in init_response["capabilities"]
|
||||
|
||||
self.server.notify.initialized({})
|
||||
self.completions_available.set()
|
||||
|
||||
# terraform-ls server is typically ready immediately after initialization
|
||||
self.server_ready.set()
|
||||
self.server_ready.wait()
|
||||
+62
-55
@@ -2,12 +2,10 @@
|
||||
Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
from time import sleep
|
||||
|
||||
@@ -20,6 +18,8 @@ from solidlsp.ls_utils import PlatformId, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
from .common import RuntimeDependency, RuntimeDependencyCollection
|
||||
|
||||
# Platform-specific imports
|
||||
if os.name != "nt": # Unix-like systems
|
||||
import pwd
|
||||
@@ -33,7 +33,7 @@ else:
|
||||
|
||||
# Conditionally import pwd module (Unix-only)
|
||||
if not PlatformUtils.get_platform_id().value.startswith("win"):
|
||||
import pwd
|
||||
pass
|
||||
|
||||
|
||||
class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
@@ -45,7 +45,7 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Creates a TypeScriptLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
ts_lsp_executable_path = self.setup_runtime_dependencies(logger, config)
|
||||
ts_lsp_executable_path = self._setup_runtime_dependencies(logger, config)
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
@@ -65,9 +65,10 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
"coverage",
|
||||
]
|
||||
|
||||
def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
|
||||
@classmethod
|
||||
def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
|
||||
"""
|
||||
Setup runtime dependencies for TypeScript Language Server.
|
||||
Setup runtime dependencies for TypeScript Language Server and return the command to start the server.
|
||||
"""
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
|
||||
@@ -82,12 +83,23 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
]
|
||||
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy javascript/typescript at the moment"
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json")) as f:
|
||||
d = json.load(f)
|
||||
del d["_description"]
|
||||
|
||||
runtime_dependencies = d.get("runtimeDependencies", [])
|
||||
tsserver_ls_dir = os.path.join(os.path.dirname(__file__), "static", "ts-lsp")
|
||||
deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="typescript",
|
||||
description="typescript package",
|
||||
command="npm install --prefix ./ typescript@5.5.4",
|
||||
platform_id="any",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="typescript-language-server",
|
||||
description="typescript-language-server package",
|
||||
command="npm install --prefix ./ typescript-language-server@4.3.3",
|
||||
platform_id="any",
|
||||
),
|
||||
]
|
||||
)
|
||||
tsserver_ls_dir = os.path.join(cls.ls_resources_dir(), "ts-lsp")
|
||||
tsserver_executable_path = os.path.join(tsserver_ls_dir, "typescript-language-server")
|
||||
|
||||
# Verify both node and npm are installed
|
||||
@@ -99,29 +111,7 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
# Install typescript and typescript-language-server if not already installed
|
||||
if not os.path.exists(tsserver_ls_dir):
|
||||
os.makedirs(tsserver_ls_dir, exist_ok=True)
|
||||
for dependency in runtime_dependencies:
|
||||
# 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.run(
|
||||
dependency["command"],
|
||||
shell=True,
|
||||
check=True,
|
||||
user=user,
|
||||
cwd=tsserver_ls_dir,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
deps.install(logger, tsserver_ls_dir)
|
||||
|
||||
tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server")
|
||||
|
||||
@@ -130,29 +120,46 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
), "typescript-language-server executable not found. Please install typescript-language-server and try again."
|
||||
return f"{tsserver_executable_path} --stdio"
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the TypeScript Language Server.
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json")) 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
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
initialize_params = {
|
||||
"locale": "en",
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {"didSave": True, "dynamicRegistration": True},
|
||||
"completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
|
||||
"definition": {"dynamicRegistration": True},
|
||||
"references": {"dynamicRegistration": True},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": True,
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
},
|
||||
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
|
||||
"signatureHelp": {"dynamicRegistration": True},
|
||||
"codeAction": {"dynamicRegistration": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
"didChangeConfiguration": {"dynamicRegistration": True},
|
||||
"symbol": {"dynamicRegistration": True},
|
||||
},
|
||||
},
|
||||
"processId": os.getpid(),
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": root_uri,
|
||||
"name": os.path.basename(repository_absolute_path),
|
||||
}
|
||||
],
|
||||
}
|
||||
return initialize_params
|
||||
|
||||
def _start_server(self):
|
||||
"""
|
||||
@@ -1,906 +0,0 @@
|
||||
{
|
||||
"_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": "Visual Studio Code - Insiders",
|
||||
"version": "1.82.0-insider"
|
||||
},
|
||||
"locale": "en",
|
||||
"rootPath": "$rootPath",
|
||||
"rootUri": "$rootUri",
|
||||
"capabilities": {
|
||||
"workspace": {
|
||||
"applyEdit": true,
|
||||
"workspaceEdit": {
|
||||
"documentChanges": true,
|
||||
"resourceOperations": [
|
||||
"create",
|
||||
"rename",
|
||||
"delete"
|
||||
],
|
||||
"failureHandling": "textOnlyTransactional",
|
||||
"normalizesLineEndings": true,
|
||||
"changeAnnotationSupport": {
|
||||
"groupsOnLabel": true
|
||||
}
|
||||
},
|
||||
"configuration": 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
|
||||
},
|
||||
"didChangeConfiguration": {
|
||||
"dynamicRegistration": 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": true,
|
||||
"commitCharactersSupport": true,
|
||||
"documentationFormat": [
|
||||
"markdown",
|
||||
"plaintext"
|
||||
],
|
||||
"deprecatedSupport": true,
|
||||
"preselectSupport": true,
|
||||
"tagSupport": {
|
||||
"valueSet": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"insertReplaceSupport": true,
|
||||
"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,
|
||||
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": false
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"allowedTags": [
|
||||
"ul",
|
||||
"li",
|
||||
"p",
|
||||
"code",
|
||||
"blockquote",
|
||||
"ol",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"hr",
|
||||
"em",
|
||||
"pre",
|
||||
"table",
|
||||
"thead",
|
||||
"tbody",
|
||||
"tr",
|
||||
"th",
|
||||
"td",
|
||||
"div",
|
||||
"del",
|
||||
"a",
|
||||
"strong",
|
||||
"br",
|
||||
"img",
|
||||
"span"
|
||||
]
|
||||
},
|
||||
"positionEncodings": [
|
||||
"utf-16"
|
||||
]
|
||||
},
|
||||
"notebookDocument": {
|
||||
"synchronization": {
|
||||
"dynamicRegistration": true,
|
||||
"executionSummarySupport": true
|
||||
}
|
||||
},
|
||||
"experimental": {
|
||||
"snippetTextEdit": true,
|
||||
"codeActionGroup": true,
|
||||
"hoverActions": true,
|
||||
"serverStatusNotification": true,
|
||||
"colorDiagnosticOutput": true,
|
||||
"openServerLogs": true,
|
||||
"localDocs": true,
|
||||
"commands": {
|
||||
"commands": [
|
||||
"rust-analyzer.runSingle",
|
||||
"rust-analyzer.debugSingle",
|
||||
"rust-analyzer.showReferences",
|
||||
"rust-analyzer.gotoLocation",
|
||||
"editor.action.triggerParameterHints"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"initializationOptions": {
|
||||
"cargoRunner": null,
|
||||
"runnables": {
|
||||
"extraEnv": null,
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
],
|
||||
"command": null,
|
||||
"extraArgs": []
|
||||
},
|
||||
"statusBar": {
|
||||
"clickAction": "openLogs"
|
||||
},
|
||||
"server": {
|
||||
"path": null,
|
||||
"extraEnv": null
|
||||
},
|
||||
"trace": {
|
||||
"server": "verbose",
|
||||
"extension": false
|
||||
},
|
||||
"debug": {
|
||||
"engine": "auto",
|
||||
"sourceFileMap": {
|
||||
"/rustc/<id>": "${env:USERPROFILE}/.rustup/toolchains/<toolchain-id>/lib/rustlib/src/rust"
|
||||
},
|
||||
"openDebugPane": false,
|
||||
"engineSettings": {}
|
||||
},
|
||||
"restartServerOnConfigChange": false,
|
||||
"typing": {
|
||||
"continueCommentsOnNewline": true,
|
||||
"autoClosingAngleBrackets": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"diagnostics": {
|
||||
"previewRustcOutput": false,
|
||||
"useRustcErrorCode": false,
|
||||
"disabled": [],
|
||||
"enable": true,
|
||||
"experimental": {
|
||||
"enable": false
|
||||
},
|
||||
"remapPrefix": {},
|
||||
"warningsAsHint": [],
|
||||
"warningsAsInfo": []
|
||||
},
|
||||
"discoverProjectRunner": null,
|
||||
"showUnlinkedFileNotification": true,
|
||||
"showDependenciesExplorer": true,
|
||||
"assist": {
|
||||
"emitMustUse": false,
|
||||
"expressionFillDefault": "todo"
|
||||
},
|
||||
"cachePriming": {
|
||||
"enable": true,
|
||||
"numThreads": 0
|
||||
},
|
||||
"cargo": {
|
||||
"autoreload": true,
|
||||
"buildScripts": {
|
||||
"enable": true,
|
||||
"invocationLocation": "workspace",
|
||||
"invocationStrategy": "per_workspace",
|
||||
"overrideCommand": null,
|
||||
"useRustcWrapper": true
|
||||
},
|
||||
"cfgs": {},
|
||||
"extraArgs": [],
|
||||
"extraEnv": {},
|
||||
"features": [],
|
||||
"noDefaultFeatures": false,
|
||||
"sysroot": "discover",
|
||||
"sysrootSrc": null,
|
||||
"target": null,
|
||||
"unsetTest": [
|
||||
"core"
|
||||
]
|
||||
},
|
||||
"checkOnSave": true,
|
||||
"check": {
|
||||
"allTargets": true,
|
||||
"command": "check",
|
||||
"extraArgs": [],
|
||||
"extraEnv": {},
|
||||
"features": null,
|
||||
"ignore": [],
|
||||
"invocationLocation": "workspace",
|
||||
"invocationStrategy": "per_workspace",
|
||||
"noDefaultFeatures": null,
|
||||
"overrideCommand": null,
|
||||
"targets": null
|
||||
},
|
||||
"completion": {
|
||||
"autoimport": {
|
||||
"enable": true
|
||||
},
|
||||
"autoself": {
|
||||
"enable": true
|
||||
},
|
||||
"callable": {
|
||||
"snippets": "fill_arguments"
|
||||
},
|
||||
"fullFunctionSignatures": {
|
||||
"enable": false
|
||||
},
|
||||
"limit": null,
|
||||
"postfix": {
|
||||
"enable": true
|
||||
},
|
||||
"privateEditable": {
|
||||
"enable": false
|
||||
},
|
||||
"snippets": {
|
||||
"custom": {
|
||||
"Arc::new": {
|
||||
"postfix": "arc",
|
||||
"body": "Arc::new(${receiver})",
|
||||
"requires": "std::sync::Arc",
|
||||
"description": "Put the expression into an `Arc`",
|
||||
"scope": "expr"
|
||||
},
|
||||
"Rc::new": {
|
||||
"postfix": "rc",
|
||||
"body": "Rc::new(${receiver})",
|
||||
"requires": "std::rc::Rc",
|
||||
"description": "Put the expression into an `Rc`",
|
||||
"scope": "expr"
|
||||
},
|
||||
"Box::pin": {
|
||||
"postfix": "pinbox",
|
||||
"body": "Box::pin(${receiver})",
|
||||
"requires": "std::boxed::Box",
|
||||
"description": "Put the expression into a pinned `Box`",
|
||||
"scope": "expr"
|
||||
},
|
||||
"Ok": {
|
||||
"postfix": "ok",
|
||||
"body": "Ok(${receiver})",
|
||||
"description": "Wrap the expression in a `Result::Ok`",
|
||||
"scope": "expr"
|
||||
},
|
||||
"Err": {
|
||||
"postfix": "err",
|
||||
"body": "Err(${receiver})",
|
||||
"description": "Wrap the expression in a `Result::Err`",
|
||||
"scope": "expr"
|
||||
},
|
||||
"Some": {
|
||||
"postfix": "some",
|
||||
"body": "Some(${receiver})",
|
||||
"description": "Wrap the expression in an `Option::Some`",
|
||||
"scope": "expr"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"excludeDirs": [],
|
||||
"watcher": "client"
|
||||
},
|
||||
"highlightRelated": {
|
||||
"breakPoints": {
|
||||
"enable": true
|
||||
},
|
||||
"closureCaptures": {
|
||||
"enable": true
|
||||
},
|
||||
"exitPoints": {
|
||||
"enable": true
|
||||
},
|
||||
"references": {
|
||||
"enable": true
|
||||
},
|
||||
"yieldPoints": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"hover": {
|
||||
"actions": {
|
||||
"debug": {
|
||||
"enable": true
|
||||
},
|
||||
"enable": true,
|
||||
"gotoTypeDef": {
|
||||
"enable": true
|
||||
},
|
||||
"implementations": {
|
||||
"enable": true
|
||||
},
|
||||
"references": {
|
||||
"enable": false
|
||||
},
|
||||
"run": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"documentation": {
|
||||
"enable": true,
|
||||
"keywords": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"enable": true
|
||||
},
|
||||
"memoryLayout": {
|
||||
"alignment": "hexadecimal",
|
||||
"enable": true,
|
||||
"niches": false,
|
||||
"offset": "hexadecimal",
|
||||
"size": "both"
|
||||
}
|
||||
},
|
||||
"imports": {
|
||||
"granularity": {
|
||||
"enforce": false,
|
||||
"group": "crate"
|
||||
},
|
||||
"group": {
|
||||
"enable": true
|
||||
},
|
||||
"merge": {
|
||||
"glob": true
|
||||
},
|
||||
"preferNoStd": false,
|
||||
"preferPrelude": false,
|
||||
"prefix": "plain"
|
||||
},
|
||||
"inlayHints": {
|
||||
"bindingModeHints": {
|
||||
"enable": false
|
||||
},
|
||||
"chainingHints": {
|
||||
"enable": true
|
||||
},
|
||||
"closingBraceHints": {
|
||||
"enable": true,
|
||||
"minLines": 25
|
||||
},
|
||||
"closureCaptureHints": {
|
||||
"enable": false
|
||||
},
|
||||
"closureReturnTypeHints": {
|
||||
"enable": "never"
|
||||
},
|
||||
"closureStyle": "impl_fn",
|
||||
"discriminantHints": {
|
||||
"enable": "never"
|
||||
},
|
||||
"expressionAdjustmentHints": {
|
||||
"enable": "never",
|
||||
"hideOutsideUnsafe": false,
|
||||
"mode": "prefix"
|
||||
},
|
||||
"lifetimeElisionHints": {
|
||||
"enable": "never",
|
||||
"useParameterNames": false
|
||||
},
|
||||
"maxLength": 25,
|
||||
"parameterHints": {
|
||||
"enable": true
|
||||
},
|
||||
"reborrowHints": {
|
||||
"enable": "never"
|
||||
},
|
||||
"renderColons": true,
|
||||
"typeHints": {
|
||||
"enable": true,
|
||||
"hideClosureInitialization": false,
|
||||
"hideNamedConstructor": false
|
||||
}
|
||||
},
|
||||
"interpret": {
|
||||
"tests": false
|
||||
},
|
||||
"joinLines": {
|
||||
"joinAssignments": true,
|
||||
"joinElseIf": true,
|
||||
"removeTrailingComma": true,
|
||||
"unwrapTrivialBlock": true
|
||||
},
|
||||
"lens": {
|
||||
"debug": {
|
||||
"enable": true
|
||||
},
|
||||
"enable": true,
|
||||
"forceCustomCommands": true,
|
||||
"implementations": {
|
||||
"enable": true
|
||||
},
|
||||
"location": "above_name",
|
||||
"references": {
|
||||
"adt": {
|
||||
"enable": false
|
||||
},
|
||||
"enumVariant": {
|
||||
"enable": false
|
||||
},
|
||||
"method": {
|
||||
"enable": false
|
||||
},
|
||||
"trait": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"run": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"linkedProjects": [],
|
||||
"lru": {
|
||||
"capacity": null,
|
||||
"query": {
|
||||
"capacities": {}
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"cargoTomlNotFound": true
|
||||
},
|
||||
"numThreads": null,
|
||||
"procMacro": {
|
||||
"attributes": {
|
||||
"enable": true
|
||||
},
|
||||
"enable": true,
|
||||
"ignored": {},
|
||||
"server": null
|
||||
},
|
||||
"references": {
|
||||
"excludeImports": false
|
||||
},
|
||||
"rust": {
|
||||
"analyzerTargetDir": null
|
||||
},
|
||||
"rustc": {
|
||||
"source": null
|
||||
},
|
||||
"rustfmt": {
|
||||
"extraArgs": [],
|
||||
"overrideCommand": null,
|
||||
"rangeFormatting": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"semanticHighlighting": {
|
||||
"doc": {
|
||||
"comment": {
|
||||
"inject": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"nonStandardTokens": true,
|
||||
"operator": {
|
||||
"enable": true,
|
||||
"specialization": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"punctuation": {
|
||||
"enable": false,
|
||||
"separate": {
|
||||
"macro": {
|
||||
"bang": false
|
||||
}
|
||||
},
|
||||
"specialization": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"strings": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"signatureInfo": {
|
||||
"detail": "full",
|
||||
"documentation": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"symbol": {
|
||||
"search": {
|
||||
"kind": "only_types",
|
||||
"limit": 128,
|
||||
"scope": "workspace"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"trace": "verbose",
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": "$uri",
|
||||
"name": "$name"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"_description": "Used to download the runtime dependencies for running typescript-language-server. Obtained from https://github.com/typescript-language-server/typescript-language-server/releases",
|
||||
"runtimeDependencies": [
|
||||
{
|
||||
"id": "typescript",
|
||||
"description": "typescript package for Linux, OSX, and Windows. Both x64 and arm64 are supported.",
|
||||
"command": "npm install --prefix ./ typescript@5.5.4"
|
||||
},
|
||||
{
|
||||
"id": "typescript-language-server",
|
||||
"description": "typescript-language-server package for Linux, OSX, and Windows. Both x64 and arm64 are supported.",
|
||||
"command": "npm install --prefix ./ typescript-language-server@4.3.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+45
-12
@@ -89,6 +89,17 @@ class SolidLanguageServer(ABC):
|
||||
"""
|
||||
return dirname.startswith(".")
|
||||
|
||||
@classmethod
|
||||
def ls_resources_dir(cls, mkdir: bool = True) -> str:
|
||||
"""
|
||||
Returns the directory where the language server resources are downloaded.
|
||||
This is used to store language server binaries, configuration files, etc.
|
||||
"""
|
||||
result = os.path.join(os.path.dirname(__file__), "language_servers", "static", cls.__name__)
|
||||
if mkdir:
|
||||
os.makedirs(result, exist_ok=True)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, timeout: float | None = None
|
||||
@@ -108,28 +119,32 @@ class SolidLanguageServer(ABC):
|
||||
ls: SolidLanguageServer
|
||||
|
||||
if config.code_language == Language.PYTHON:
|
||||
from solidlsp.language_servers.pyright_language_server.pyright_server import (
|
||||
# We can also use jedi
|
||||
# from solidlsp.language_servers.jedi_server import JediServer
|
||||
#
|
||||
# ls = JediServer(config, logger, repository_root_path)
|
||||
from solidlsp.language_servers.pyright_server import (
|
||||
PyrightServer,
|
||||
)
|
||||
|
||||
ls = PyrightServer(config, logger, repository_root_path)
|
||||
|
||||
elif config.code_language == Language.JAVA:
|
||||
from solidlsp.language_servers.eclipse_jdtls.eclipse_jdtls import (
|
||||
from solidlsp.language_servers.eclipse_jdtls import (
|
||||
EclipseJDTLS,
|
||||
)
|
||||
|
||||
ls = EclipseJDTLS(config, logger, repository_root_path)
|
||||
|
||||
elif config.code_language == Language.KOTLIN:
|
||||
from solidlsp.language_servers.kotlin_language_server.kotlin_language_server import (
|
||||
from solidlsp.language_servers.kotlin_language_server import (
|
||||
KotlinLanguageServer,
|
||||
)
|
||||
|
||||
ls = KotlinLanguageServer(config, logger, repository_root_path)
|
||||
|
||||
elif config.code_language == Language.RUST:
|
||||
from solidlsp.language_servers.rust_analyzer.rust_analyzer import (
|
||||
from solidlsp.language_servers.rust_analyzer import (
|
||||
RustAnalyzer,
|
||||
)
|
||||
|
||||
@@ -141,47 +156,57 @@ class SolidLanguageServer(ABC):
|
||||
# from solidlsp.language_servers.omnisharp.omnisharp import OmniSharp
|
||||
# ls = OmniSharp(config, logger, repository_root_path)
|
||||
|
||||
from solidlsp.language_servers.csharp_language_server.csharp_language_server import CSharpLanguageServer
|
||||
from solidlsp.language_servers.csharp_language_server import CSharpLanguageServer
|
||||
|
||||
ls = CSharpLanguageServer(config, logger, repository_root_path)
|
||||
|
||||
elif config.code_language == Language.TYPESCRIPT:
|
||||
from solidlsp.language_servers.typescript_language_server.typescript_language_server import (
|
||||
from solidlsp.language_servers.typescript_language_server import (
|
||||
TypeScriptLanguageServer,
|
||||
)
|
||||
|
||||
ls = TypeScriptLanguageServer(config, logger, repository_root_path)
|
||||
|
||||
elif config.code_language == Language.GO:
|
||||
from solidlsp.language_servers.gopls.gopls import Gopls
|
||||
from solidlsp.language_servers.gopls import Gopls
|
||||
|
||||
ls = Gopls(config, logger, repository_root_path)
|
||||
|
||||
elif config.code_language == Language.RUBY:
|
||||
from solidlsp.language_servers.solargraph.solargraph import Solargraph
|
||||
from solidlsp.language_servers.solargraph import Solargraph
|
||||
|
||||
ls = Solargraph(config, logger, repository_root_path)
|
||||
|
||||
elif config.code_language == Language.DART:
|
||||
from solidlsp.language_servers.dart_language_server.dart_language_server import DartLanguageServer
|
||||
from solidlsp.language_servers.dart_language_server import DartLanguageServer
|
||||
|
||||
ls = DartLanguageServer(config, logger, repository_root_path)
|
||||
|
||||
elif config.code_language == Language.CPP:
|
||||
from solidlsp.language_servers.clangd_language_server.clangd_language_server import ClangdLanguageServer
|
||||
from solidlsp.language_servers.clangd_language_server import ClangdLanguageServer
|
||||
|
||||
ls = ClangdLanguageServer(config, logger, repository_root_path)
|
||||
|
||||
elif config.code_language == Language.PHP:
|
||||
from solidlsp.language_servers.intelephense.intelephense import Intelephense
|
||||
from solidlsp.language_servers.intelephense import Intelephense
|
||||
|
||||
ls = Intelephense(config, logger, repository_root_path)
|
||||
|
||||
elif config.code_language == Language.CLOJURE:
|
||||
from solidlsp.language_servers.clojure_lsp.clojure_lsp import ClojureLSP
|
||||
from solidlsp.language_servers.clojure_lsp import ClojureLSP
|
||||
|
||||
ls = ClojureLSP(config, logger, repository_root_path)
|
||||
|
||||
elif config.code_language == Language.ELIXIR:
|
||||
from solidlsp.language_servers.elixir_tools.elixir_tools import ElixirTools
|
||||
|
||||
ls = ElixirTools(config, logger, repository_root_path)
|
||||
|
||||
elif config.code_language == Language.TERRAFORM:
|
||||
from solidlsp.language_servers.terraform_ls import TerraformLS
|
||||
|
||||
ls = TerraformLS(config, logger, repository_root_path)
|
||||
|
||||
else:
|
||||
logger.log(f"Language {config.code_language} is not supported", logging.ERROR)
|
||||
raise LanguageServerException(f"Language {config.code_language} is not supported")
|
||||
@@ -664,6 +689,14 @@ class SolidLanguageServer(ABC):
|
||||
assert LSPConstants.RANGE in item
|
||||
|
||||
abs_path = PathUtils.uri_to_path(item[LSPConstants.URI])
|
||||
if not Path(abs_path).is_relative_to(self.repository_root_path):
|
||||
self.logger.log(
|
||||
"Found a reference in a path outside the repository, probably the LS is parsing things in installed packages or in the standardlib! "
|
||||
f"Path: {abs_path}. This is a bug but we currently simply skip these references.",
|
||||
logging.WARNING,
|
||||
)
|
||||
continue
|
||||
|
||||
rel_path = Path(abs_path).relative_to(self.repository_root_path)
|
||||
if self.is_ignored_path(str(rel_path)):
|
||||
self.logger.log(f"Ignoring reference in {rel_path} since it should be ignored", logging.DEBUG)
|
||||
|
||||
@@ -38,6 +38,8 @@ class Language(str, Enum):
|
||||
CPP = "cpp"
|
||||
PHP = "php"
|
||||
CLOJURE = "clojure"
|
||||
ELIXIR = "elixir"
|
||||
TERRAFORM = "terraform"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
@@ -74,6 +76,10 @@ class Language(str, Enum):
|
||||
return FilenameMatcher("*.php")
|
||||
case self.CLOJURE:
|
||||
return FilenameMatcher("*.clj", "*.cljs", "*.cljc", "*.edn") # codespell:ignore edn
|
||||
case self.ELIXIR:
|
||||
return FilenameMatcher("*.ex", "*.exs")
|
||||
case self.TERRAFORM:
|
||||
return FilenameMatcher("*.tf", "*.tfvars", "*.tfstate")
|
||||
case _:
|
||||
raise ValueError(f"Unhandled language: {self}")
|
||||
|
||||
|
||||
@@ -206,10 +206,10 @@ class FileUtils:
|
||||
os.makedirs(os.path.dirname(tmp_file_name), exist_ok=True)
|
||||
FileUtils.download_file(logger, url, tmp_file_name)
|
||||
if archive_type in ["zip", "tar", "gztar", "bztar", "xztar"]:
|
||||
assert os.path.isdir(target_path)
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
shutil.unpack_archive(tmp_file_name, target_path, archive_type)
|
||||
elif archive_type == "zip.gz":
|
||||
assert os.path.isdir(target_path)
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
tmp_file_name_ungzipped = tmp_file_name + ".zip"
|
||||
tmp_files.append(tmp_file_name_ungzipped)
|
||||
with gzip.open(tmp_file_name, "rb") as f_in, open(tmp_file_name_ungzipped, "wb") as f_out:
|
||||
@@ -257,6 +257,7 @@ class DotnetVersion(str, Enum):
|
||||
V6 = "6"
|
||||
V7 = "7"
|
||||
V8 = "8"
|
||||
V9 = "9"
|
||||
VMONO = "mono"
|
||||
|
||||
|
||||
@@ -345,15 +346,14 @@ class PlatformUtils:
|
||||
|
||||
# Check for supported versions in order of preference (latest first)
|
||||
for version_cmd_output in available_version_cmd_output:
|
||||
if version_cmd_output.startswith("9"):
|
||||
return DotnetVersion.V9
|
||||
if version_cmd_output.startswith("8"):
|
||||
return DotnetVersion.V8
|
||||
for version_cmd_output in available_version_cmd_output:
|
||||
if version_cmd_output.startswith("7"):
|
||||
return DotnetVersion.V7
|
||||
for version_cmd_output in available_version_cmd_output:
|
||||
if version_cmd_output.startswith("6"):
|
||||
return DotnetVersion.V6
|
||||
for version_cmd_output in available_version_cmd_output:
|
||||
if version_cmd_output.startswith("4"):
|
||||
return DotnetVersion.V4
|
||||
|
||||
|
||||
+38
-38
@@ -1,38 +1,38 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.2.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C88DD14-F956-CE84-757C-A364CCF449FC}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "resources", "resources", "{EF7103B4-4C75-1E6D-A485-A154A88D107A}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "repos", "repos", "{E2326EEF-E677-6A44-0935-7677816F09E7}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "csharp", "csharp", "{C21E6CE7-177A-86D9-040F-A317F18B6DBF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject", "test\resources\repos\csharp\test_repo\TestProject.csproj", "{A4D04E18-760A-73F9-3303-0542F6298C84}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A4D04E18-760A-73F9-3303-0542F6298C84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A4D04E18-760A-73F9-3303-0542F6298C84}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A4D04E18-760A-73F9-3303-0542F6298C84}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A4D04E18-760A-73F9-3303-0542F6298C84}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{EF7103B4-4C75-1E6D-A485-A154A88D107A} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
|
||||
{E2326EEF-E677-6A44-0935-7677816F09E7} = {EF7103B4-4C75-1E6D-A485-A154A88D107A}
|
||||
{C21E6CE7-177A-86D9-040F-A317F18B6DBF} = {E2326EEF-E677-6A44-0935-7677816F09E7}
|
||||
{A4D04E18-760A-73F9-3303-0542F6298C84} = {C21E6CE7-177A-86D9-040F-A317F18B6DBF}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {BDCA748E-D888-4BAF-BF24-DAC683113BFC}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.2.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C88DD14-F956-CE84-757C-A364CCF449FC}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "resources", "resources", "{EF7103B4-4C75-1E6D-A485-A154A88D107A}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "repos", "repos", "{E2326EEF-E677-6A44-0935-7677816F09E7}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "csharp", "csharp", "{C21E6CE7-177A-86D9-040F-A317F18B6DBF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject", "test\resources\repos\csharp\test_repo\TestProject.csproj", "{A4D04E18-760A-73F9-3303-0542F6298C84}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A4D04E18-760A-73F9-3303-0542F6298C84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A4D04E18-760A-73F9-3303-0542F6298C84}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A4D04E18-760A-73F9-3303-0542F6298C84}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A4D04E18-760A-73F9-3303-0542F6298C84}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{EF7103B4-4C75-1E6D-A485-A154A88D107A} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
|
||||
{E2326EEF-E677-6A44-0935-7677816F09E7} = {EF7103B4-4C75-1E6D-A485-A154A88D107A}
|
||||
{C21E6CE7-177A-86D9-040F-A317F18B6DBF} = {E2326EEF-E677-6A44-0935-7677816F09E7}
|
||||
{A4D04E18-760A-73F9-3303-0542F6298C84} = {C21E6CE7-177A-86D9-040F-A317F18B6DBF}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {BDCA748E-D888-4BAF-BF24-DAC683113BFC}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
Whitespace-only changes.
@@ -0,0 +1,211 @@
|
||||
defmodule TestRepo.Examples do
|
||||
@moduledoc """
|
||||
Examples module demonstrating usage of models and services.
|
||||
Similar to Python's examples directory, this shows how different modules work together.
|
||||
"""
|
||||
|
||||
alias TestRepo.Models.{User, Item}
|
||||
alias TestRepo.Services.{UserService, ItemService, OrderService}
|
||||
|
||||
defmodule UserManagement do
|
||||
@doc """
|
||||
Creates a complete user workflow example.
|
||||
"""
|
||||
def run_user_example do
|
||||
# Start user service
|
||||
{:ok, user_service} = UserService.start_link()
|
||||
|
||||
# Create users
|
||||
{:ok, alice} = UserService.create_user(user_service, "1", "Alice", "alice@example.com", ["admin"])
|
||||
{:ok, bob} = UserService.create_user(user_service, "2", "Bob", "bob@example.com", ["user"])
|
||||
|
||||
# Get users
|
||||
{:ok, retrieved_alice} = UserService.get_user(user_service, "1")
|
||||
|
||||
# List all users
|
||||
all_users = UserService.list_users(user_service)
|
||||
|
||||
# Clean up
|
||||
GenServer.stop(user_service)
|
||||
|
||||
%{
|
||||
created_alice: alice,
|
||||
created_bob: bob,
|
||||
retrieved_alice: retrieved_alice,
|
||||
all_users: all_users
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Demonstrates user role management.
|
||||
"""
|
||||
def manage_user_roles do
|
||||
user = User.new("role_user", "Role User", "role@example.com")
|
||||
|
||||
# Add roles
|
||||
user_with_admin = User.add_role(user, "admin")
|
||||
user_with_multiple = User.add_role(user_with_admin, "moderator")
|
||||
|
||||
# Check roles
|
||||
has_admin = User.has_role?(user_with_multiple, "admin")
|
||||
has_guest = User.has_role?(user_with_multiple, "guest")
|
||||
|
||||
%{
|
||||
original_user: user,
|
||||
user_with_roles: user_with_multiple,
|
||||
has_admin: has_admin,
|
||||
has_guest: has_guest
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defmodule ShoppingExample do
|
||||
@doc """
|
||||
Creates a complete shopping workflow.
|
||||
"""
|
||||
def run_shopping_example do
|
||||
# Create user and items
|
||||
user = User.new("customer1", "Customer One", "customer@example.com")
|
||||
item1 = Item.new("widget1", "Super Widget", 19.99, "electronics")
|
||||
item2 = Item.new("gadget1", "Cool Gadget", 29.99, "electronics")
|
||||
|
||||
# Create order
|
||||
order = OrderService.create_order("order1", user)
|
||||
|
||||
# Add items to order
|
||||
order_with_item1 = OrderService.add_item_to_order(order, item1)
|
||||
order_with_items = OrderService.add_item_to_order(order_with_item1, item2)
|
||||
|
||||
# Process the order
|
||||
processed_order = OrderService.process_order(order_with_items)
|
||||
completed_order = OrderService.complete_order(processed_order)
|
||||
|
||||
%{
|
||||
user: user,
|
||||
items: [item1, item2],
|
||||
final_order: completed_order,
|
||||
total_cost: completed_order.total
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Demonstrates item filtering and searching.
|
||||
"""
|
||||
def item_filtering_example do
|
||||
# Start item service
|
||||
{:ok, item_service} = ItemService.start_link()
|
||||
|
||||
# Create various items
|
||||
ItemService.create_item(item_service, "laptop", "Gaming Laptop", 1299.99, "electronics")
|
||||
ItemService.create_item(item_service, "book", "Elixir Guide", 39.99, "books")
|
||||
ItemService.create_item(item_service, "phone", "Smartphone", 699.99, "electronics")
|
||||
ItemService.create_item(item_service, "novel", "Great Novel", 19.99, "books")
|
||||
|
||||
# Get all items
|
||||
all_items = ItemService.list_items(item_service)
|
||||
|
||||
# Filter by category
|
||||
electronics = ItemService.list_items(item_service, "electronics")
|
||||
books = ItemService.list_items(item_service, "books")
|
||||
|
||||
# Clean up
|
||||
Agent.stop(item_service)
|
||||
|
||||
%{
|
||||
all_items: all_items,
|
||||
electronics: electronics,
|
||||
books: books,
|
||||
total_items: length(all_items),
|
||||
electronics_count: length(electronics),
|
||||
books_count: length(books)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defmodule IntegrationExample do
|
||||
@doc """
|
||||
Runs a complete e-commerce scenario.
|
||||
"""
|
||||
def run_full_scenario do
|
||||
# Setup services
|
||||
container = TestRepo.Services.create_service_container()
|
||||
TestRepo.Services.setup_sample_data(container)
|
||||
|
||||
# Get sample data
|
||||
{:ok, sample_user} = UserService.get_user(container.user_service, TestRepo.Services.sample_user_id())
|
||||
{:ok, sample_item} = ItemService.get_item(container.item_service, TestRepo.Services.sample_item_id())
|
||||
|
||||
# Create additional items
|
||||
{:ok, premium_item} = ItemService.create_item(
|
||||
container.item_service,
|
||||
"premium",
|
||||
"Premium Product",
|
||||
99.99,
|
||||
"premium"
|
||||
)
|
||||
|
||||
# Create order with multiple items
|
||||
order = OrderService.create_order("big_order", sample_user, [sample_item])
|
||||
order_with_premium = OrderService.add_item_to_order(order, premium_item)
|
||||
|
||||
# Process through order lifecycle
|
||||
processing_order = OrderService.process_order(order_with_premium)
|
||||
final_order = OrderService.complete_order(processing_order)
|
||||
|
||||
# Serialize everything for output
|
||||
serialized_user = TestRepo.Services.serialize_model(sample_user)
|
||||
serialized_order = TestRepo.Services.serialize_model(final_order)
|
||||
|
||||
# Clean up
|
||||
GenServer.stop(container.user_service)
|
||||
Agent.stop(container.item_service)
|
||||
|
||||
%{
|
||||
scenario: "full_ecommerce",
|
||||
user: serialized_user,
|
||||
order: serialized_order,
|
||||
total_revenue: final_order.total,
|
||||
items_sold: length(final_order.items)
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Demonstrates error handling scenarios.
|
||||
"""
|
||||
def error_handling_example do
|
||||
{:ok, user_service} = UserService.start_link()
|
||||
|
||||
# Try to create duplicate user
|
||||
{:ok, _user1} = UserService.create_user(user_service, "dup", "User", "user@example.com")
|
||||
duplicate_result = UserService.create_user(user_service, "dup", "Another User", "another@example.com")
|
||||
|
||||
# Try to get non-existent user
|
||||
missing_user_result = UserService.get_user(user_service, "nonexistent")
|
||||
|
||||
# Try to delete non-existent user
|
||||
delete_result = UserService.delete_user(user_service, "nonexistent")
|
||||
|
||||
GenServer.stop(user_service)
|
||||
|
||||
%{
|
||||
duplicate_user_error: duplicate_result,
|
||||
missing_user_error: missing_user_result,
|
||||
delete_missing_error: delete_result
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Main function to run all examples.
|
||||
"""
|
||||
def run_all_examples do
|
||||
%{
|
||||
user_management: UserManagement.run_user_example(),
|
||||
role_management: UserManagement.manage_user_roles(),
|
||||
shopping: ShoppingExample.run_shopping_example(),
|
||||
item_filtering: ShoppingExample.item_filtering_example(),
|
||||
integration: IntegrationExample.run_full_scenario(),
|
||||
error_handling: IntegrationExample.error_handling_example()
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
defmodule TestRepo.IgnoredDir.IgnoredModule do
|
||||
@moduledoc """
|
||||
This module is in a directory that should be ignored by the language server.
|
||||
It's used for testing directory filtering functionality.
|
||||
"""
|
||||
|
||||
alias TestRepo.Models.User
|
||||
|
||||
@doc """
|
||||
This function references the User model to test that ignored directories
|
||||
don't show up in symbol references.
|
||||
"""
|
||||
def create_ignored_user do
|
||||
User.new("ignored", "Ignored User", "ignored@example.com")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Another function that uses models.
|
||||
"""
|
||||
def process_ignored_user(user) do
|
||||
User.add_role(user, "ignored_role")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,166 @@
|
||||
defmodule TestRepo.Models do
|
||||
@moduledoc """
|
||||
Models module demonstrating various Elixir patterns including structs, protocols, and behaviours.
|
||||
"""
|
||||
|
||||
defprotocol Serializable do
|
||||
@doc "Convert model to map representation"
|
||||
def to_map(model)
|
||||
end
|
||||
|
||||
defmodule User do
|
||||
@type t :: %__MODULE__{
|
||||
id: String.t(),
|
||||
name: String.t() | nil,
|
||||
email: String.t(),
|
||||
roles: list(String.t())
|
||||
}
|
||||
|
||||
defstruct [:id, :name, :email, roles: []]
|
||||
|
||||
@doc """
|
||||
Creates a new user.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> TestRepo.Models.User.new("1", "Alice", "alice@example.com")
|
||||
%TestRepo.Models.User{id: "1", name: "Alice", email: "alice@example.com", roles: []}
|
||||
|
||||
"""
|
||||
def new(id, name, email, roles \\ []) do
|
||||
%__MODULE__{id: id, name: name, email: email, roles: roles}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if user has a specific role.
|
||||
"""
|
||||
def has_role?(%__MODULE__{roles: roles}, role) do
|
||||
role in roles
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a role to the user.
|
||||
"""
|
||||
def add_role(%__MODULE__{roles: roles} = user, role) do
|
||||
%{user | roles: [role | roles]}
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Item do
|
||||
@type t :: %__MODULE__{
|
||||
id: String.t(),
|
||||
name: String.t(),
|
||||
price: float(),
|
||||
category: String.t()
|
||||
}
|
||||
|
||||
defstruct [:id, :name, :price, :category]
|
||||
|
||||
@doc """
|
||||
Creates a new item.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> TestRepo.Models.Item.new("1", "Widget", 19.99, "electronics")
|
||||
%TestRepo.Models.Item{id: "1", name: "Widget", price: 19.99, category: "electronics"}
|
||||
|
||||
"""
|
||||
def new(id, name, price, category) do
|
||||
%__MODULE__{id: id, name: name, price: price, category: category}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats price for display.
|
||||
"""
|
||||
def display_price(%__MODULE__{price: price}) do
|
||||
"$#{:erlang.float_to_binary(price, decimals: 2)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if item is in a specific category.
|
||||
"""
|
||||
def in_category?(%__MODULE__{category: category}, target_category) do
|
||||
category == target_category
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Order do
|
||||
alias TestRepo.Models.{User, Item}
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: String.t(),
|
||||
user: User.t(),
|
||||
items: list(Item.t()),
|
||||
total: float(),
|
||||
status: atom()
|
||||
}
|
||||
|
||||
defstruct [:id, :user, items: [], total: 0.0, status: :pending]
|
||||
|
||||
@doc """
|
||||
Creates a new order.
|
||||
"""
|
||||
def new(id, user, items \\ []) do
|
||||
total = calculate_total(items)
|
||||
%__MODULE__{id: id, user: user, items: items, total: total}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds an item to the order.
|
||||
"""
|
||||
def add_item(%__MODULE__{items: items} = order, item) do
|
||||
new_items = [item | items]
|
||||
%{order | items: new_items, total: calculate_total(new_items)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates order status.
|
||||
"""
|
||||
def update_status(%__MODULE__{} = order, status) do
|
||||
%{order | status: status}
|
||||
end
|
||||
|
||||
defp calculate_total(items) do
|
||||
Enum.reduce(items, 0.0, fn item, acc -> acc + item.price end)
|
||||
end
|
||||
end
|
||||
|
||||
# Protocol implementations
|
||||
defimpl Serializable, for: User do
|
||||
def to_map(%User{id: id, name: name, email: email, roles: roles}) do
|
||||
%{id: id, name: name, email: email, roles: roles}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Serializable, for: Item do
|
||||
def to_map(%Item{id: id, name: name, price: price, category: category}) do
|
||||
%{id: id, name: name, price: price, category: category}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Serializable, for: Order do
|
||||
def to_map(%Order{id: id, user: user, items: items, total: total, status: status}) do
|
||||
%{
|
||||
id: id,
|
||||
user: Serializable.to_map(user),
|
||||
items: Enum.map(items, &Serializable.to_map/1),
|
||||
total: total,
|
||||
status: status
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Factory function to create a sample user.
|
||||
"""
|
||||
def create_sample_user do
|
||||
User.new("sample", "Sample User", "sample@example.com", ["user"])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Factory function to create a sample item.
|
||||
"""
|
||||
def create_sample_item do
|
||||
Item.new("sample", "Sample Item", 9.99, "sample")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,257 @@
|
||||
defmodule TestRepo.Services do
|
||||
@moduledoc """
|
||||
Services module demonstrating function usage and dependencies.
|
||||
Similar to Python's services.py, this module uses the models defined in TestRepo.Models.
|
||||
"""
|
||||
|
||||
alias TestRepo.Models.{User, Item, Order, Serializable}
|
||||
|
||||
defmodule UserService do
|
||||
use GenServer
|
||||
|
||||
# Client API
|
||||
|
||||
@doc """
|
||||
Starts the UserService GenServer.
|
||||
"""
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, %{}, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a new user and stores it.
|
||||
"""
|
||||
def create_user(pid, id, name, email, roles \\ []) do
|
||||
GenServer.call(pid, {:create_user, id, name, email, roles})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a user by ID.
|
||||
"""
|
||||
def get_user(pid, id) do
|
||||
GenServer.call(pid, {:get_user, id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lists all users.
|
||||
"""
|
||||
def list_users(pid) do
|
||||
GenServer.call(pid, :list_users)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a user by ID.
|
||||
"""
|
||||
def delete_user(pid, id) do
|
||||
GenServer.call(pid, {:delete_user, id})
|
||||
end
|
||||
|
||||
# Server callbacks
|
||||
|
||||
@impl true
|
||||
def init(_) do
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:create_user, id, name, email, roles}, _from, users) do
|
||||
if Map.has_key?(users, id) do
|
||||
{:reply, {:error, "User with ID #{id} already exists"}, users}
|
||||
else
|
||||
user = User.new(id, name, email, roles)
|
||||
new_users = Map.put(users, id, user)
|
||||
{:reply, {:ok, user}, new_users}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_user, id}, _from, users) do
|
||||
case Map.get(users, id) do
|
||||
nil -> {:reply, {:error, :not_found}, users}
|
||||
user -> {:reply, {:ok, user}, users}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:list_users, _from, users) do
|
||||
user_list = Map.values(users)
|
||||
{:reply, user_list, users}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:delete_user, id}, _from, users) do
|
||||
if Map.has_key?(users, id) do
|
||||
new_users = Map.delete(users, id)
|
||||
{:reply, :ok, new_users}
|
||||
else
|
||||
{:reply, {:error, :not_found}, users}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule ItemService do
|
||||
use Agent
|
||||
|
||||
@doc """
|
||||
Starts the ItemService Agent.
|
||||
"""
|
||||
def start_link(opts \\ []) do
|
||||
Agent.start_link(fn -> %{} end, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a new item and stores it.
|
||||
"""
|
||||
def create_item(pid, id, name, price, category) do
|
||||
Agent.get_and_update(pid, fn items ->
|
||||
if Map.has_key?(items, id) do
|
||||
{{:error, "Item with ID #{id} already exists"}, items}
|
||||
else
|
||||
item = Item.new(id, name, price, category)
|
||||
new_items = Map.put(items, id, item)
|
||||
{{:ok, item}, new_items}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets an item by ID.
|
||||
"""
|
||||
def get_item(pid, id) do
|
||||
Agent.get(pid, fn items ->
|
||||
case Map.get(items, id) do
|
||||
nil -> {:error, :not_found}
|
||||
item -> {:ok, item}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lists all items, optionally filtered by category.
|
||||
"""
|
||||
def list_items(pid, category \\ nil) do
|
||||
Agent.get(pid, fn items ->
|
||||
item_list = Map.values(items)
|
||||
|
||||
case category do
|
||||
nil -> item_list
|
||||
cat -> Enum.filter(item_list, &Item.in_category?(&1, cat))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes an item by ID.
|
||||
"""
|
||||
def delete_item(pid, id) do
|
||||
Agent.get_and_update(pid, fn items ->
|
||||
if Map.has_key?(items, id) do
|
||||
new_items = Map.delete(items, id)
|
||||
{:ok, new_items}
|
||||
else
|
||||
{{:error, :not_found}, items}
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule OrderService do
|
||||
@doc """
|
||||
Creates a new order.
|
||||
"""
|
||||
def create_order(id, user, items \\ []) do
|
||||
Order.new(id, user, items)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds an item to an existing order.
|
||||
"""
|
||||
def add_item_to_order(order, item) do
|
||||
Order.add_item(order, item)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the status of an order.
|
||||
"""
|
||||
def update_order_status(order, status) do
|
||||
Order.update_status(order, status)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Processes an order (changes status to :processing).
|
||||
"""
|
||||
def process_order(order) do
|
||||
update_order_status(order, :processing)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Completes an order (changes status to :completed).
|
||||
"""
|
||||
def complete_order(order) do
|
||||
update_order_status(order, :completed)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Cancels an order (changes status to :cancelled).
|
||||
"""
|
||||
def cancel_order(order) do
|
||||
update_order_status(order, :cancelled)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Factory function to create a service container.
|
||||
"""
|
||||
def create_service_container do
|
||||
{:ok, user_service} = UserService.start_link()
|
||||
{:ok, item_service} = ItemService.start_link()
|
||||
|
||||
%{
|
||||
user_service: user_service,
|
||||
item_service: item_service,
|
||||
order_service: OrderService
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Helper function to serialize any model that implements the Serializable protocol.
|
||||
"""
|
||||
def serialize_model(model) do
|
||||
Serializable.to_map(model)
|
||||
end
|
||||
|
||||
# Module-level variables for testing
|
||||
@sample_user_id "sample_user"
|
||||
@sample_item_id "sample_item"
|
||||
|
||||
@doc """
|
||||
Gets the sample user ID.
|
||||
"""
|
||||
def sample_user_id, do: @sample_user_id
|
||||
|
||||
@doc """
|
||||
Gets the sample item ID.
|
||||
"""
|
||||
def sample_item_id, do: @sample_item_id
|
||||
|
||||
# Create some sample data at module load time
|
||||
def setup_sample_data(container) do
|
||||
# Create sample user
|
||||
UserService.create_user(
|
||||
container.user_service,
|
||||
@sample_user_id,
|
||||
"Sample User",
|
||||
"sample@example.com",
|
||||
["user", "customer"]
|
||||
)
|
||||
|
||||
# Create sample item
|
||||
ItemService.create_item(
|
||||
container.item_service,
|
||||
@sample_item_id,
|
||||
"Sample Widget",
|
||||
29.99,
|
||||
"electronics"
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
defmodule TestRepo do
|
||||
@moduledoc """
|
||||
Documentation for `TestRepo`.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Hello world.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> TestRepo.hello()
|
||||
:world
|
||||
|
||||
"""
|
||||
def hello do
|
||||
:world
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds two numbers together.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> TestRepo.add(2, 3)
|
||||
5
|
||||
|
||||
"""
|
||||
def add(a, b) do
|
||||
a + b
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
defmodule TestRepo.Utils do
|
||||
@moduledoc """
|
||||
Utility functions for TestRepo.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Converts a string to uppercase.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> TestRepo.Utils.upcase("hello")
|
||||
"HELLO"
|
||||
|
||||
"""
|
||||
def upcase(string) when is_binary(string) do
|
||||
String.upcase(string)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the factorial of a number.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> TestRepo.Utils.factorial(5)
|
||||
120
|
||||
|
||||
"""
|
||||
def factorial(0), do: 1
|
||||
def factorial(n) when n > 0 do
|
||||
n * factorial(n - 1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a number is even.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> TestRepo.Utils.even?(4)
|
||||
true
|
||||
|
||||
iex> TestRepo.Utils.even?(3)
|
||||
false
|
||||
|
||||
"""
|
||||
def even?(n) when is_integer(n) do
|
||||
rem(n, 2) == 0
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
defmodule TestRepo.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :test_repo,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.14",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger]
|
||||
]
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
|
||||
]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
%{
|
||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||
"credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},
|
||||
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
defmodule TestRepo.Scripts.BuildScript do
|
||||
@moduledoc """
|
||||
Build script that references models.
|
||||
This is in the scripts directory which should be ignored in some tests.
|
||||
"""
|
||||
|
||||
alias TestRepo.Models.{User, Item}
|
||||
|
||||
@doc """
|
||||
Script function that creates test data.
|
||||
"""
|
||||
def create_test_data do
|
||||
user = User.new("script_user", "Script User", "script@example.com")
|
||||
item = Item.new("script_item", "Script Item", 1.0, "script")
|
||||
|
||||
{user, item}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Another script function referencing User.
|
||||
"""
|
||||
def cleanup_users do
|
||||
# This would reference User in a real scenario
|
||||
IO.puts("Cleaning up users...")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,169 @@
|
||||
defmodule TestRepo.ModelsTest do
|
||||
use ExUnit.Case
|
||||
doctest TestRepo.Models
|
||||
|
||||
alias TestRepo.Models.{User, Item, Order, Serializable}
|
||||
|
||||
describe "User" do
|
||||
test "creates a new user with default roles" do
|
||||
user = User.new("1", "Alice", "alice@example.com")
|
||||
|
||||
assert user.id == "1"
|
||||
assert user.name == "Alice"
|
||||
assert user.email == "alice@example.com"
|
||||
assert user.roles == []
|
||||
end
|
||||
|
||||
test "creates a user with specified roles" do
|
||||
user = User.new("2", "Bob", "bob@example.com", ["admin", "user"])
|
||||
|
||||
assert user.roles == ["admin", "user"]
|
||||
end
|
||||
|
||||
test "checks if user has role" do
|
||||
user = User.new("3", "Charlie", "charlie@example.com", ["admin"])
|
||||
|
||||
assert User.has_role?(user, "admin")
|
||||
refute User.has_role?(user, "guest")
|
||||
end
|
||||
|
||||
test "adds role to user" do
|
||||
user = User.new("4", "David", "david@example.com")
|
||||
user_with_role = User.add_role(user, "moderator")
|
||||
|
||||
assert User.has_role?(user_with_role, "moderator")
|
||||
assert length(user_with_role.roles) == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "Item" do
|
||||
test "creates a new item" do
|
||||
item = Item.new("widget1", "Super Widget", 19.99, "electronics")
|
||||
|
||||
assert item.id == "widget1"
|
||||
assert item.name == "Super Widget"
|
||||
assert item.price == 19.99
|
||||
assert item.category == "electronics"
|
||||
end
|
||||
|
||||
test "formats price for display" do
|
||||
item = Item.new("item1", "Test Item", 29.99, "test")
|
||||
|
||||
assert Item.display_price(item) == "$29.99"
|
||||
end
|
||||
|
||||
test "checks if item is in category" do
|
||||
item = Item.new("book1", "Elixir Book", 39.99, "books")
|
||||
|
||||
assert Item.in_category?(item, "books")
|
||||
refute Item.in_category?(item, "electronics")
|
||||
end
|
||||
end
|
||||
|
||||
describe "Order" do
|
||||
setup do
|
||||
user = User.new("customer1", "Customer", "customer@example.com")
|
||||
item1 = Item.new("item1", "Item 1", 10.00, "category1")
|
||||
item2 = Item.new("item2", "Item 2", 20.00, "category2")
|
||||
|
||||
%{user: user, item1: item1, item2: item2}
|
||||
end
|
||||
|
||||
test "creates a new order", %{user: user} do
|
||||
order = Order.new("order1", user)
|
||||
|
||||
assert order.id == "order1"
|
||||
assert order.user == user
|
||||
assert order.items == []
|
||||
assert order.total == 0.0
|
||||
assert order.status == :pending
|
||||
end
|
||||
|
||||
test "creates order with items", %{user: user, item1: item1, item2: item2} do
|
||||
order = Order.new("order2", user, [item1, item2])
|
||||
|
||||
assert length(order.items) == 2
|
||||
assert order.total == 30.0
|
||||
end
|
||||
|
||||
test "adds item to order", %{user: user, item1: item1, item2: item2} do
|
||||
order = Order.new("order3", user, [item1])
|
||||
order_with_item = Order.add_item(order, item2)
|
||||
|
||||
assert length(order_with_item.items) == 2
|
||||
assert order_with_item.total == 30.0
|
||||
end
|
||||
|
||||
test "updates order status", %{user: user} do
|
||||
order = Order.new("order4", user)
|
||||
processed_order = Order.update_status(order, :processing)
|
||||
|
||||
assert processed_order.status == :processing
|
||||
end
|
||||
end
|
||||
|
||||
describe "Serializable protocol" do
|
||||
test "serializes User" do
|
||||
user = User.new("1", "Alice", "alice@example.com", ["admin"])
|
||||
serialized = Serializable.to_map(user)
|
||||
|
||||
expected = %{
|
||||
id: "1",
|
||||
name: "Alice",
|
||||
email: "alice@example.com",
|
||||
roles: ["admin"]
|
||||
}
|
||||
|
||||
assert serialized == expected
|
||||
end
|
||||
|
||||
test "serializes Item" do
|
||||
item = Item.new("widget1", "Widget", 19.99, "electronics")
|
||||
serialized = Serializable.to_map(item)
|
||||
|
||||
expected = %{
|
||||
id: "widget1",
|
||||
name: "Widget",
|
||||
price: 19.99,
|
||||
category: "electronics"
|
||||
}
|
||||
|
||||
assert serialized == expected
|
||||
end
|
||||
|
||||
test "serializes Order" do
|
||||
user = User.new("1", "Alice", "alice@example.com")
|
||||
item = Item.new("widget1", "Widget", 19.99, "electronics")
|
||||
order = Order.new("order1", user, [item])
|
||||
|
||||
serialized = Serializable.to_map(order)
|
||||
|
||||
assert serialized.id == "order1"
|
||||
assert serialized.total == 19.99
|
||||
assert serialized.status == :pending
|
||||
assert is_map(serialized.user)
|
||||
assert is_list(serialized.items)
|
||||
assert length(serialized.items) == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "factory functions" do
|
||||
test "creates sample user" do
|
||||
user = TestRepo.Models.create_sample_user()
|
||||
|
||||
assert user.id == "sample"
|
||||
assert user.name == "Sample User"
|
||||
assert user.email == "sample@example.com"
|
||||
assert "user" in user.roles
|
||||
end
|
||||
|
||||
test "creates sample item" do
|
||||
item = TestRepo.Models.create_sample_item()
|
||||
|
||||
assert item.id == "sample"
|
||||
assert item.name == "Sample Item"
|
||||
assert item.price == 9.99
|
||||
assert item.category == "sample"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
defmodule TestRepoTest do
|
||||
use ExUnit.Case
|
||||
doctest TestRepo
|
||||
|
||||
test "greets the world" do
|
||||
assert TestRepo.hello() == :world
|
||||
end
|
||||
|
||||
test "adds numbers correctly" do
|
||||
assert TestRepo.add(2, 3) == 5
|
||||
assert TestRepo.add(-1, 1) == 0
|
||||
assert TestRepo.add(0, 0) == 0
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
# Data sources for the Terraform configuration
|
||||
|
||||
# Get the latest Ubuntu AMI
|
||||
data "aws_ami" "ubuntu" {
|
||||
most_recent = true
|
||||
owners = ["099720109477"] # Canonical
|
||||
|
||||
filter {
|
||||
name = "name"
|
||||
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
|
||||
}
|
||||
|
||||
filter {
|
||||
name = "virtualization-type"
|
||||
values = ["hvm"]
|
||||
}
|
||||
}
|
||||
|
||||
# Get available availability zones
|
||||
data "aws_availability_zones" "available" {
|
||||
state = "available"
|
||||
}
|
||||
|
||||
# Get current AWS caller identity
|
||||
data "aws_caller_identity" "current" {}
|
||||
|
||||
# Get current AWS region
|
||||
data "aws_region" "current" {}
|
||||
@@ -0,0 +1,126 @@
|
||||
# Main Terraform configuration
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = var.aws_region
|
||||
}
|
||||
|
||||
# EC2 Instance
|
||||
resource "aws_instance" "web_server" {
|
||||
ami = data.aws_ami.ubuntu.id
|
||||
instance_type = var.instance_type
|
||||
|
||||
vpc_security_group_ids = [aws_security_group.web_sg.id]
|
||||
subnet_id = aws_subnet.public.id
|
||||
|
||||
user_data = <<-EOF
|
||||
#!/bin/bash
|
||||
apt-get update
|
||||
apt-get install -y nginx
|
||||
systemctl start nginx
|
||||
systemctl enable nginx
|
||||
EOF
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-web-server"
|
||||
Environment = var.environment
|
||||
Project = var.project_name
|
||||
}
|
||||
}
|
||||
|
||||
# S3 Bucket
|
||||
resource "aws_s3_bucket" "app_bucket" {
|
||||
bucket = "${var.project_name}-${var.environment}-bucket"
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-bucket"
|
||||
Environment = var.environment
|
||||
Project = var.project_name
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_versioning" "app_bucket_versioning" {
|
||||
bucket = aws_s3_bucket.app_bucket.id
|
||||
versioning_configuration {
|
||||
status = "Enabled"
|
||||
}
|
||||
}
|
||||
|
||||
# VPC
|
||||
resource "aws_vpc" "main" {
|
||||
cidr_block = "10.0.0.0/16"
|
||||
enable_dns_hostnames = true
|
||||
enable_dns_support = true
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-vpc"
|
||||
Environment = var.environment
|
||||
Project = var.project_name
|
||||
}
|
||||
}
|
||||
|
||||
# Internet Gateway
|
||||
resource "aws_internet_gateway" "main" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-igw"
|
||||
Environment = var.environment
|
||||
Project = var.project_name
|
||||
}
|
||||
}
|
||||
|
||||
# Public Subnet
|
||||
resource "aws_subnet" "public" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
cidr_block = "10.0.1.0/24"
|
||||
availability_zone = data.aws_availability_zones.available.names[0]
|
||||
map_public_ip_on_launch = true
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-public-subnet"
|
||||
Environment = var.environment
|
||||
Project = var.project_name
|
||||
}
|
||||
}
|
||||
|
||||
# Security Group
|
||||
resource "aws_security_group" "web_sg" {
|
||||
name_prefix = "${var.project_name}-web-"
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
ingress {
|
||||
from_port = 80
|
||||
to_port = 80
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
ingress {
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
egress {
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-web-sg"
|
||||
Environment = var.environment
|
||||
Project = var.project_name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
# Output values for the Terraform configuration
|
||||
|
||||
output "instance_id" {
|
||||
description = "ID of the EC2 instance"
|
||||
value = aws_instance.web_server.id
|
||||
}
|
||||
|
||||
output "instance_public_ip" {
|
||||
description = "Public IP address of the EC2 instance"
|
||||
value = aws_instance.web_server.public_ip
|
||||
}
|
||||
|
||||
output "instance_public_dns" {
|
||||
description = "Public DNS name of the EC2 instance"
|
||||
value = aws_instance.web_server.public_dns
|
||||
}
|
||||
|
||||
output "s3_bucket_name" {
|
||||
description = "Name of the S3 bucket"
|
||||
value = aws_s3_bucket.app_bucket.bucket
|
||||
}
|
||||
|
||||
output "s3_bucket_arn" {
|
||||
description = "ARN of the S3 bucket"
|
||||
value = aws_s3_bucket.app_bucket.arn
|
||||
}
|
||||
|
||||
output "vpc_id" {
|
||||
description = "ID of the VPC"
|
||||
value = aws_vpc.main.id
|
||||
}
|
||||
|
||||
output "subnet_id" {
|
||||
description = "ID of the public subnet"
|
||||
value = aws_subnet.public.id
|
||||
}
|
||||
|
||||
output "security_group_id" {
|
||||
description = "ID of the security group"
|
||||
value = aws_security_group.web_sg.id
|
||||
}
|
||||
|
||||
output "application_url" {
|
||||
description = "URL to access the application"
|
||||
value = "http://${aws_instance.web_server.public_dns}"
|
||||
}
|
||||
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user