mirror of
https://github.com/tiennm99/serena.git
synced 2026-09-10 02:17:49 +00:00
Merge branch 'refs/heads/main' into feature/tools-extension
# Conflicts: # CHANGELOG.md # src/multilspy/language_server.py
This commit is contained in:
@@ -8,9 +8,13 @@ Changes prior to the next official version change will appear here.
|
||||
* one-click setup for Cline enabled
|
||||
* search for pattern tool can now (optionally) search in the entire project
|
||||
* new tool for restarting the language server, in case of other sources of editing apart from Serena
|
||||
* Fix `CheckOnboardingPerformedTool`:
|
||||
* Tool description was incompatible with project change
|
||||
* Returned result was not as useful as it could be (now added list of memories)
|
||||
|
||||
* Language Servers:
|
||||
* Add further file extensions considered by the language servers for Python (.pyi), JavaScript (.jsx) and TypeScript (.tsx, .jsx)
|
||||
* Updated multilspy, adding support for Kotlin, Dart and C/C++ and several improvements.
|
||||
|
||||
|
||||
# 2025-04-07
|
||||
|
||||
@@ -3,15 +3,9 @@
|
||||
<img src="resources/serena-logo-dark-mode.svg#gh-dark-mode-only" style="width:500px">
|
||||
</p>
|
||||
|
||||
* :rocket: Serena is a powerful, fully-featured **coding agent that works directly on your codebase**.
|
||||
* :wrench: Serena **integrates with existing LLMs**, providing them with essential **semantic code retrieval and editing tools!**
|
||||
* :free: Serena is **free to use**. No API keys or subscriptions required!
|
||||
|
||||
Q: Can I have a state-of-the-art coding agent without paying (enormous) API costs
|
||||
or constantly purchasing tokens?
|
||||
A: Yes, you can!
|
||||
By integrating Serena with your favourite (even free) LLM and thereby enabling it
|
||||
to perform coding tasks directly on your codebase.
|
||||
* :rocket: Serena is a powerful **coding agent toolkit** capable of turning an LLM into a fully-featured agent that works **directly on your codebase**.
|
||||
* :wrench: Serena provides essential **semantic code retrieval and editing tools** that are akin to an IDE's capabilities, extracting code entities at the symbol level and exploiting relational structure.
|
||||
* :free: Serena is **free & open-source**, enhancing the capabilities of LLMs you already have access to free of charge.
|
||||
|
||||
### Demonstration
|
||||
|
||||
@@ -27,14 +21,17 @@ orchestrating tool use.
|
||||
|
||||
Serena can be integrated with an LLM in several ways:
|
||||
* by using the **model context protocol (MCP)**.
|
||||
Serena provides an MCP server which integrates with Claude (and [soon also ChatGPT](https://x.com/OpenAIDevs/status/1904957755829481737)).
|
||||
Serena provides an MCP server which integrates with
|
||||
* Claude Desktop,
|
||||
* IDEs like VSCode, Cursor or IntelliJ,
|
||||
* and [soon also ChatGPT](https://x.com/OpenAIDevs/status/1904957755829481737)
|
||||
* by using **Agno – the model-agnostic agent framework**.
|
||||
Serena's Agno-based agent allows you to turn virtually any LLM into a coding agent, whether it's provided by Google, OpenAI or DeepSeek (with a paid API key)
|
||||
Serena's Agno-based agent allows you to turn virtually any LLM into a coding agent, whether it's provided by Google, OpenAI or Anthropic (with a paid API key)
|
||||
or a free model provided by Ollama, Together or Anyscale.
|
||||
* by incorporating Serena's tools into an agent framework of your choice.
|
||||
Serena's tool implementation is decoupled from the framework-specific code and can thus easily be adapted to any agent framework.
|
||||
|
||||
### Programming Language Support & Semantic Analysis
|
||||
### Programming Language Support & Semantic Analysis Capabilities
|
||||
|
||||
Serena's semantic code analysis capabilities build on **language servers** using the widely implemented
|
||||
language server protocol (LSP). The LSP provides a set of versatile code querying
|
||||
@@ -56,6 +53,9 @@ With Serena, we provide
|
||||
* Go (untested)
|
||||
* C# (untested)
|
||||
* Rust (untested)
|
||||
* Kotlin (untested)
|
||||
* Dart (untested)
|
||||
* C/C++ (untested)
|
||||
|
||||
These languages are supported by the language server library [multilspy](https://github.com/microsoft/multilspy), which Serena uses under the hood.
|
||||
But we did not explicitly test whether the support for these languages actually works.
|
||||
@@ -63,15 +63,6 @@ With Serena, we provide
|
||||
Further languages can, in principle, easily be supported by providing a shallow adapter for a new language server
|
||||
implementation.
|
||||
|
||||
Coming soon: Kotlin and Dart.
|
||||
|
||||
|
||||
> ⚠️ **Note:** Serena is under active development, we are continuously adding features, improving stability and UX.
|
||||
> As a result, configuration might change in a breaking way. If you have an invalid configuration,
|
||||
> the MCP server (or the Serena Agent) may not start properly (investigate the MCP logs in the former case).
|
||||
> Check the [changelog](CHANGELOG.md)
|
||||
> and the configuration templates when updating Serena and update your configs accordingly.
|
||||
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -80,8 +71,8 @@ Coming soon: Kotlin and Dart.
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
- [Is It Really Free to Use?](#is-it-really-free-to-use)
|
||||
- [What Can I Use Serena For?](#what-can-i-use-serena-for)
|
||||
- [Free Coding Agents with Serena](#free-coding-agents-with-serena)
|
||||
- [Quick Start](#quick-start)
|
||||
* [Setup and Configuration](#setup-and-configuration)
|
||||
* [MCP Server (Claude Desktop)](#mcp-server-claude-desktop)
|
||||
@@ -113,9 +104,24 @@ Coming soon: Kotlin and Dart.
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
## Is It Really Free to Use?
|
||||
## What Can I Use Serena For?
|
||||
|
||||
Yes! Even the free tier of Anthropic's Claude has support for MCP Servers, so you can use Serena with Claude for free.
|
||||
You can use Serena for any coding tasks – whether it is focussed on analysis, planning,
|
||||
designing new components or refactoring existing ones.
|
||||
Since Serena's tools allow an LLM to close the cognitive perception-action loop,
|
||||
agents based on Serena can autonomously carry out coding tasks from start to finish –
|
||||
from the initial analysis to the implementation, testing and, finally, the version
|
||||
control system commit.
|
||||
|
||||
Serena can read, write and execute code, read logs and the terminal output.
|
||||
While we do not necessarily encourage it, "vibe coding" is certainly possible, and if you
|
||||
want to almost feel like "the code no longer exists",
|
||||
you may find Serena even more adequate for vibing than an agent inside an IDE
|
||||
(since you will have a separate GUI that really lets you forget).
|
||||
|
||||
## Free Coding Agents with Serena
|
||||
|
||||
Even the free tier of Anthropic's Claude has support for MCP Servers, so you can use Serena with Claude for free.
|
||||
Presumably, the same will soon be possible with ChatGPT Desktop once support for MCP servers is added.
|
||||
Through Agno, you furthermore have the option to use Serena with a free/open-weights model.
|
||||
|
||||
@@ -127,14 +133,6 @@ IDE-based subscriptions (such as Windsurf or Cursor) that forced us to keep purc
|
||||
The substantial API costs incurred by tools like Claude Code, Cline, Aider and other API-based tools are similarly unattractive.
|
||||
We thus built Serena with the prospect of being able to cancel most other subscriptions.
|
||||
|
||||
## What Can I Use Serena For?
|
||||
|
||||
You can use Serena for any coding tasks – analyzing, planning, editing and so on.
|
||||
Serena can read, write and execute code, read logs and the terminal output.
|
||||
"Vibe coding" is possible, and if you want to almost feel like "the code no longer exists",
|
||||
you may find Serena even more adequate for vibing than an agent inside an IDE
|
||||
(since you will have a separate GUI that really lets you forget).
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Setup and Configuration
|
||||
@@ -147,7 +145,13 @@ Serena can read, write and execute code, read logs and the terminal output.
|
||||
5. If you want Serena to dynamically switch between projects, add the list of all project files
|
||||
created in the previous step to the `projects` list in `serena_config.yml`.
|
||||
|
||||
After this initial setup, continue with one of the sections below, depending on how you
|
||||
> ⚠️ **Note:** Serena is under active development. We are continuously adding features, improving stability and the UX.
|
||||
> As a result, configuration may change in a breaking manner. If you have an invalid configuration,
|
||||
> the MCP server or Serena-based Agent may fail to start (investigate the MCP logs in the former case).
|
||||
> Check the [changelog](CHANGELOG.md)
|
||||
> and the configuration templates when updating Serena, adapting your configurations accordingly.
|
||||
|
||||
After the initial setup, continue with one of the sections below, depending on how you
|
||||
want to use Serena.
|
||||
|
||||
### MCP Server (Claude Desktop)
|
||||
@@ -195,9 +199,6 @@ necessarily has to be started by the client in order for communication to take p
|
||||
In other words, you do not need to start the server yourself. The client application (e.g. Claude Desktop) takes care of this and
|
||||
therefore needs to be configured with a launch command.
|
||||
|
||||
ℹ️ Furthermore note that Serena is always configured *for a single project*. To use it for another, you will have to
|
||||
write a new configuration file, adjust the configuration to point to it and then restart the client.
|
||||
|
||||
For more information on MCP servers with Claude Desktop, see [the official quick start guide](https://modelcontextprotocol.io/quickstart/user).
|
||||
|
||||
### Other MCP Clients - Cline, Roo-Code, Cursor, Windsurf etc.
|
||||
|
||||
@@ -31,6 +31,7 @@ dependencies = [
|
||||
"jinja2>=3.1.6",
|
||||
"dotenv>=0.9.9",
|
||||
"pathspec>=0.12.1",
|
||||
"psutil>=7.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -1 +1 @@
|
||||
07ffa48166beb0c7f20ac76dbf7724d0bca4bbb4
|
||||
43209c8521e93e8c41bdd2060800474fd0b9f8b1
|
||||
@@ -1 +1 @@
|
||||
e766d55fd568f5313fb46796b8463097bdb9a293
|
||||
90c42923c8299348634a3ebf7a23d9411d55ad65
|
||||
+237
-179
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import stat
|
||||
import pathlib
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncIterator
|
||||
|
||||
from multilspy.multilspy_logger import MultilspyLogger
|
||||
from multilspy.language_server import LanguageServer
|
||||
from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
from multilspy.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from multilspy.multilspy_config import MultilspyConfig
|
||||
from multilspy.multilspy_utils import FileUtils
|
||||
from multilspy.multilspy_utils import PlatformUtils
|
||||
|
||||
|
||||
class ClangdLanguageServer(LanguageServer):
|
||||
"""
|
||||
Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++.
|
||||
As the project gets bigger in size, building index will take time. Try running clangd multiple times to ensure index is built properly.
|
||||
Also make sure compile_commands.json is created at root of the source directory. Check clangd test case for example.
|
||||
"""
|
||||
|
||||
def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str):
|
||||
"""
|
||||
Creates a ClangdLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
clangd_executable_path = self.setup_runtime_dependencies(logger, config)
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path),
|
||||
"cpp",
|
||||
)
|
||||
self.server_ready = asyncio.Event()
|
||||
|
||||
def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str:
|
||||
"""
|
||||
Setup runtime dependencies for ClangdLanguageServer.
|
||||
"""
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f:
|
||||
d = json.load(f)
|
||||
del d["_description"]
|
||||
|
||||
assert platform_id.value in [
|
||||
"linux-x64"
|
||||
], "Only linux-x64 is supported for in multilspy at the moment"
|
||||
|
||||
runtime_dependencies = d["runtimeDependencies"]
|
||||
runtime_dependencies = [
|
||||
dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value
|
||||
]
|
||||
assert len(runtime_dependencies) == 1
|
||||
dependency = runtime_dependencies[0]
|
||||
|
||||
clangd_ls_dir = os.path.join(os.path.dirname(__file__), "static/clangd")
|
||||
clangd_executable_path = os.path.join(clangd_ls_dir, "clangd_19.1.2", "bin", dependency["binaryName"])
|
||||
if not os.path.exists(clangd_ls_dir):
|
||||
os.makedirs(clangd_ls_dir)
|
||||
if dependency["archiveType"] == "zip":
|
||||
FileUtils.download_and_extract_archive(
|
||||
logger, dependency["url"], clangd_ls_dir, dependency["archiveType"]
|
||||
)
|
||||
assert os.path.exists(clangd_executable_path)
|
||||
os.chmod(clangd_executable_path, stat.S_IEXEC)
|
||||
|
||||
return clangd_executable_path
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the clangd Language Server.
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r") as f:
|
||||
d = json.load(f)
|
||||
|
||||
del d["_description"]
|
||||
|
||||
d["processId"] = os.getpid()
|
||||
assert d["rootPath"] == "$rootPath"
|
||||
d["rootPath"] = repository_absolute_path
|
||||
|
||||
assert d["rootUri"] == "$rootUri"
|
||||
d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri()
|
||||
|
||||
assert d["workspaceFolders"][0]["uri"] == "$uri"
|
||||
d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri()
|
||||
|
||||
assert d["workspaceFolders"][0]["name"] == "$name"
|
||||
d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path)
|
||||
|
||||
return d
|
||||
|
||||
@asynccontextmanager
|
||||
async def start_server(self) -> AsyncIterator["ClangdLanguageServer"]:
|
||||
"""
|
||||
Starts the Clangd Language Server, waits for the server to be ready and yields the LanguageServer instance.
|
||||
|
||||
Usage:
|
||||
```
|
||||
async with lsp.start_server():
|
||||
# LanguageServer has been initialized and ready to serve requests
|
||||
await lsp.request_definition(...)
|
||||
await lsp.request_references(...)
|
||||
# Shutdown the LanguageServer on exit from scope
|
||||
# LanguageServer has been shutdown
|
||||
"""
|
||||
async def register_capability_handler(params):
|
||||
assert "registrations" in params
|
||||
for registration in params["registrations"]:
|
||||
if registration["method"] == "workspace/executeCommand":
|
||||
self.initialize_searcher_command_available.set()
|
||||
self.resolve_main_method_available.set()
|
||||
return
|
||||
|
||||
async def lang_status_handler(params):
|
||||
# TODO: Should we wait for
|
||||
# server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}}
|
||||
# Before proceeding?
|
||||
if params["type"] == "ServiceReady" and params["message"] == "ServiceReady":
|
||||
self.service_ready_event.set()
|
||||
|
||||
async def execute_client_command_handler(params):
|
||||
return []
|
||||
|
||||
async def do_nothing(params):
|
||||
return
|
||||
|
||||
async def check_experimental_status(params):
|
||||
if params["quiescent"] == True:
|
||||
self.server_ready.set()
|
||||
|
||||
async def window_log_message(msg):
|
||||
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
|
||||
|
||||
self.server.on_request("client/registerCapability", register_capability_handler)
|
||||
self.server.on_notification("language/status", lang_status_handler)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
self.server.on_notification("language/actionableNotification", do_nothing)
|
||||
self.server.on_notification("experimental/serverStatus", check_experimental_status)
|
||||
|
||||
async with super().start_server():
|
||||
self.logger.log("Starting Clangd server process", logging.INFO)
|
||||
await self.server.start()
|
||||
initialize_params = self._get_initialize_params(self.repository_root_path)
|
||||
|
||||
self.logger.log(
|
||||
"Sending initialize request from LSP client to LSP server and awaiting response",
|
||||
logging.INFO,
|
||||
)
|
||||
init_response = await self.server.send.initialize(initialize_params)
|
||||
assert init_response["capabilities"]["textDocumentSync"]["change"] == 2
|
||||
assert "completionProvider" in init_response["capabilities"]
|
||||
assert init_response["capabilities"]["completionProvider"] == {
|
||||
"triggerCharacters": ['.', '<', '>', ':', '"', '/', '*'],
|
||||
"resolveProvider": False,
|
||||
}
|
||||
|
||||
self.server.notify.initialized({})
|
||||
|
||||
self.completions_available.set()
|
||||
# set ready flag
|
||||
self.server_ready.set()
|
||||
await self.server_ready.wait()
|
||||
|
||||
yield self
|
||||
|
||||
await self.server.shutdown()
|
||||
await self.server.stop()
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize",
|
||||
"processId": "os.getpid()",
|
||||
"locale": "en",
|
||||
"rootPath": "$rootPath",
|
||||
"rootUri": "$rootUri",
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {
|
||||
"didSave": true,
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"completion": {
|
||||
"dynamicRegistration": true,
|
||||
"completionItem": {
|
||||
"snippetSupport": true
|
||||
}
|
||||
},
|
||||
"definition": {
|
||||
"dynamicRegistration": true
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": true,
|
||||
"didChangeConfiguration": {
|
||||
"dynamicRegistration": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": "$uri",
|
||||
"name": "$name"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"_description": "Used to download the runtime dependencies for running Clangd.",
|
||||
"runtimeDependencies": [
|
||||
{
|
||||
"id": "Clangd",
|
||||
"description": "Clangd for Linux (x64)",
|
||||
"url": "https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip",
|
||||
"platformId": "linux-x64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "clangd"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import stat
|
||||
from typing import AsyncIterator
|
||||
from multilspy.language_server import LanguageServer
|
||||
from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
import json
|
||||
from multilspy.multilspy_utils import FileUtils, PlatformUtils
|
||||
|
||||
|
||||
class DartLanguageServer(LanguageServer):
|
||||
"""
|
||||
Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart.
|
||||
"""
|
||||
|
||||
def __init__(self, config, logger, repository_root_path):
|
||||
"""
|
||||
Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
|
||||
executable_path = self.setup_runtime_dependencies(logger)
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path),
|
||||
"dart",
|
||||
)
|
||||
|
||||
def setup_runtime_dependencies(self, logger: "MultilspyLogger") -> str:
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f:
|
||||
d = json.load(f)
|
||||
del d["_description"]
|
||||
|
||||
runtime_dependencies = d["runtimeDependencies"]
|
||||
runtime_dependencies = [
|
||||
dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value
|
||||
]
|
||||
|
||||
assert len(runtime_dependencies) == 1
|
||||
dependency = runtime_dependencies[0]
|
||||
|
||||
dart_ls_dir = os.path.join(os.path.dirname(__file__), "static", "dart-language-server")
|
||||
dart_executable_path = os.path.join(dart_ls_dir, dependency["binaryName"])
|
||||
|
||||
if not os.path.exists(dart_ls_dir):
|
||||
os.makedirs(dart_ls_dir)
|
||||
FileUtils.download_and_extract_archive(
|
||||
logger, dependency["url"], dart_ls_dir, dependency["archiveType"]
|
||||
)
|
||||
|
||||
|
||||
assert os.path.exists(dart_executable_path)
|
||||
os.chmod(dart_executable_path, stat.S_IEXEC)
|
||||
|
||||
return f"{dart_executable_path} language-server --client-id multilspy.dart --client-version 1.2"
|
||||
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str):
|
||||
"""
|
||||
Returns the initialize params for the Dart Language Server.
|
||||
"""
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r"
|
||||
) as f:
|
||||
d = json.load(f)
|
||||
|
||||
del d["_description"]
|
||||
|
||||
d["processId"] = os.getpid()
|
||||
assert d["rootPath"] == "$rootPath"
|
||||
d["rootPath"] = repository_absolute_path
|
||||
|
||||
assert d["rootUri"] == "$rootUri"
|
||||
d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri()
|
||||
|
||||
assert d["workspaceFolders"][0]["uri"] == "$uri"
|
||||
d["workspaceFolders"][0]["uri"] = pathlib.Path(
|
||||
repository_absolute_path
|
||||
).as_uri()
|
||||
|
||||
assert d["workspaceFolders"][0]["name"] == "$name"
|
||||
d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path)
|
||||
|
||||
return d
|
||||
|
||||
@asynccontextmanager
|
||||
async def start_server(self) -> AsyncIterator["DartLanguageServer"]:
|
||||
"""
|
||||
Start the language server and yield when the server is ready.
|
||||
"""
|
||||
|
||||
async def execute_client_command_handler(params):
|
||||
return []
|
||||
|
||||
async def do_nothing(params):
|
||||
return
|
||||
|
||||
async def check_experimental_status(params):
|
||||
pass
|
||||
|
||||
async def window_log_message(msg):
|
||||
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
|
||||
|
||||
self.server.on_request("client/registerCapability", do_nothing)
|
||||
self.server.on_notification("language/status", do_nothing)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_request(
|
||||
"workspace/executeClientCommand", execute_client_command_handler
|
||||
)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
self.server.on_notification("language/actionableNotification", do_nothing)
|
||||
self.server.on_notification(
|
||||
"experimental/serverStatus", check_experimental_status
|
||||
)
|
||||
|
||||
async with super().start_server():
|
||||
self.logger.log(
|
||||
"Starting dart-language-server server process", logging.INFO
|
||||
)
|
||||
await self.server.start()
|
||||
initialize_params = self._get_initialize_params(self.repository_root_path)
|
||||
self.logger.log(
|
||||
"Sending initialize request to dart-language-server",
|
||||
logging.DEBUG,
|
||||
)
|
||||
init_response = await self.server.send_request(
|
||||
"initialize", initialize_params
|
||||
)
|
||||
self.logger.log(
|
||||
f"Received initialize response from dart-language-server: {init_response}",
|
||||
logging.INFO,
|
||||
)
|
||||
|
||||
self.server.notify.initialized({})
|
||||
|
||||
yield self
|
||||
|
||||
await self.server.shutdown()
|
||||
await self.server.stop()
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"_description": "This file contains the initialization parameters for the Dart Language Server.",
|
||||
"processId": "$processId",
|
||||
"rootPath": "$rootPath",
|
||||
"rootUri": "$rootUri",
|
||||
"capabilities": {},
|
||||
"initializationOptions": {
|
||||
"onlyAnalyzeProjectsWithOpenFiles": false,
|
||||
"suggestFromUnimportedLibraries": true,
|
||||
"closingLabels": false,
|
||||
"outline": false,
|
||||
"flutterOutline": false,
|
||||
"allowOpenUri": false
|
||||
},
|
||||
"trace": "verbose",
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": "$uri",
|
||||
"name": "$name"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"_description": "Used to download the runtime dependencies for running Dart Language Server, downloaded from https://dart.dev/get-dart/archive",
|
||||
"runtimeDependencies": [
|
||||
{
|
||||
"id": "DartLanguageServer",
|
||||
"description": "Dart Language Server for Linux (x64)",
|
||||
"url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-x64-release.zip",
|
||||
"platformId": "linux-x64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "dart-sdk/bin/dart"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -23,6 +23,16 @@
|
||||
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar",
|
||||
"jdtls_readonly_config_path": "extension/server/config_mac_arm"
|
||||
},
|
||||
"osx-x64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@darwin-x64-1.23.0.vsix",
|
||||
"archiveType": "zip",
|
||||
"relative_extraction_path": "vscode-java",
|
||||
"jre_home_path": "extension/jre/17.0.8.1-macosx-x86_64",
|
||||
"jre_path": "extension/jre/17.0.8.1-macosx-x86_64/bin/java",
|
||||
"lombok_jar_path": "extension/lombok/lombok-1.18.30.jar",
|
||||
"jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar",
|
||||
"jdtls_readonly_config_path": "extension/server/config_mac"
|
||||
},
|
||||
"linux-arm64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@linux-arm64-1.23.0.vsix",
|
||||
"archiveType": "zip",
|
||||
@@ -59,4 +69,4 @@
|
||||
"intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,521 @@
|
||||
{
|
||||
"_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize",
|
||||
"processId": "os.getpid()",
|
||||
"clientInfo": {
|
||||
"name": "Multilspy Kotlin Client",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"locale": "en",
|
||||
"rootPath": "repository_absolute_path",
|
||||
"rootUri": "pathlib.Path(repository_absolute_path).as_uri()",
|
||||
"capabilities": {
|
||||
"workspace": {
|
||||
"applyEdit": true,
|
||||
"workspaceEdit": {
|
||||
"documentChanges": true,
|
||||
"resourceOperations": [
|
||||
"create",
|
||||
"rename",
|
||||
"delete"
|
||||
],
|
||||
"failureHandling": "textOnlyTransactional",
|
||||
"normalizesLineEndings": true,
|
||||
"changeAnnotationSupport": {
|
||||
"groupsOnLabel": true
|
||||
}
|
||||
},
|
||||
"didChangeConfiguration": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"didChangeWatchedFiles": {
|
||||
"dynamicRegistration": true,
|
||||
"relativePatternSupport": true
|
||||
},
|
||||
"symbol": {
|
||||
"dynamicRegistration": true,
|
||||
"symbolKind": {
|
||||
"valueSet": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26
|
||||
]
|
||||
},
|
||||
"tagSupport": {
|
||||
"valueSet": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"resolveSupport": {
|
||||
"properties": [
|
||||
"location.range"
|
||||
]
|
||||
}
|
||||
},
|
||||
"codeLens": {
|
||||
"refreshSupport": true
|
||||
},
|
||||
"executeCommand": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"configuration": true,
|
||||
"workspaceFolders": true,
|
||||
"semanticTokens": {
|
||||
"refreshSupport": true
|
||||
},
|
||||
"fileOperations": {
|
||||
"dynamicRegistration": true,
|
||||
"didCreate": true,
|
||||
"didRename": true,
|
||||
"didDelete": true,
|
||||
"willCreate": true,
|
||||
"willRename": true,
|
||||
"willDelete": true
|
||||
},
|
||||
"inlineValue": {
|
||||
"refreshSupport": true
|
||||
},
|
||||
"inlayHint": {
|
||||
"refreshSupport": true
|
||||
},
|
||||
"diagnostics": {
|
||||
"refreshSupport": true
|
||||
}
|
||||
},
|
||||
"textDocument": {
|
||||
"publishDiagnostics": {
|
||||
"relatedInformation": true,
|
||||
"versionSupport": false,
|
||||
"tagSupport": {
|
||||
"valueSet": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
"codeDescriptionSupport": true,
|
||||
"dataSupport": true
|
||||
},
|
||||
"synchronization": {
|
||||
"dynamicRegistration": true,
|
||||
"willSave": true,
|
||||
"willSaveWaitUntil": true,
|
||||
"didSave": true
|
||||
},
|
||||
"completion": {
|
||||
"dynamicRegistration": true,
|
||||
"contextSupport": true,
|
||||
"completionItem": {
|
||||
"snippetSupport": false,
|
||||
"commitCharactersSupport": true,
|
||||
"documentationFormat": [
|
||||
"markdown",
|
||||
"plaintext"
|
||||
],
|
||||
"deprecatedSupport": true,
|
||||
"preselectSupport": true,
|
||||
"tagSupport": {
|
||||
"valueSet": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"insertReplaceSupport": false,
|
||||
"resolveSupport": {
|
||||
"properties": [
|
||||
"documentation",
|
||||
"detail",
|
||||
"additionalTextEdits"
|
||||
]
|
||||
},
|
||||
"insertTextModeSupport": {
|
||||
"valueSet": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
"labelDetailsSupport": true
|
||||
},
|
||||
"insertTextMode": 2,
|
||||
"completionItemKind": {
|
||||
"valueSet": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25
|
||||
]
|
||||
},
|
||||
"completionList": {
|
||||
"itemDefaults": [
|
||||
"commitCharacters",
|
||||
"editRange",
|
||||
"insertTextFormat",
|
||||
"insertTextMode"
|
||||
]
|
||||
}
|
||||
},
|
||||
"hover": {
|
||||
"dynamicRegistration": true,
|
||||
"contentFormat": [
|
||||
"markdown",
|
||||
"plaintext"
|
||||
]
|
||||
},
|
||||
"signatureHelp": {
|
||||
"dynamicRegistration": true,
|
||||
"signatureInformation": {
|
||||
"documentationFormat": [
|
||||
"markdown",
|
||||
"plaintext"
|
||||
],
|
||||
"parameterInformation": {
|
||||
"labelOffsetSupport": true
|
||||
},
|
||||
"activeParameterSupport": true
|
||||
},
|
||||
"contextSupport": true
|
||||
},
|
||||
"definition": {
|
||||
"dynamicRegistration": true,
|
||||
"linkSupport": true
|
||||
},
|
||||
"references": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"documentHighlight": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": true,
|
||||
"symbolKind": {
|
||||
"valueSet": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26
|
||||
]
|
||||
},
|
||||
"hierarchicalDocumentSymbolSupport": true,
|
||||
"tagSupport": {
|
||||
"valueSet": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"labelSupport": true
|
||||
},
|
||||
"codeAction": {
|
||||
"dynamicRegistration": true,
|
||||
"isPreferredSupport": true,
|
||||
"disabledSupport": true,
|
||||
"dataSupport": true,
|
||||
"resolveSupport": {
|
||||
"properties": [
|
||||
"edit"
|
||||
]
|
||||
},
|
||||
"codeActionLiteralSupport": {
|
||||
"codeActionKind": {
|
||||
"valueSet": [
|
||||
"",
|
||||
"quickfix",
|
||||
"refactor",
|
||||
"refactor.extract",
|
||||
"refactor.inline",
|
||||
"refactor.rewrite",
|
||||
"source",
|
||||
"source.organizeImports"
|
||||
]
|
||||
}
|
||||
},
|
||||
"honorsChangeAnnotations": false
|
||||
},
|
||||
"codeLens": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"formatting": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"rangeFormatting": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"onTypeFormatting": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"rename": {
|
||||
"dynamicRegistration": true,
|
||||
"prepareSupport": true,
|
||||
"prepareSupportDefaultBehavior": 1,
|
||||
"honorsChangeAnnotations": true
|
||||
},
|
||||
"documentLink": {
|
||||
"dynamicRegistration": true,
|
||||
"tooltipSupport": true
|
||||
},
|
||||
"typeDefinition": {
|
||||
"dynamicRegistration": true,
|
||||
"linkSupport": true
|
||||
},
|
||||
"implementation": {
|
||||
"dynamicRegistration": true,
|
||||
"linkSupport": true
|
||||
},
|
||||
"colorProvider": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"foldingRange": {
|
||||
"dynamicRegistration": true,
|
||||
"rangeLimit": 5000,
|
||||
"lineFoldingOnly": true,
|
||||
"foldingRangeKind": {
|
||||
"valueSet": [
|
||||
"comment",
|
||||
"imports",
|
||||
"region"
|
||||
]
|
||||
},
|
||||
"foldingRange": {
|
||||
"collapsedText": false
|
||||
}
|
||||
},
|
||||
"declaration": {
|
||||
"dynamicRegistration": true,
|
||||
"linkSupport": true
|
||||
},
|
||||
"selectionRange": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"callHierarchy": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"semanticTokens": {
|
||||
"dynamicRegistration": true,
|
||||
"tokenTypes": [
|
||||
"namespace",
|
||||
"type",
|
||||
"class",
|
||||
"enum",
|
||||
"interface",
|
||||
"struct",
|
||||
"typeParameter",
|
||||
"parameter",
|
||||
"variable",
|
||||
"property",
|
||||
"enumMember",
|
||||
"event",
|
||||
"function",
|
||||
"method",
|
||||
"macro",
|
||||
"keyword",
|
||||
"modifier",
|
||||
"comment",
|
||||
"string",
|
||||
"number",
|
||||
"regexp",
|
||||
"operator",
|
||||
"decorator"
|
||||
],
|
||||
"tokenModifiers": [
|
||||
"declaration",
|
||||
"definition",
|
||||
"readonly",
|
||||
"static",
|
||||
"deprecated",
|
||||
"abstract",
|
||||
"async",
|
||||
"modification",
|
||||
"documentation",
|
||||
"defaultLibrary"
|
||||
],
|
||||
"formats": [
|
||||
"relative"
|
||||
],
|
||||
"requests": {
|
||||
"range": true,
|
||||
"full": {
|
||||
"delta": true
|
||||
}
|
||||
},
|
||||
"multilineTokenSupport": false,
|
||||
"overlappingTokenSupport": false,
|
||||
"serverCancelSupport": true,
|
||||
"augmentsSyntaxTokens": true
|
||||
},
|
||||
"linkedEditingRange": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"typeHierarchy": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"inlineValue": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"inlayHint": {
|
||||
"dynamicRegistration": true,
|
||||
"resolveSupport": {
|
||||
"properties": [
|
||||
"tooltip",
|
||||
"textEdits",
|
||||
"label.tooltip",
|
||||
"label.location",
|
||||
"label.command"
|
||||
]
|
||||
}
|
||||
},
|
||||
"diagnostic": {
|
||||
"dynamicRegistration": true,
|
||||
"relatedDocumentSupport": false
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"showMessage": {
|
||||
"messageActionItem": {
|
||||
"additionalPropertiesSupport": true
|
||||
}
|
||||
},
|
||||
"showDocument": {
|
||||
"support": true
|
||||
},
|
||||
"workDoneProgress": true
|
||||
},
|
||||
"general": {
|
||||
"staleRequestSupport": {
|
||||
"cancel": true,
|
||||
"retryOnContentModified": [
|
||||
"textDocument/semanticTokens/full",
|
||||
"textDocument/semanticTokens/range",
|
||||
"textDocument/semanticTokens/full/delta"
|
||||
]
|
||||
},
|
||||
"regularExpressions": {
|
||||
"engine": "ECMAScript",
|
||||
"version": "ES2020"
|
||||
},
|
||||
"markdown": {
|
||||
"parser": "marked",
|
||||
"version": "1.1.0"
|
||||
},
|
||||
"positionEncodings": [
|
||||
"utf-16"
|
||||
]
|
||||
},
|
||||
"notebookDocument": {
|
||||
"synchronization": {
|
||||
"dynamicRegistration": true,
|
||||
"executionSummarySupport": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"initializationOptions": {
|
||||
"workspaceFolders": "[pathlib.Path(repository_absolute_path).as_uri()]",
|
||||
"storagePath": null,
|
||||
"codegen": {
|
||||
"enabled": false
|
||||
},
|
||||
"compiler": {
|
||||
"jvm": {
|
||||
"target": "default"
|
||||
}
|
||||
},
|
||||
"completion": {
|
||||
"snippets": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"diagnostics": {
|
||||
"enabled": true,
|
||||
"level": 4,
|
||||
"debounceTime": 250
|
||||
},
|
||||
"scripts": {
|
||||
"enabled": true,
|
||||
"buildScriptsEnabled": true
|
||||
},
|
||||
"indexing": {
|
||||
"enabled": true
|
||||
},
|
||||
"externalSources": {
|
||||
"useKlsScheme": false,
|
||||
"autoConvertToKotlin": false
|
||||
},
|
||||
"inlayHints": {
|
||||
"typeHints": false,
|
||||
"parameterHints": false,
|
||||
"chainedHints": false
|
||||
},
|
||||
"formatting": {
|
||||
"formatter": "ktfmt",
|
||||
"ktfmt": {
|
||||
"style": "google",
|
||||
"indent": 4,
|
||||
"maxWidth": 100,
|
||||
"continuationIndent": 8,
|
||||
"removeUnusedImports": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"trace": "verbose",
|
||||
"workspaceFolders": "[\n {\n \"uri\": pathlib.Path(repository_absolute_path).as_uri(),\n \"name\": os.path.basename(repository_absolute_path),\n }\n ]"
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import stat
|
||||
import pathlib
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncIterator
|
||||
|
||||
from multilspy.multilspy_logger import MultilspyLogger
|
||||
from multilspy.language_server import LanguageServer
|
||||
from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
from multilspy.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from multilspy.multilspy_config import MultilspyConfig
|
||||
from multilspy.multilspy_utils import FileUtils
|
||||
from multilspy.multilspy_utils import PlatformUtils
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class KotlinRuntimeDependencyPaths:
|
||||
"""
|
||||
Stores the paths to the runtime dependencies of Kotlin Language Server
|
||||
"""
|
||||
java_path: str
|
||||
java_home_path: str
|
||||
kotlin_executable_path: str
|
||||
|
||||
|
||||
class KotlinLanguageServer(LanguageServer):
|
||||
"""
|
||||
Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin.
|
||||
"""
|
||||
|
||||
def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str):
|
||||
"""
|
||||
Creates a Kotlin Language Server instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
runtime_dependency_paths = self.setup_runtime_dependencies(logger, config)
|
||||
self.runtime_dependency_paths = runtime_dependency_paths
|
||||
|
||||
# Create command to execute the Kotlin Language Server script
|
||||
cmd = f'"{self.runtime_dependency_paths.kotlin_executable_path}"'
|
||||
|
||||
# Set environment variables including JAVA_HOME
|
||||
proc_env = {"JAVA_HOME": self.runtime_dependency_paths.java_home_path}
|
||||
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=cmd, env=proc_env, cwd=repository_root_path),
|
||||
"kotlin",
|
||||
)
|
||||
|
||||
def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> KotlinRuntimeDependencyPaths:
|
||||
"""
|
||||
Setup runtime dependencies for Kotlin Language Server.
|
||||
"""
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
|
||||
# Verify platform support
|
||||
assert platform_id.value.startswith("win-") or platform_id.value.startswith("linux-") or platform_id.value.startswith("osx-"), "Only Windows, Linux and macOS platforms are supported for Kotlin in multilspy at the moment"
|
||||
|
||||
# Load dependency information
|
||||
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f:
|
||||
d = json.load(f)
|
||||
del d["_description"]
|
||||
|
||||
kotlin_dependency = d["runtimeDependency"]
|
||||
java_dependency = d["java"][platform_id.value]
|
||||
|
||||
# Setup paths for dependencies
|
||||
static_dir = os.path.join(os.path.dirname(__file__), "static")
|
||||
os.makedirs(static_dir, exist_ok=True)
|
||||
|
||||
# Setup Java paths
|
||||
java_dir = os.path.join(static_dir, "java")
|
||||
os.makedirs(java_dir, exist_ok=True)
|
||||
|
||||
java_home_path = os.path.join(java_dir, java_dependency["java_home_path"])
|
||||
java_path = os.path.join(java_dir, java_dependency["java_path"])
|
||||
|
||||
# Download and extract Java if not exists
|
||||
if not os.path.exists(java_path):
|
||||
logger.log(f"Downloading Java for {platform_id.value}...", logging.INFO)
|
||||
FileUtils.download_and_extract_archive(
|
||||
logger, java_dependency["url"], java_dir, java_dependency["archiveType"]
|
||||
)
|
||||
# Make Java executable
|
||||
if not platform_id.value.startswith("win-"):
|
||||
os.chmod(java_path, 0o755)
|
||||
|
||||
assert os.path.exists(java_path), f"Java executable not found at {java_path}"
|
||||
|
||||
# Setup Kotlin Language Server paths
|
||||
kotlin_ls_dir = os.path.join(static_dir, "server")
|
||||
|
||||
# Get platform-specific executable script path
|
||||
if platform_id.value.startswith("win-"):
|
||||
kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server.bat")
|
||||
else:
|
||||
kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server")
|
||||
|
||||
# Download and extract Kotlin Language Server if script doesn't exist
|
||||
if not os.path.exists(kotlin_script):
|
||||
logger.log("Downloading Kotlin Language Server...", logging.INFO)
|
||||
FileUtils.download_and_extract_archive(
|
||||
logger, kotlin_dependency["url"], static_dir, kotlin_dependency["archiveType"]
|
||||
)
|
||||
|
||||
# Make script executable on Unix platforms
|
||||
if os.path.exists(kotlin_script) and not platform_id.value.startswith("win-"):
|
||||
os.chmod(kotlin_script, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
||||
|
||||
# Use script file
|
||||
if os.path.exists(kotlin_script):
|
||||
kotlin_executable_path = kotlin_script
|
||||
logger.log(f"Using Kotlin Language Server script at {kotlin_script}", logging.INFO)
|
||||
else:
|
||||
raise FileNotFoundError(f"Kotlin Language Server script not found at {kotlin_script}")
|
||||
|
||||
return KotlinRuntimeDependencyPaths(
|
||||
java_path=java_path,
|
||||
java_home_path=java_home_path,
|
||||
kotlin_executable_path=kotlin_executable_path
|
||||
)
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Kotlin Language Server.
|
||||
"""
|
||||
with open(str(pathlib.PurePath(os.path.dirname(__file__), "initialize_params.json")), "r") as f:
|
||||
d: InitializeParams = json.load(f)
|
||||
|
||||
del d["_description"]
|
||||
|
||||
if not os.path.isabs(repository_absolute_path):
|
||||
repository_absolute_path = os.path.abspath(repository_absolute_path)
|
||||
|
||||
assert d["processId"] == "os.getpid()"
|
||||
d["processId"] = os.getpid()
|
||||
|
||||
assert d["rootPath"] == "repository_absolute_path"
|
||||
d["rootPath"] = repository_absolute_path
|
||||
|
||||
assert d["rootUri"] == "pathlib.Path(repository_absolute_path).as_uri()"
|
||||
d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri()
|
||||
|
||||
assert d["initializationOptions"]["workspaceFolders"] == "[pathlib.Path(repository_absolute_path).as_uri()]"
|
||||
d["initializationOptions"]["workspaceFolders"] = [pathlib.Path(repository_absolute_path).as_uri()]
|
||||
|
||||
assert (
|
||||
d["workspaceFolders"]
|
||||
== '[\n {\n "uri": pathlib.Path(repository_absolute_path).as_uri(),\n "name": os.path.basename(repository_absolute_path),\n }\n ]'
|
||||
)
|
||||
d["workspaceFolders"] = [
|
||||
{
|
||||
"uri": pathlib.Path(repository_absolute_path).as_uri(),
|
||||
"name": os.path.basename(repository_absolute_path),
|
||||
}
|
||||
]
|
||||
|
||||
return d
|
||||
|
||||
@asynccontextmanager
|
||||
async def start_server(self) -> AsyncIterator["KotlinLanguageServer"]:
|
||||
"""
|
||||
Starts the Kotlin Language Server, waits for the server to be ready and yields the LanguageServer instance.
|
||||
|
||||
Usage:
|
||||
```
|
||||
async with lsp.start_server():
|
||||
# LanguageServer has been initialized and ready to serve requests
|
||||
await lsp.request_definition(...)
|
||||
await lsp.request_references(...)
|
||||
# Shutdown the LanguageServer on exit from scope
|
||||
# LanguageServer has been shutdown
|
||||
```
|
||||
"""
|
||||
async def execute_client_command_handler(params):
|
||||
return []
|
||||
|
||||
async def do_nothing(params):
|
||||
return
|
||||
|
||||
async def window_log_message(msg):
|
||||
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
|
||||
|
||||
self.server.on_request("client/registerCapability", do_nothing)
|
||||
self.server.on_notification("language/status", do_nothing)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
self.server.on_notification("language/actionableNotification", do_nothing)
|
||||
|
||||
async with super().start_server():
|
||||
self.logger.log("Starting Kotlin server process", logging.INFO)
|
||||
await self.server.start()
|
||||
initialize_params = self._get_initialize_params(self.repository_root_path)
|
||||
|
||||
self.logger.log(
|
||||
"Sending initialize request from LSP client to LSP server and awaiting response",
|
||||
logging.INFO,
|
||||
)
|
||||
init_response = await self.server.send.initialize(initialize_params)
|
||||
|
||||
capabilities = init_response["capabilities"]
|
||||
assert "textDocumentSync" in capabilities, "Server must support textDocumentSync"
|
||||
assert "hoverProvider" in capabilities, "Server must support hover"
|
||||
assert "completionProvider" in capabilities, "Server must support code completion"
|
||||
assert "signatureHelpProvider" in capabilities, "Server must support signature help"
|
||||
assert "definitionProvider" in capabilities, "Server must support go to definition"
|
||||
assert "referencesProvider" in capabilities, "Server must support find references"
|
||||
assert "documentSymbolProvider" in capabilities, "Server must support document symbols"
|
||||
assert "workspaceSymbolProvider" in capabilities, "Server must support workspace symbols"
|
||||
assert "semanticTokensProvider" in capabilities, "Server must support semantic tokens"
|
||||
|
||||
self.server.notify.initialized({})
|
||||
self.completions_available.set()
|
||||
|
||||
yield self
|
||||
|
||||
try:
|
||||
await self.server.shutdown()
|
||||
except Exception as e:
|
||||
self.logger.log(f"Error during Kotlin server shutdown: {str(e)}", logging.WARNING)
|
||||
finally:
|
||||
await self.server.stop()
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"_description": "Used to download the runtime dependencies for Kotlin Language Server from https://github.com/fwcd/kotlin-language-server",
|
||||
"runtimeDependency": {
|
||||
"id": "KotlinLsp",
|
||||
"description": "Kotlin Language Server",
|
||||
"url": "https://github.com/fwcd/kotlin-language-server/releases/download/1.3.13/server.zip",
|
||||
"archiveType": "zip"
|
||||
},
|
||||
"java": {
|
||||
"win-x64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@win32-x64-1.23.0.vsix",
|
||||
"archiveType": "zip",
|
||||
"java_home_path": "extension/jre/17.0.8.1-win32-x86_64",
|
||||
"java_path": "extension/jre/17.0.8.1-win32-x86_64/bin/java.exe"
|
||||
},
|
||||
"linux-x64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@linux-x64-1.23.0.vsix",
|
||||
"archiveType": "zip",
|
||||
"java_home_path": "extension/jre/17.0.8.1-linux-x86_64",
|
||||
"java_path": "extension/jre/17.0.8.1-linux-x86_64/bin/java"
|
||||
},
|
||||
"linux-arm64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@linux-arm64-1.23.0.vsix",
|
||||
"archiveType": "zip",
|
||||
"java_home_path": "extension/jre/17.0.8.1-linux-aarch64",
|
||||
"java_path": "extension/jre/17.0.8.1-linux-aarch64/bin/java"
|
||||
},
|
||||
"osx-x64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@darwin-x64-1.23.0.vsix",
|
||||
"archiveType": "zip",
|
||||
"java_home_path": "extension/jre/17.0.8.1-macosx-x86_64",
|
||||
"java_path": "extension/jre/17.0.8.1-macosx-x86_64/bin/java"
|
||||
},
|
||||
"osx-arm64": {
|
||||
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@darwin-arm64-1.23.0.vsix",
|
||||
"archiveType": "zip",
|
||||
"java_home_path": "extension/jre/17.0.8.1-macosx-aarch64",
|
||||
"java_path": "extension/jre/17.0.8.1-macosx-aarch64/bin/java"
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
-17
@@ -32,6 +32,11 @@ else:
|
||||
return type('obj', (), {'pw_name': os.environ.get('USERNAME', 'unknown')})()
|
||||
|
||||
|
||||
# Conditionally import pwd module (Unix-only)
|
||||
if not PlatformUtils.get_platform_id().value.startswith("win"):
|
||||
import pwd
|
||||
|
||||
|
||||
class TypeScriptLanguageServer(LanguageServer):
|
||||
"""
|
||||
Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript.
|
||||
@@ -50,7 +55,7 @@ class TypeScriptLanguageServer(LanguageServer):
|
||||
"typescript",
|
||||
)
|
||||
self.server_ready = asyncio.Event()
|
||||
|
||||
|
||||
@override
|
||||
def should_always_ignore(self, dirname: str) -> bool:
|
||||
return super().should_always_ignore(dirname) or dirname in [
|
||||
@@ -95,22 +100,29 @@ class TypeScriptLanguageServer(LanguageServer):
|
||||
if not os.path.exists(tsserver_ls_dir):
|
||||
os.makedirs(tsserver_ls_dir, exist_ok=True)
|
||||
for dependency in runtime_dependencies:
|
||||
# Handle platform-specific user settings
|
||||
subprocess_kwargs = {
|
||||
'shell': True,
|
||||
'check': True,
|
||||
'cwd': tsserver_ls_dir,
|
||||
'stdout': subprocess.DEVNULL,
|
||||
'stderr': subprocess.DEVNULL
|
||||
}
|
||||
|
||||
# Only add user parameter on Unix-like systems
|
||||
if os.name != 'nt': # Not Windows
|
||||
# Windows doesn't support the 'user' parameter and doesn't have pwd module
|
||||
if PlatformUtils.get_platform_id().value.startswith("win"):
|
||||
subprocess.run(
|
||||
dependency["command"],
|
||||
shell=True,
|
||||
check=True,
|
||||
cwd=tsserver_ls_dir,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
else:
|
||||
# On Unix-like systems, run as non-root user
|
||||
user = pwd.getpwuid(os.getuid()).pw_name
|
||||
subprocess_kwargs['user'] = user
|
||||
|
||||
subprocess.run(dependency["command"], **subprocess_kwargs)
|
||||
|
||||
subprocess.run(
|
||||
dependency["command"],
|
||||
shell=True,
|
||||
check=True,
|
||||
user=user,
|
||||
cwd=tsserver_ls_dir,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
|
||||
tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server")
|
||||
|
||||
assert os.path.exists(tsserver_executable_path), "typescript-language-server executable not found. Please install typescript-language-server and try again."
|
||||
@@ -209,4 +221,4 @@ class TypeScriptLanguageServer(LanguageServer):
|
||||
yield self
|
||||
|
||||
await self.server.shutdown()
|
||||
await self.server.stop()
|
||||
await self.server.stop()
|
||||
|
||||
@@ -57,3 +57,6 @@ class LSPConstants:
|
||||
|
||||
# key used to represent children in document symbols
|
||||
CHILDREN = "children"
|
||||
|
||||
# key used to represent the location in symbols
|
||||
LOCATION = "location"
|
||||
|
||||
@@ -33,6 +33,7 @@ import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import psutil
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from .lsp_requests import LspNotification, LspRequest
|
||||
@@ -173,9 +174,18 @@ class LanguageServerHandler:
|
||||
the asynchronous tasks created by the handler.
|
||||
task_counter: An integer that represents the next available task id for the handler.
|
||||
loop: An asyncio.AbstractEventLoop object that represents the event loop used by the handler.
|
||||
start_independent_lsp_process: An optional boolean flag that indicates whether to start the
|
||||
language server process in an independent process group. Default is `True`. Setting it to
|
||||
`False` means that the language server process will be in the same process group as the
|
||||
the current process, and any SIGINT and SIGTERM signals will be sent to both processes.
|
||||
"""
|
||||
|
||||
def __init__(self, process_launch_info: ProcessLaunchInfo, logger=None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
process_launch_info: ProcessLaunchInfo,
|
||||
logger=None,
|
||||
start_independent_lsp_process=True,
|
||||
) -> None:
|
||||
"""
|
||||
Params:
|
||||
cmd: A string that represents the command to launch the language server process.
|
||||
@@ -197,6 +207,7 @@ class LanguageServerHandler:
|
||||
self.tasks = {}
|
||||
self.task_counter = 0
|
||||
self.loop = None
|
||||
self.start_independent_lsp_process = start_independent_lsp_process
|
||||
|
||||
async def start(self) -> None:
|
||||
"""
|
||||
@@ -214,6 +225,7 @@ class LanguageServerHandler:
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
env=child_proc_env,
|
||||
cwd=self.process_launch_info.cwd,
|
||||
start_new_session=self.start_independent_lsp_process,
|
||||
)
|
||||
|
||||
# Check if process terminated immediately
|
||||
@@ -234,24 +246,111 @@ class LanguageServerHandler:
|
||||
"""
|
||||
Sends the terminate signal to the language server process and waits for it to exit, with a timeout, killing it if necessary
|
||||
"""
|
||||
for task in self.tasks.values():
|
||||
task.cancel()
|
||||
|
||||
self.tasks = {}
|
||||
|
||||
# First cancel all tasks
|
||||
await self._cancel_pending_tasks()
|
||||
|
||||
process = self.process
|
||||
self.process = None
|
||||
|
||||
if not process:
|
||||
return
|
||||
|
||||
# Clean up the process
|
||||
await self._cleanup_process(process)
|
||||
|
||||
if process:
|
||||
# TODO: Ideally, we should terminate the process here,
|
||||
# However, there's an issue with asyncio terminating processes documented at
|
||||
# https://bugs.python.org/issue35539 and https://bugs.python.org/issue41320
|
||||
# process.terminate()
|
||||
wait_for_end = process.wait()
|
||||
async def _cancel_pending_tasks(self):
|
||||
"""Cancel all pending tasks and wait for them to complete or timeout."""
|
||||
pending_tasks = []
|
||||
for task in self.tasks.values():
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
pending_tasks.append(task)
|
||||
|
||||
if pending_tasks:
|
||||
try:
|
||||
await asyncio.wait_for(wait_for_end, timeout=60)
|
||||
except asyncio.TimeoutError:
|
||||
process.kill()
|
||||
await asyncio.wait_for(asyncio.gather(*pending_tasks, return_exceptions=True), timeout=5.0)
|
||||
except (asyncio.TimeoutError, Exception):
|
||||
pass
|
||||
|
||||
self.tasks = {}
|
||||
|
||||
async def _cleanup_process(self, process):
|
||||
"""Clean up a process: close stdin, terminate/kill process, close stdout/stderr."""
|
||||
# Close stdin first to prevent deadlocks
|
||||
# See: https://bugs.python.org/issue35539
|
||||
self._safely_close_pipe(process.stdin)
|
||||
|
||||
# Terminate/kill the process if it's still running
|
||||
if process.returncode is None:
|
||||
await self._terminate_or_kill_process(process)
|
||||
|
||||
# Close stdout and stderr pipes after process has exited
|
||||
# This is essential to prevent "I/O operation on closed pipe" errors and
|
||||
# "Event loop is closed" errors during garbage collection
|
||||
# See: https://bugs.python.org/issue41320 and https://github.com/python/cpython/issues/88050
|
||||
self._safely_close_pipe(process.stdout)
|
||||
self._safely_close_pipe(process.stderr)
|
||||
|
||||
# Small delay to ensure OS has released file handles
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
def _safely_close_pipe(self, pipe):
|
||||
"""Safely close a pipe, ignoring any exceptions."""
|
||||
if pipe:
|
||||
try:
|
||||
pipe.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _terminate_or_kill_process(self, process):
|
||||
"""Try to terminate the process gracefully, then forcefully if necessary."""
|
||||
# First try to terminate the process tree gracefully
|
||||
self._signal_process_tree(process, terminate=True)
|
||||
|
||||
# Wait for the process to exit (with timeout)
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), timeout=10)
|
||||
except (asyncio.TimeoutError, Exception):
|
||||
# If termination failed, forcefully kill the process tree
|
||||
self._signal_process_tree(process, terminate=False)
|
||||
try:
|
||||
# Give it one more chance to exit
|
||||
await asyncio.wait_for(process.wait(), timeout=2)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _signal_process_tree(self, process, terminate=True):
|
||||
"""Send signal (terminate or kill) to the process and all its children."""
|
||||
signal_method = "terminate" if terminate else "kill"
|
||||
|
||||
# Try to get the parent process
|
||||
parent = None
|
||||
try:
|
||||
parent = psutil.Process(process.pid)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, Exception):
|
||||
pass
|
||||
|
||||
# If we have the parent process and it's running, signal the entire tree
|
||||
if parent and parent.is_running():
|
||||
# Signal children first
|
||||
for child in parent.children(recursive=True):
|
||||
try:
|
||||
getattr(child, signal_method)()
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, Exception):
|
||||
pass
|
||||
|
||||
# Then signal the parent
|
||||
try:
|
||||
getattr(parent, signal_method)()
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, Exception):
|
||||
pass
|
||||
else:
|
||||
# Fall back to direct process signaling
|
||||
try:
|
||||
getattr(process, signal_method)()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""
|
||||
@@ -310,7 +409,7 @@ class LanguageServerHandler:
|
||||
line = await self.process.stderr.readline()
|
||||
if not line:
|
||||
continue
|
||||
self._log("LSP stderr: " + line.decode(ENCODING))
|
||||
self._log("LSP stderr: " + line.decode(ENCODING, errors='replace'))
|
||||
except (BrokenPipeError, ConnectionResetError, StopLoopException):
|
||||
pass
|
||||
|
||||
|
||||
@@ -30,10 +30,13 @@ class Language(str, Enum):
|
||||
PYTHON = "python"
|
||||
RUST = "rust"
|
||||
JAVA = "java"
|
||||
KOTLIN = "kotlin"
|
||||
TYPESCRIPT = "typescript"
|
||||
JAVASCRIPT = "javascript"
|
||||
GO = "go"
|
||||
RUBY = "ruby"
|
||||
DART = "dart"
|
||||
CPP = "cpp"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
@@ -56,8 +59,14 @@ class Language(str, Enum):
|
||||
return FilenameMatcher("*.go")
|
||||
case self.RUBY:
|
||||
return FilenameMatcher("*.rb")
|
||||
case self.CPP:
|
||||
return FilenameMatcher("*.cpp", "*.h", "*.hpp", "*.c", "*.hxx", "*.cc", "*.cxx")
|
||||
case self.KOTLIN:
|
||||
return FilenameMatcher("*.kt", "*.kts")
|
||||
case self.DART:
|
||||
return FilenameMatcher("*.dart")
|
||||
case _:
|
||||
raise ValueError
|
||||
raise ValueError(f"Unhandled language: {self}")
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -67,6 +76,7 @@ class MultilspyConfig:
|
||||
"""
|
||||
code_language: Language
|
||||
trace_lsp_communication: bool = False
|
||||
start_independent_lsp_process: bool = True
|
||||
ignored_paths: list[str] = field(default_factory=list)
|
||||
"""Paths, dirs or glob-like patterns. The matching will follow the same logic as for .gitignore entries"""
|
||||
gitignore_file_content: str | None = None
|
||||
|
||||
@@ -82,7 +82,7 @@ class Location(TypedDict):
|
||||
uri: DocumentUri
|
||||
range: Range
|
||||
absolutePath: str
|
||||
relativePath: str
|
||||
relativePath: Union[str, None]
|
||||
|
||||
class CompletionItemKind(IntEnum):
|
||||
"""The kind of a completion entry."""
|
||||
|
||||
@@ -5,7 +5,7 @@ This file contains various utility functions like I/O operations, handling paths
|
||||
import gzip
|
||||
import logging
|
||||
import os
|
||||
from typing import Tuple
|
||||
from typing import Tuple, Union
|
||||
import requests
|
||||
import shutil
|
||||
import uuid
|
||||
@@ -94,6 +94,16 @@ class PathUtils:
|
||||
"""Check if a pattern contains glob-specific characters."""
|
||||
return any(c in pattern for c in '*?[]!')
|
||||
|
||||
@staticmethod
|
||||
def get_relative_path(path: str, base_path: str) -> Union[str, None]:
|
||||
"""
|
||||
Gets relative path if it's possible (paths should be on the same drive),
|
||||
returns `None` otherwise.
|
||||
"""
|
||||
if PurePath(path).drive == PurePath(base_path).drive:
|
||||
return str(PurePath(os.path.relpath(path, base_path)))
|
||||
return None
|
||||
|
||||
class FileUtils:
|
||||
"""
|
||||
Utility functions for file operations.
|
||||
|
||||
+4
-6
@@ -973,15 +973,13 @@ class InsertAtLineTool(Tool):
|
||||
|
||||
class CheckOnboardingPerformedTool(Tool):
|
||||
"""
|
||||
Checks whether the onboarding was already performed.
|
||||
Checks whether project onboarding was already performed.
|
||||
"""
|
||||
|
||||
def apply(self) -> str:
|
||||
"""
|
||||
Check if onboarding was performed yet.
|
||||
You should always call this tool in the beginning of the conversation,
|
||||
before any question about code or the project is asked.
|
||||
You will call this tool only once per conversation.
|
||||
Checks whether project onboarding was already performed.
|
||||
You should always call this tool before beginning to actually work on a project/after activating a project.
|
||||
"""
|
||||
list_memories_tool = self.agent.get_tool(ListMemoriesTool)
|
||||
memories = json.loads(list_memories_tool.apply())
|
||||
@@ -991,7 +989,7 @@ class CheckOnboardingPerformedTool(Tool):
|
||||
+ "You should perform onboarding by calling the `onboarding` tool before proceeding with the task."
|
||||
)
|
||||
else:
|
||||
return "Onboarding already performed, no need to perform it again."
|
||||
return json.dumps({"result": "Onboarding already performed.", "available_memories": memories})
|
||||
|
||||
|
||||
class OnboardingTool(Tool):
|
||||
|
||||
+14
-6
@@ -23,9 +23,9 @@ class SymbolLocation:
|
||||
Represents the (start) location of a symbol identifier
|
||||
"""
|
||||
|
||||
relative_path: str
|
||||
relative_path: str | None
|
||||
"""
|
||||
the relative path of the file containing the symbol
|
||||
the relative path of the file containing the symbol; if None, the symbol is defined outside of the project's scope
|
||||
"""
|
||||
line: int | None
|
||||
"""
|
||||
@@ -39,13 +39,14 @@ class SymbolLocation:
|
||||
"""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.relative_path = self.relative_path.replace("/", os.path.sep)
|
||||
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 has_position_in_file(self) -> bool:
|
||||
return self.line is not None and self.column is not None
|
||||
return self.relative_path is not None and self.line is not None and self.column is not None
|
||||
|
||||
|
||||
class Symbol(ToStringMixin):
|
||||
@@ -71,7 +72,7 @@ class Symbol(ToStringMixin):
|
||||
return self.s["kind"]
|
||||
|
||||
@property
|
||||
def relative_path(self) -> str:
|
||||
def relative_path(self) -> str | None:
|
||||
return self.s["location"]["relativePath"]
|
||||
|
||||
@property
|
||||
@@ -239,6 +240,8 @@ class SymbolManager:
|
||||
return symbols
|
||||
|
||||
def find_by_location(self, location: SymbolLocation) -> Symbol | None:
|
||||
if location.relative_path is None:
|
||||
return None
|
||||
symbol_dicts, roots = self.lang_server.request_document_symbols(location.relative_path, include_body=False)
|
||||
for symbol_dict in symbol_dicts:
|
||||
symbol = Symbol(symbol_dict)
|
||||
@@ -268,6 +271,7 @@ class SymbolManager:
|
||||
"""
|
||||
if not symbol_location.has_position_in_file():
|
||||
raise ValueError("Symbol location does not contain a valid position in a file")
|
||||
assert symbol_location.relative_path is not None
|
||||
assert symbol_location.line is not None
|
||||
assert symbol_location.column is not None
|
||||
symbol_dicts = self.lang_server.request_referencing_symbols(
|
||||
@@ -301,7 +305,8 @@ class SymbolManager:
|
||||
def _edited_symbol_location(self, location: SymbolLocation) -> Iterator[Symbol]:
|
||||
symbol = self.find_by_location(location)
|
||||
if symbol is None:
|
||||
raise ValueError("Symbol not found")
|
||||
raise ValueError("Symbol not found/has no defined location within a file")
|
||||
assert location.relative_path is not None
|
||||
with self._edited_file(location.relative_path):
|
||||
yield symbol
|
||||
|
||||
@@ -313,6 +318,7 @@ class SymbolManager:
|
||||
:param body: the new body
|
||||
"""
|
||||
with self._edited_symbol_location(location) as symbol:
|
||||
assert location.relative_path is not None
|
||||
self.lang_server.delete_text_between_positions(location.relative_path, symbol.body_start_position, symbol.body_end_position)
|
||||
self.lang_server.insert_text_at_position(
|
||||
location.relative_path, symbol.body_start_position["line"], symbol.body_start_position["character"], body
|
||||
@@ -327,6 +333,7 @@ class SymbolManager:
|
||||
"""
|
||||
with self._edited_symbol_location(location) as symbol:
|
||||
pos = symbol.body_end_position
|
||||
assert location.relative_path is not None
|
||||
self.lang_server.insert_text_at_position(location.relative_path, pos["line"], pos["character"], body)
|
||||
|
||||
def insert_before(self, location: SymbolLocation, body: str) -> None:
|
||||
@@ -338,6 +345,7 @@ class SymbolManager:
|
||||
"""
|
||||
with self._edited_symbol_location(location) as symbol:
|
||||
pos = copy(symbol.body_start_position)
|
||||
assert location.relative_path is not None
|
||||
self.lang_server.insert_text_at_position(location.relative_path, pos["line"], pos["character"], body)
|
||||
|
||||
def insert_at_line(self, relative_path: str, line: int, content: str) -> None:
|
||||
|
||||
@@ -687,6 +687,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
version = "7.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ptyprocess"
|
||||
version = "0.7.0"
|
||||
@@ -951,6 +966,7 @@ dependencies = [
|
||||
{ name = "mcp" },
|
||||
{ name = "overrides" },
|
||||
{ name = "pathspec" },
|
||||
{ name = "psutil" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pyright" },
|
||||
{ name = "python-dotenv" },
|
||||
@@ -1000,6 +1016,7 @@ requires-dist = [
|
||||
{ name = "overrides", specifier = ">=7.7.0,<8" },
|
||||
{ name = "pathspec", specifier = ">=0.12.1" },
|
||||
{ name = "poethepoet", marker = "extra == 'dev'", specifier = ">=0.20.0" },
|
||||
{ name = "psutil", specifier = ">=7.0.0" },
|
||||
{ name = "pydantic", specifier = ">=2.10.6" },
|
||||
{ name = "pyright", specifier = ">=1.1.396,<2" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.2" },
|
||||
|
||||
Reference in New Issue
Block a user