Skip to content

Feat/dart #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 3, 2025
4 changes: 4 additions & 0 deletions src/multilspy/language_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_roo
from multilspy.language_servers.solargraph.solargraph import Solargraph

return Solargraph(config, logger, repository_root_path)
elif config.code_language == Language.DART:
from multilspy.language_servers.dart_language_server.dart_language_server import DartLanguageServer

return DartLanguageServer(config, logger, repository_root_path)
else:
logger.log(f"Language {config.code_language} is not supported", logging.ERROR)
raise MultilspyException(f"Language {config.code_language} is not supported")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from contextlib import asynccontextmanager
import logging
import os
import pathlib
import shutil
from typing import AsyncIterator
from multilspy.language_server import LanguageServer
from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo
import json


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()
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path),
"dart",
)

def setup_runtime_dependencies(self):
dart_executable_path = shutil.which("dart")
assert dart_executable_path, "Dart executable not found in PATH"

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()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we also want to add a workspace configuration file https://github.com/dart-lang/sdk/blob/main/pkg/analysis_server/tool/lsp_spec/README.md#client-workspace-configuration? Could be useful, but can leave out for now if it is too much work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be honest i do not know what that configuration is for, so i wouldnt be sure what to add exactly

would u mind sending me some docs or adding it yourself if possible?

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"_description": "This file contains the initialization parameters for the Dart Language Server.",
"processId": "$processId",
"rootPath": "$rootPath",
"rootUri": "$rootUri",
"capabilities": {},
"trace": "verbose",
"workspaceFolders": [
{
"uri": "$uri",
"name": "$name"
}
]
}
1 change: 1 addition & 0 deletions src/multilspy/multilspy_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Language(str, Enum):
JAVASCRIPT = "javascript"
GO = "go"
RUBY = "ruby"
DART = "dart"

def __str__(self) -> str:
return self.value
Expand Down
76 changes: 76 additions & 0 deletions tests/multilspy/test_multilspy_dart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""
This file contains tests for running the Dart Language Server.
"""

import pytest
from multilspy import LanguageServer
from multilspy.multilspy_config import Language
from tests.test_utils import create_test_context
from pathlib import PurePath

pytest_plugins = ("pytest_asyncio",)


@pytest.mark.asyncio
async def test_multilspy_dart():
"""
Test the working of multilspy with a Dart repository.
"""
code_language = Language.DART
params = {
"code_language": code_language,
"repo_url": "https://github.com/simonoppowa/OpenNutriTracker/",
"repo_commit": "2df39185bdd822dec6a0e521f4c14e3eab6b0805",
}
with create_test_context(params) as context:
lsp = LanguageServer.create(
context.config, context.logger, context.source_directory
)

async with lsp.start_server():
result = await lsp.request_document_symbols(
str(
PurePath("lib/core/presentation/widgets/copy_or_delete_dialog.dart")
)
)

assert isinstance(result, tuple)
assert len(result) == 2

symbols = result[0]
for symbol in symbols:
del symbol["deprecated"]
del symbol["kind"]
del symbol["location"]["uri"]

assert symbols == [
{
"location": {
"range": {
"end": {"character": 24, "line": 3},
"start": {"character": 6, "line": 3},
},
},
"name": "CopyOrDeleteDialog",
},
{
"containerName": "CopyOrDeleteDialog",
"location": {
"range": {
"end": {"character": 26, "line": 4},
"start": {"character": 8, "line": 4},
},
},
"name": "CopyOrDeleteDialog",
},
{
"containerName": "CopyOrDeleteDialog",
"location": {
"range": {
"end": {"character": 14, "line": 7},
"start": {"character": 9, "line": 7},
},
},
"name": "build",
},
]
63 changes: 63 additions & 0 deletions tests/multilspy/test_sync_multilspy_dart.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good to start with now, but can we maybe add tests for any other commonly used lsp features as well?

Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
This file contains tests for running the Python Language Server: jedi-language-server
"""

from multilspy import SyncLanguageServer
from multilspy.multilspy_config import Language
from tests.test_utils import create_test_context
from pathlib import PurePath


def test_sync_multilspy_dart() -> None:
"""
Test the working of multilspy with python repository - black
"""
code_language = Language.DART
params = {
"code_language": code_language,
"repo_url": "https://github.com/simonoppowa/OpenNutriTracker/",
"repo_commit": "2df39185bdd822dec6a0e521f4c14e3eab6b0805",
}
with create_test_context(params) as context:
lsp = SyncLanguageServer.create(
context.config, context.logger, context.source_directory
)

# All the communication with the language server must be performed inside the context manager
# The server process is started when the context manager is entered and is terminated when the context manager is exited.
with lsp.start_server():
result = lsp.request_references(
file_path=str(
PurePath("lib/features/add_meal/presentation/add_meal_screen.dart")
),
line=19,
column=5,
)

for item in result:
del item["uri"]
del item["absolutePath"]

assert result == [
{
"range": {
"end": {"character": 21, "line": 20},
"start": {"character": 8, "line": 20},
},
"relativePath": "lib/features/add_meal/presentation/add_meal_screen.dart",
},
{
"range": {
"end": {"character": 21, "line": 23},
"start": {"character": 8, "line": 23},
},
"relativePath": "lib/features/add_meal/presentation/add_meal_screen.dart",
},
{
"range": {
"end": {"character": 53, "line": 26},
"start": {"character": 40, "line": 26},
},
"relativePath": "lib/features/add_meal/presentation/add_meal_screen.dart",
},
]
Loading