mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 04:15:11 +00:00
feat: Elm language support (#667)
Also added Node.js setup to GitHub Actions workflow --------- Co-authored-by: Jordan Munch O'Hare <git@jordan.munchohare.com>
This commit is contained in:
1 parent
21af8bf451
commit
675a421287
11 files changed
+388
No files matched your search
@@ -28,6 +28,10 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ">=1.17.0"
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
- name: Ensure cached directory exist before calling cache-related actions
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -230,6 +234,9 @@ jobs:
|
||||
echo "PERL_MB_OPT=--install_base \"$HOME/perl5\"" >> $GITHUB_ENV
|
||||
echo "PERL_MM_OPT=INSTALL_BASE=$HOME/perl5" >> $GITHUB_ENV
|
||||
echo "$HOME/perl5/bin" >> $GITHUB_PATH
|
||||
- name: Install Elm
|
||||
shell: bash
|
||||
run: npm install -g elm@0.19.1-6
|
||||
- name: Install Nix
|
||||
if: runner.os != 'Windows' # Nix doesn't support Windows natively
|
||||
uses: cachix/install-nix-action@v30
|
||||
|
||||
@@ -234,3 +234,7 @@ tmp/
|
||||
# Swift
|
||||
/test/resources/repos/swift/test_repo/.build
|
||||
/test/resources/repos/swift/test_repo/.swiftpm
|
||||
|
||||
# Elm
|
||||
/test/resources/repos/elm/test_repo/.elm/
|
||||
/test/resources/repos/elm/test_repo/elm-stuff/
|
||||
@@ -2,6 +2,7 @@
|
||||
Status of the `main` branch. Changes prior to the next official version change will appear here.
|
||||
|
||||
* Language support:
|
||||
* **Add support for Elm** via @elm-tooling/elm-language-server (automatically downloads if not installed; requires Elm compiler)
|
||||
* **Add support for Perl** via Perl::LanguageServer with LSP integration for .pl, .pm, and .t files
|
||||
* **Add support for AL (Application Language)** for Microsoft Dynamics 365 Business Central development. Requires VS Code AL extension (ms-dynamics-smb.al).
|
||||
* **Add support for R** via the R languageserver package with LSP integration, performance optimizations, and fallback symbol extraction
|
||||
|
||||
@@ -95,6 +95,7 @@ With Serena, we provide direct, out-of-the-box support for:
|
||||
* Lua (automatically downloads lua-language-server if not installed)
|
||||
* Nix (requires nixd installation)
|
||||
* Elixir (requires installation of NextLS and Elixir; **Windows not supported**)
|
||||
* Elm (automatically downloads elm-language-server if not installed; requires Elm compiler)
|
||||
* Erlang (requires installation of beam and [erlang_ls](https://github.com/erlang-ls/erlang_ls), experimental, might be slow or hang)
|
||||
* Perl (requires installation of Perl::LanguageServer)
|
||||
* AL
|
||||
|
||||
@@ -262,6 +262,7 @@ markers = [
|
||||
"perl: language server running for Perl",
|
||||
"csharp: language server running for C#",
|
||||
"elixir: language server running for Elixir",
|
||||
"elm: language server running for Elm",
|
||||
"terraform: language server running for Terraform",
|
||||
"swift: language server running for Swift",
|
||||
"bash: language server running for Bash",
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
"""
|
||||
Provides Elm specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Elm.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import threading
|
||||
|
||||
from overrides import override
|
||||
from sensai.util.logging import LogTime
|
||||
|
||||
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 solidlsp.settings import SolidLSPSettings
|
||||
|
||||
from .common import RuntimeDependency, RuntimeDependencyCollection
|
||||
|
||||
|
||||
class ElmLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Provides Elm specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Elm.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
|
||||
):
|
||||
"""
|
||||
Creates an ElmLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
elm_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=elm_lsp_executable_path, cwd=repository_root_path),
|
||||
"elm",
|
||||
solidlsp_settings,
|
||||
)
|
||||
self.server_ready = threading.Event()
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in [
|
||||
"elm-stuff",
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _setup_runtime_dependencies(
|
||||
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
|
||||
) -> list[str]:
|
||||
"""
|
||||
Setup runtime dependencies for Elm Language Server and return the command to start the server.
|
||||
"""
|
||||
# Check if elm-language-server is already installed globally
|
||||
system_elm_ls = shutil.which("elm-language-server")
|
||||
if system_elm_ls:
|
||||
logger.log(f"Found system-installed elm-language-server at {system_elm_ls}", logging.INFO)
|
||||
return [system_elm_ls, "--stdio"]
|
||||
|
||||
# Verify 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."
|
||||
is_npm_installed = shutil.which("npm") is not None
|
||||
assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
|
||||
|
||||
deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="elm-language-server",
|
||||
description="@elm-tooling/elm-language-server package",
|
||||
command=["npm", "install", "--prefix", "./", "@elm-tooling/elm-language-server@2.8.0"],
|
||||
platform_id="any",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# Install elm-language-server if not already installed
|
||||
elm_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "elm-lsp")
|
||||
elm_ls_executable_path = os.path.join(elm_ls_dir, "node_modules", ".bin", "elm-language-server")
|
||||
if not os.path.exists(elm_ls_executable_path):
|
||||
logger.log(f"Elm Language Server executable not found at {elm_ls_executable_path}. Installing...", logging.INFO)
|
||||
with LogTime("Installation of Elm language server dependencies", logger=logger.logger):
|
||||
deps.install(logger, elm_ls_dir)
|
||||
|
||||
if not os.path.exists(elm_ls_executable_path):
|
||||
raise FileNotFoundError(
|
||||
f"elm-language-server executable not found at {elm_ls_executable_path}, something went wrong with the installation."
|
||||
)
|
||||
return [elm_ls_executable_path, "--stdio"]
|
||||
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Elm Language Server.
|
||||
"""
|
||||
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"]},
|
||||
"codeAction": {"dynamicRegistration": True},
|
||||
"rename": {"dynamicRegistration": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
"didChangeConfiguration": {"dynamicRegistration": True},
|
||||
"symbol": {"dynamicRegistration": True},
|
||||
},
|
||||
},
|
||||
"initializationOptions": {
|
||||
"elmPath": "elm",
|
||||
"elmFormatPath": "elm-format",
|
||||
"elmTestPath": "elm-test",
|
||||
"skipInstallPackageConfirmation": True,
|
||||
"onlyUpdateDiagnosticsOnSave": False,
|
||||
},
|
||||
"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 Elm Language Server, waits for the server to be ready and yields the LanguageServer instance.
|
||||
"""
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
def window_log_message(msg):
|
||||
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
|
||||
|
||||
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 Elm 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)
|
||||
|
||||
# Elm-specific capability checks
|
||||
assert "textDocumentSync" in init_response["capabilities"]
|
||||
assert "completionProvider" in init_response["capabilities"]
|
||||
assert "definitionProvider" in init_response["capabilities"]
|
||||
assert "referencesProvider" in init_response["capabilities"]
|
||||
assert "documentSymbolProvider" in init_response["capabilities"]
|
||||
|
||||
self.server.notify.initialized({})
|
||||
self.logger.log("Elm server initialized successfully, waiting for workspace scan...", logging.INFO)
|
||||
|
||||
self.server_ready.set()
|
||||
self.completions_available.set()
|
||||
self.logger.log("Elm server ready", logging.INFO)
|
||||
|
||||
@override
|
||||
def _get_wait_time_for_cross_file_referencing(self) -> float:
|
||||
return 1.0
|
||||
@@ -46,6 +46,7 @@ class Language(str, Enum):
|
||||
PERL = "perl"
|
||||
CLOJURE = "clojure"
|
||||
ELIXIR = "elixir"
|
||||
ELM = "elm"
|
||||
TERRAFORM = "terraform"
|
||||
SWIFT = "swift"
|
||||
BASH = "bash"
|
||||
@@ -128,6 +129,8 @@ class Language(str, Enum):
|
||||
return FilenameMatcher("*.clj", "*.cljs", "*.cljc", "*.edn") # codespell:ignore edn
|
||||
case self.ELIXIR:
|
||||
return FilenameMatcher("*.ex", "*.exs")
|
||||
case self.ELM:
|
||||
return FilenameMatcher("*.elm")
|
||||
case self.TERRAFORM:
|
||||
return FilenameMatcher("*.tf", "*.tfvars", "*.tfstate")
|
||||
case self.SWIFT:
|
||||
@@ -223,6 +226,10 @@ class Language(str, Enum):
|
||||
from solidlsp.language_servers.elixir_tools.elixir_tools import ElixirTools
|
||||
|
||||
return ElixirTools
|
||||
case self.ELM:
|
||||
from solidlsp.language_servers.elm_language_server import ElmLanguageServer
|
||||
|
||||
return ElmLanguageServer
|
||||
case self.TERRAFORM:
|
||||
from solidlsp.language_servers.terraform_ls import TerraformLS
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
module Main exposing (main, greet, calculateSum)
|
||||
|
||||
{-| Main module for testing Elm language server functionality.
|
||||
|
||||
This module contains basic functions to test:
|
||||
|
||||
- Symbol discovery
|
||||
- Reference finding
|
||||
- Cross-file imports
|
||||
|
||||
-}
|
||||
|
||||
import Browser
|
||||
import Html exposing (Html, div, h1, p, text)
|
||||
import Utils exposing (formatMessage, addNumbers)
|
||||
|
||||
|
||||
{-| The main entry point for the application
|
||||
-}
|
||||
main : Program () Model Msg
|
||||
main =
|
||||
Browser.sandbox
|
||||
{ init = init
|
||||
, view = view
|
||||
, update = update
|
||||
}
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ message : String
|
||||
, count : Int
|
||||
}
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
{ message = greet "World"
|
||||
, count = calculateSum 5 10
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= NoOp
|
||||
|
||||
|
||||
update : Msg -> Model -> Model
|
||||
update msg model =
|
||||
case msg of
|
||||
NoOp ->
|
||||
model
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
div []
|
||||
[ h1 [] [ text (formatMessage model.message) ]
|
||||
, p [] [ text ("Count: " ++ String.fromInt model.count) ]
|
||||
]
|
||||
|
||||
|
||||
{-| Greet someone by name
|
||||
-}
|
||||
greet : String -> String
|
||||
greet name =
|
||||
"Hello, " ++ name ++ "!"
|
||||
|
||||
|
||||
{-| Calculate the sum of two numbers
|
||||
-}
|
||||
calculateSum : Int -> Int -> Int
|
||||
calculateSum a b =
|
||||
addNumbers a b
|
||||
@@ -0,0 +1,28 @@
|
||||
module Utils exposing (formatMessage, addNumbers, multiplyNumbers)
|
||||
|
||||
{-| Utility functions for the Elm test application.
|
||||
|
||||
This module provides helper functions used by other modules.
|
||||
|
||||
-}
|
||||
|
||||
|
||||
{-| Format a message by adding brackets around it
|
||||
-}
|
||||
formatMessage : String -> String
|
||||
formatMessage msg =
|
||||
"[ " ++ msg ++ " ]"
|
||||
|
||||
|
||||
{-| Add two numbers together
|
||||
-}
|
||||
addNumbers : Int -> Int -> Int
|
||||
addNumbers x y =
|
||||
x + y
|
||||
|
||||
|
||||
{-| Multiply two numbers
|
||||
-}
|
||||
multiplyNumbers : Int -> Int -> Int
|
||||
multiplyNumbers x y =
|
||||
x * y
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"type": "application",
|
||||
"source-directories": [
|
||||
"."
|
||||
],
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"elm/browser": "1.0.2",
|
||||
"elm/core": "1.0.5",
|
||||
"elm/html": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/json": "1.1.3",
|
||||
"elm/time": "1.0.0",
|
||||
"elm/url": "1.0.0",
|
||||
"elm/virtual-dom": "1.0.3"
|
||||
}
|
||||
},
|
||||
"test-dependencies": {
|
||||
"direct": {},
|
||||
"indirect": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_utils import SymbolUtils
|
||||
|
||||
|
||||
@pytest.mark.elm
|
||||
class TestElmLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.ELM], indirect=True)
|
||||
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "greet"), "greet function not found in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "calculateSum"), "calculateSum function not found in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "formatMessage"), "formatMessage function not found in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "addNumbers"), "addNumbers function not found in symbol tree"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.ELM], indirect=True)
|
||||
def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
|
||||
file_path = os.path.join("Main.elm")
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
greet_symbol = None
|
||||
for sym in symbols[0]:
|
||||
if sym.get("name") == "greet":
|
||||
greet_symbol = sym
|
||||
break
|
||||
assert greet_symbol is not None, "Could not find 'greet' symbol in Main.elm"
|
||||
sel_start = greet_symbol["selectionRange"]["start"]
|
||||
refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
|
||||
assert any("Main.elm" in ref.get("relativePath", "") for ref in refs), "Main.elm should reference greet function"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.ELM], indirect=True)
|
||||
def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None:
|
||||
# Test formatMessage function which is defined in Utils.elm and used in Main.elm
|
||||
utils_path = os.path.join("Utils.elm")
|
||||
symbols = language_server.request_document_symbols(utils_path)
|
||||
formatMessage_symbol = None
|
||||
for sym in symbols[0]:
|
||||
if sym.get("name") == "formatMessage":
|
||||
formatMessage_symbol = sym
|
||||
break
|
||||
assert formatMessage_symbol is not None, "Could not find 'formatMessage' symbol in Utils.elm"
|
||||
|
||||
# Get references from the definition in Utils.elm
|
||||
sel_start = formatMessage_symbol["selectionRange"]["start"]
|
||||
refs = language_server.request_references(utils_path, sel_start["line"], sel_start["character"])
|
||||
|
||||
# Verify that we found references
|
||||
assert refs, "Expected to find references for formatMessage"
|
||||
|
||||
# Verify that at least one reference is in Main.elm (where formatMessage is used)
|
||||
assert any("Main.elm" in ref.get("relativePath", "") for ref in refs), "Expected to find usage of formatMessage in Main.elm"
|
||||
Reference in new issue
Block a user