Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Status of the `main` branch. Changes prior to the next official version change w

* Language support:

* **Add support for Nim** via [nimlangserver](https://github.com/nim-lang/langserver) (requires `nimble install nimlangserver`)
* **Add support for OCaml** via ocaml-lsp-server with cross-file reference support on OCaml 5.2+ (requires opam; see [setup guide](docs/03-special-guides/ocaml_setup_guide_for_serena.md))
* **Add Phpactor as alternative PHP language server** (specify `php_phpactor` as language; requires PHP 8.1+)
* **Add support for Fortran** via fortls language server (requires `pip install fortls`)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ that implement the language server protocol (LSP).
The underlying language servers are typically open-source projects (like Serena) or at least freely available for use.

With Serena's LSP library, we provide **support for over 30 programming languages**, including
AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, GLSL, Go, Groovy (partial support), Haskell, HLSL, Java, Javascript, Julia, Kotlin, Lua, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Swift, TOML, TypeScript, WGSL, YAML, and Zig.
AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, GLSL, Go, Groovy (partial support), Haskell, HLSL, Java, Javascript, Julia, Kotlin, Lua, Markdown, MATLAB, Nim, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Swift, TOML, TypeScript, WGSL, YAML, and Zig.

> [!IMPORTANT]
> Some language servers require additional dependencies to be installed; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details.
Expand Down
2 changes: 2 additions & 0 deletions docs/01-about/020_programming-languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Some languages require additional installations or setup steps, as noted.
* **Lua**
* **Markdown**
(must explicitly enable language `markdown`, primarily useful for documentation-heavy projects)
* **Nim**
(requires installation of [nimlangserver](https://github.com/nim-lang/langserver): `nimble install nimlangserver`)
* **Nix**
(requires nixd installation)
* **OCaml**
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ markers = [
"julia: Julia language server tests",
"fortran: language server running for Fortran",
"haskell: Haskell language server tests",
"nim: language server running for Nim",
"yaml: language server running for YAML",
"powershell: language server running for PowerShell",
"pascal: language server running for Pascal (Free Pascal/Lazarus)",
Expand Down
152 changes: 152 additions & 0 deletions src/solidlsp/language_servers/nim_language_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""
Provides Nim specific instantiation of the LanguageServer class using nimlangserver
(https://github.com/nim-lang/langserver).
"""

import logging
import os
import pathlib
import platform
import shutil

from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings

log = logging.getLogger(__name__)


class NimLanguageServer(SolidLanguageServer):
"""
Provides Nim specific instantiation of the LanguageServer class using nimlangserver.
"""

@staticmethod
def _check_nimlangserver_installed() -> bool:
"""Check if nimlangserver is installed in the system."""
return shutil.which("nimlangserver") is not None

@staticmethod
def _setup_runtime_dependency() -> str:
"""
Verify that nimlangserver is available and return its path.
Raises RuntimeError with helpful message if missing.
"""
if platform.system() == "Windows":
raise RuntimeError(
"Nim language server support on Windows is experimental. "
"Please install nimlangserver via nimble and add it to your PATH."
)

nimlangserver_path = shutil.which("nimlangserver")
if not nimlangserver_path:
raise RuntimeError(
"nimlangserver (Nim Language Server) is not installed.\n"
"Please install it using one of the following methods:\n"
" - Using nimble: nimble install nimlangserver\n"
" - From the GitHub releases: https://github.com/nim-lang/langserver/releases\n"
"After installation, make sure 'nimlangserver' is in your PATH.\n"
"Nim itself can be installed from https://nim-lang.org/install.html"
)

return nimlangserver_path

def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
nimlangserver_path = self._setup_runtime_dependency()

super().__init__(
config,
repository_root_path,
ProcessLaunchInfo(cmd=nimlangserver_path, cwd=repository_root_path),
"nim",
solidlsp_settings,
)
self.request_id = 0

@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Nim Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"completion": {
"dynamicRegistration": True,
"completionItem": {
"snippetSupport": True,
"commitCharactersSupport": True,
"documentationFormat": ["markdown", "plaintext"],
"deprecatedSupport": True,
"preselectSupport": True,
},
},
"hover": {
"dynamicRegistration": True,
"contentFormat": ["markdown", "plaintext"],
},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"configuration": True,
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
"initializationOptions": {},
}
return initialize_params # type: ignore[return-value]

def _start_server(self) -> None:
"""Start nimlangserver process."""

def register_capability_handler(params: dict) -> None:
return

def window_log_message(msg: dict) -> None:
log.info(f"LSP: window/logMessage: {msg}")

def do_nothing(params: dict) -> None:
return

self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)

log.info("Starting nimlangserver process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)

log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)

# Verify server capabilities
assert "textDocumentSync" in init_response["capabilities"]
assert "definitionProvider" in init_response["capabilities"]
assert "documentSymbolProvider" in init_response["capabilities"]
assert "referencesProvider" in init_response["capabilities"]

self.server.notify.initialized({})

# nimlangserver is ready after initialization
7 changes: 7 additions & 0 deletions src/solidlsp/ls_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class Language(str, Enum):
JULIA = "julia"
FORTRAN = "fortran"
HASKELL = "haskell"
NIM = "nim"
GROOVY = "groovy"
VUE = "vue"
POWERSHELL = "powershell"
Expand Down Expand Up @@ -244,6 +245,8 @@ def get_source_fn_matcher(self) -> FilenameMatcher:
)
case self.HASKELL:
return FilenameMatcher("*.hs", "*.lhs")
case self.NIM:
return FilenameMatcher("*.nim", "*.nims")
case self.VUE:
path_patterns = ["*.vue"]
for prefix in ["c", "m", ""]:
Expand Down Expand Up @@ -444,6 +447,10 @@ def get_ls_class(self) -> type["SolidLanguageServer"]:
from solidlsp.language_servers.haskell_language_server import HaskellLanguageServer

return HaskellLanguageServer
case self.NIM:
from solidlsp.language_servers.nim_language_server import NimLanguageServer

return NimLanguageServer
case self.FSHARP:
from solidlsp.language_servers.fsharp_language_server import FSharpLanguageServer

Expand Down
50 changes: 50 additions & 0 deletions test/resources/repos/nim/test_repo/main.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
## main.nim: Entry point demonstrating usage of calculator and utils modules

import src/calculator
import src/utils

proc printBanner() =
## Print a simple banner
echo "=== Nim Test Project ==="

proc testCalculator() =
## Test basic calculator operations
let sum = add(5, 3)
echo "5 + 3 = ", sum

let diff = subtract(10, 4)
echo "10 - 4 = ", diff

let prod = multiply(6, 7)
echo "6 * 7 = ", prod

let quot = divide(15.0, 4.0)
echo "15 / 4 = ", quot

let fact = factorial(5)
echo "5! = ", fact

let pw = power(2, 8)
echo "2^8 = ", pw

proc testUtils() =
## Test string utility functions
let trimmed = trim(" hello world ")
echo "Trimmed: '", trimmed, "'"

let hasPrefix = startsWith("hello world", "hello")
echo "Starts with 'hello': ", hasPrefix

let hasSuffix = endsWith("hello world", "world")
echo "Ends with 'world': ", hasSuffix

let parts = split("one,two,three", ",")
echo "Split parts: ", parts

let joined = join(parts, " | ")
echo "Joined: ", joined

when isMainModule:
printBanner()
testCalculator()
testUtils()
38 changes: 38 additions & 0 deletions test/resources/repos/nim/test_repo/src/calculator.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## calculator.nim: A simple calculator module for testing LSP features

proc add*(a, b: int): int =
## Add two integers
a + b

proc subtract*(a, b: int): int =
## Subtract b from a
a - b

proc multiply*(a, b: int): int =
## Multiply two integers
a * b

proc divide*(a, b: float): float =
## Divide a by b
if b == 0.0:
raise newException(ValueError, "Division by zero")
a / b

proc factorial*(n: int): int =
## Compute factorial of n
if n < 0:
raise newException(ValueError, "Factorial is not defined for negative numbers")
elif n == 0 or n == 1:
1
else:
var result = 1
for i in 2..n:
result = result * i
result

proc power*(base, exponent: int): int =
## Compute base raised to exponent
var result = 1
for _ in 1..exponent:
result = result * base
result
54 changes: 54 additions & 0 deletions test/resources/repos/nim/test_repo/src/utils.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
## utils.nim: String and sequence utility functions for testing LSP features

proc trim*(s: string): string =
## Remove leading and trailing whitespace
var start = 0
var stop = s.len - 1
while start <= stop and s[start] == ' ':
inc start
while stop >= start and s[stop] == ' ':
dec stop
s[start..stop]

proc startsWith*(s, prefix: string): bool =
## Check if string s starts with prefix
if prefix.len > s.len:
return false
for i in 0..<prefix.len:
if s[i] != prefix[i]:
return false
true

proc endsWith*(s, suffix: string): bool =
## Check if string s ends with suffix
if suffix.len > s.len:
return false
let offset = s.len - suffix.len
for i in 0..<suffix.len:
if s[offset + i] != suffix[i]:
return false
true

proc split*(s, sep: string): seq[string] =
## Split string s by separator sep
var parts: seq[string] = @[]
var start = 0
var i = 0
while i <= s.len - sep.len:
if s[i..i+sep.len-1] == sep:
parts.add(s[start..i-1])
start = i + sep.len
i = start
else:
inc i
parts.add(s[start..s.len-1])
parts

proc join*(parts: seq[string], sep: string): string =
## Join sequence of strings with separator
var result = ""
for i, part in parts:
if i > 0:
result = result & sep
result = result & part
result
12 changes: 12 additions & 0 deletions test/resources/repos/nim/test_repo/test_repo.nimble
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Package

version = "0.1.0"
author = "Test"
description = "A minimal Nim test project for LSP testing"
license = "MIT"
srcDir = "."
bin = @["main"]

# Dependencies

requires "nim >= 1.6.0"
Empty file added test/solidlsp/nim/__init__.py
Empty file.
Loading
Loading