Merge branch 'main' of github.com:oraios/serena

This commit is contained in:
MischaPanch committed 2025-10-27 22:15:46 +01:00
commit c43c9c55f5
11 files changed
+1112 -404

No files matched your search

+2
View File
@@ -5,6 +5,8 @@ Status of the `main` branch. Changes prior to the next official version change w
* Added `RenameSymbolTool` for renaming symbols across the codebase (if LS supports this operation).
* Language support:
* **Add support for Fortran** via fortls language server (requires `pip install fortls`)
* **Add support for Julia** via LanguageServer.jl
* **Add support for Haskell** via Haskell Language Server (HLS) with automatic discovery via ghcup, stack, or system PATH; supports both Stack and Cabal projects
* **Add support for Scala** via Metals language server (requires some [manual setup](docs/scala_setup_guide_for_serena.md))
+1
View File
@@ -99,6 +99,7 @@ With Serena, we provide direct, out-of-the-box support for:
* Scala (requires some [manual setup](docs/scala_setup_guide_for_serena.md); uses Metals LSP)
* 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)
* Fortran (requires installation of fortls: `pip install fortls`)
* Haskell (automatically locates HLS via ghcup, stack, or system PATH; supports Stack and Cabal projects)
* Julia
* AL
+2
View File
@@ -16,6 +16,7 @@ classifiers = [
dependencies = [
"requests>=2.32.3,<3",
"pyright>=1.1.396,<2",
"fortls>=3.2.2",
"overrides>=7.7.0,<8",
"python-dotenv>=1.0.0, <2",
"mcp==1.12.3",
@@ -281,6 +282,7 @@ markers = [
"rego: language server running for Rego",
"markdown: language server running for Markdown",
"julia: Julia language server tests",
"fortran: language server running for Fortran",
"haskell: Haskell language server tests",
]
@@ -0,0 +1,311 @@
"""
Fortran Language Server implementation using fortls.
"""
import logging
import os
import pathlib
import re
import shutil
import threading
from overrides import override
from solidlsp import ls_types
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
class FortranLanguageServer(SolidLanguageServer):
"""Fortran Language Server implementation using fortls."""
@override
def _get_wait_time_for_cross_file_referencing(self) -> float:
return 3.0 # fortls needs time for workspace indexing
@override
def is_ignored_dirname(self, dirname: str) -> bool:
# For Fortran projects, ignore common build directories
return super().is_ignored_dirname(dirname) or dirname in [
"build",
"Build",
"BUILD",
"bin",
"lib",
"mod", # Module files directory
"obj", # Object files directory
".cmake",
"CMakeFiles",
]
def _fix_fortls_selection_range(
self, symbol: ls_types.UnifiedSymbolInformation, file_content: str
) -> ls_types.UnifiedSymbolInformation:
"""
Fix fortls's incorrect selectionRange that points to line start instead of identifier name.
fortls bug: selectionRange.start.character is 0 (line start) but should point to the
function/subroutine/module/program name position. This breaks MCP server features that
rely on the exact identifier position for finding references.
Args:
symbol: The symbol with potentially incorrect selectionRange
file_content: Full file content to parse the line
Returns:
Symbol with corrected selectionRange pointing to the identifier name
"""
if "selectionRange" not in symbol:
return symbol
sel_range = symbol["selectionRange"]
start_line = sel_range["start"]["line"]
start_char = sel_range["start"]["character"]
# Split file content into lines
lines = file_content.split("\n")
if start_line >= len(lines):
return symbol
line = lines[start_line]
# Fortran keywords that define named constructs
# Match patterns:
# Standard keywords: <keyword> <whitespace> <identifier_name>
# " function add_numbers(a, b) result(sum)" -> keyword="function", name="add_numbers"
# "subroutine print_result(value)" -> keyword="subroutine", name="print_result"
# "module math_utils" -> keyword="module", name="math_utils"
# "program test_program" -> keyword="program", name="test_program"
# "interface distance" -> keyword="interface", name="distance"
#
# Type definitions (can have :: syntax):
# "type point" -> keyword="type", name="point"
# "type :: point" -> keyword="type", name="point"
# "type, extends(base) :: derived" -> keyword="type", name="derived"
#
# Submodules (have parent module in parentheses):
# "submodule (parent_mod) child_mod" -> keyword="submodule", name="child_mod"
# Try type pattern first (has complex syntax with optional comma and ::)
type_pattern = r"^\s*type\s*(?:,.*?)?\s*(?:::)?\s*([a-zA-Z_]\w*)"
match = re.match(type_pattern, line, re.IGNORECASE)
if match:
# For type pattern, identifier is in group 1
identifier_name = match.group(1)
identifier_start = match.start(1)
else:
# Try standard keywords pattern
standard_pattern = r"^\s*(function|subroutine|module|program|interface)\s+([a-zA-Z_]\w*)"
match = re.match(standard_pattern, line, re.IGNORECASE)
if not match:
# Try submodule pattern
submodule_pattern = r"^\s*submodule\s*\([^)]+\)\s+([a-zA-Z_]\w*)"
match = re.match(submodule_pattern, line, re.IGNORECASE)
if match:
identifier_name = match.group(1)
identifier_start = match.start(1)
else:
identifier_name = match.group(2)
identifier_start = match.start(2)
if match:
# Create corrected selectionRange
new_sel_range = {
"start": {"line": start_line, "character": identifier_start},
"end": {"line": start_line, "character": identifier_start + len(identifier_name)},
}
# Create modified symbol with corrected selectionRange
corrected_symbol = symbol.copy()
corrected_symbol["selectionRange"] = new_sel_range
self.logger.log(
f"Fixed fortls selectionRange for {identifier_name}: char {start_char} -> {identifier_start}",
logging.DEBUG,
)
return corrected_symbol
# If no match, return symbol unchanged (e.g., for variables, which don't have this pattern)
return symbol
@override
def request_document_symbols(
self, relative_file_path: str, include_body: bool = False
) -> tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]:
"""
Override to fix fortls's incorrect selectionRange bug.
fortls returns selectionRange pointing to line start (character 0) instead of the
identifier name position. This breaks MCP server features that rely on exact positions.
This override:
1. Gets symbols from fortls via parent implementation
2. Parses each symbol's line to find the correct identifier position
3. Fixes selectionRange for all symbols recursively
4. Returns corrected symbols
"""
# Get symbols from fortls (with incorrect selectionRange)
all_symbols, root_symbols = super().request_document_symbols(relative_file_path, include_body)
# Get file content for parsing
with self.open_file(relative_file_path) as file_data:
file_content = file_data.contents
# Fix selectionRange recursively for all symbols
def fix_symbol_and_children(symbol: ls_types.UnifiedSymbolInformation) -> ls_types.UnifiedSymbolInformation:
# Fix this symbol's selectionRange
fixed = self._fix_fortls_selection_range(symbol, file_content)
# Fix children recursively
if fixed.get("children"):
fixed["children"] = [fix_symbol_and_children(child) for child in fixed["children"]]
return fixed
# Apply fix to all symbols
fixed_all_symbols = [fix_symbol_and_children(sym) for sym in all_symbols]
fixed_root_symbols = [fix_symbol_and_children(sym) for sym in root_symbols]
return fixed_all_symbols, fixed_root_symbols
@staticmethod
def _check_fortls_installation():
"""Check if fortls is available."""
fortls_path = shutil.which("fortls")
if fortls_path is None:
raise RuntimeError("fortls is not installed or not in PATH.\nInstall it with: pip install fortls")
return fortls_path
def __init__(
self,
config: LanguageServerConfig,
logger: LanguageServerLogger,
repository_root_path: str,
solidlsp_settings: SolidLSPSettings,
):
# Check fortls installation
fortls_path = self._check_fortls_installation()
# Command to start fortls language server
# fortls uses stdio for LSP communication by default
fortls_cmd = f"{fortls_path}"
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=fortls_cmd, cwd=repository_root_path),
"fortran", # Language ID for LSP
solidlsp_settings,
)
self.server_ready = threading.Event()
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""Initialize params for Fortran 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,
"commitCharactersSupport": True,
"documentationFormat": ["markdown", "plaintext"],
"deprecatedSupport": True,
"preselectSupport": True,
},
},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"formatting": {"dynamicRegistration": True},
"rangeFormatting": {"dynamicRegistration": True},
"codeAction": {"dynamicRegistration": True},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"symbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params
def _start_server(self):
"""Start Fortran Language Server process."""
def window_log_message(msg):
self.logger.log(f"Fortran LSP: window/logMessage: {msg}", logging.INFO)
def do_nothing(params):
return
def register_capability_handler(params):
return
# Register LSP message handlers
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.logger.log("Starting Fortran Language Server (fortls) process", logging.INFO)
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request to Fortran Language Server",
logging.INFO,
)
init_response = self.server.send.initialize(initialize_params)
# Verify server capabilities
capabilities = init_response.get("capabilities", {})
assert "textDocumentSync" in capabilities
if "completionProvider" in capabilities:
self.logger.log("Fortran LSP completion provider available", logging.INFO)
if "definitionProvider" in capabilities:
self.logger.log("Fortran LSP definition provider available", logging.INFO)
if "referencesProvider" in capabilities:
self.logger.log("Fortran LSP references provider available", logging.INFO)
if "documentSymbolProvider" in capabilities:
self.logger.log("Fortran LSP document symbol provider available", logging.INFO)
self.server.notify.initialized({})
self.completions_available.set()
# Fortran Language Server is ready after initialization
self.server_ready.set()
self.logger.log("Fortran Language Server initialization complete", logging.INFO)
+9
View File
@@ -58,6 +58,7 @@ class Language(str, Enum):
REGO = "rego"
SCALA = "scala"
JULIA = "julia"
FORTRAN = "fortran"
HASKELL = "haskell"
# Experimental or deprecated Language Servers
TYPESCRIPT_VTS = "typescript_vts"
@@ -159,6 +160,10 @@ class Language(str, Enum):
return FilenameMatcher("*.scala", "*.sbt")
case self.JULIA:
return FilenameMatcher("*.jl")
case self.FORTRAN:
return FilenameMatcher(
"*.f90", "*.F90", "*.f95", "*.F95", "*.f03", "*.F03", "*.f08", "*.F08", "*.f", "*.F", "*.for", "*.FOR", "*.fpp", "*.FPP"
)
case self.HASKELL:
return FilenameMatcher("*.hs", "*.lhs")
case _:
@@ -294,6 +299,10 @@ class Language(str, Enum):
from solidlsp.language_servers.julia_server import JuliaLanguageServer
return JuliaLanguageServer
case self.FORTRAN:
from solidlsp.language_servers.fortran_language_server import FortranLanguageServer
return FortranLanguageServer
case self.HASKELL:
from solidlsp.language_servers.haskell_language_server import HaskellLanguageServer
@@ -0,0 +1,16 @@
program test_program
use math_utils
implicit none
real :: result
! Test addition
result = add_numbers(5.0, 3.0)
call print_result(result)
! Test multiplication
result = multiply_numbers(4.0, 2.0)
call print_result(result)
print *, "All tests completed"
end program test_program
@@ -0,0 +1,46 @@
module geometry_types
implicit none
! Simple type definition
type Point2D
real :: x, y
end type Point2D
! Type with double colon syntax
type :: Circle
real :: radius
type(Point2D) :: center
end type Circle
! Type with extends (inheritance)
type, extends(Point2D) :: Point3D
real :: z
end type Point3D
! Named interface
interface distance
module procedure distance_2d, distance_3d
end interface distance
contains
function distance_2d(p1, p2) result(dist)
type(Point2D), intent(in) :: p1, p2
real :: dist
dist = sqrt((p2%x - p1%x)**2 + (p2%y - p1%y)**2)
end function distance_2d
function distance_3d(p1, p2) result(dist)
type(Point3D), intent(in) :: p1, p2
real :: dist
dist = sqrt((p2%x - p1%x)**2 + (p2%y - p1%y)**2 + (p2%z - p1%z)**2)
end function distance_3d
function circle_area(c) result(area)
type(Circle), intent(in) :: c
real :: area
real, parameter :: pi = 3.14159265359
area = pi * c%radius**2
end function circle_area
end module geometry_types
@@ -0,0 +1,22 @@
module math_utils
implicit none
contains
function add_numbers(a, b) result(sum)
real, intent(in) :: a, b
real :: sum
sum = a + b
end function add_numbers
function multiply_numbers(x, y) result(product)
real, intent(in) :: x, y
real :: product
product = x * y
end function multiply_numbers
subroutine print_result(value)
real, intent(in) :: value
print *, "Result is:", value
end subroutine print_result
end module math_utils
+1
View File
@@ -0,0 +1 @@
# Fortran language server tests
+274
View File
@@ -0,0 +1,274 @@
"""
Basic tests for Fortran language server integration.
These tests validate some low-level LSP functionality and high-level Serena APIs.
Note: These tests require fortls to be installed: pip install fortls
"""
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_types import SymbolKind
from solidlsp.ls_utils import SymbolUtils
# Mark all tests in this module as fortran tests
pytestmark = pytest.mark.fortran
class TestFortranLanguageServer:
"""Test Fortran language server functionality."""
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
"""Test finding symbols using request_full_symbol_tree."""
symbols = language_server.request_full_symbol_tree()
# Verify program symbol
assert SymbolUtils.symbol_tree_contains_name(symbols, "test_program"), "test_program not found in symbol tree"
# Verify module symbol
assert SymbolUtils.symbol_tree_contains_name(symbols, "math_utils"), "math_utils module not found in symbol tree"
# Verify function symbols
assert SymbolUtils.symbol_tree_contains_name(symbols, "add_numbers"), "add_numbers function not found in symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "multiply_numbers"), "multiply_numbers function not found in symbol tree"
# Verify subroutine symbol
assert SymbolUtils.symbol_tree_contains_name(symbols, "print_result"), "print_result subroutine not found in symbol tree"
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
def test_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test that document symbols can be retrieved from Fortran files."""
# Test main.f90 - should have a program symbol
main_symbols, _ = language_server.request_document_symbols("main.f90")
program_names = [s.get("name") for s in main_symbols]
assert "test_program" in program_names, f"Program 'test_program' not found in main.f90. Found: {program_names}"
# Test modules/math_utils.f90 - should have module and function symbols
module_symbols, _ = language_server.request_document_symbols("modules/math_utils.f90")
all_names = [s.get("name") for s in module_symbols]
assert "math_utils" in all_names, f"Module 'math_utils' not found. Found: {all_names}"
assert "add_numbers" in all_names, f"Function 'add_numbers' not found. Found: {all_names}"
assert "multiply_numbers" in all_names, f"Function 'multiply_numbers' not found. Found: {all_names}"
assert "print_result" in all_names, f"Subroutine 'print_result' not found. Found: {all_names}"
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
def test_find_references_cross_file(self, language_server: SolidLanguageServer) -> None:
"""Test finding references across files using low-level request_references.
This tests the LSP textDocument/references capability.
"""
file_path = "modules/math_utils.f90"
symbols = language_server.request_document_symbols(file_path)
# Find the add_numbers function
add_numbers_symbol = None
for sym in symbols[0]:
if sym.get("name") == "add_numbers":
add_numbers_symbol = sym
break
assert add_numbers_symbol is not None, "Could not find 'add_numbers' function symbol in math_utils.f90"
# Use selectionRange to query for references
# Note: FortranLanguageServer automatically fixes fortls's incorrect selectionRange
sel_start = add_numbers_symbol["selectionRange"]["start"]
# Query from the function name position using corrected selectionRange
refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
# Should find references (usage in main.f90 + definition in math_utils.f90)
assert len(refs) > 0, "Should find references to add_numbers function"
# Verify that main.f90 references the function
main_refs = [ref for ref in refs if "main.f90" in ref.get("relativePath", "")]
assert (
len(main_refs) > 0
), f"Expected to find reference in main.f90, but found references in: {[ref.get('relativePath') for ref in refs]}"
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
def test_find_definition_cross_file(self, language_server: SolidLanguageServer) -> None:
"""Test finding definition across files using request_definition."""
# In main.f90, line 7 (0-indexed: line 6) contains: result = add_numbers(5.0, 3.0)
# We want to find the definition of add_numbers in modules/math_utils.f90
main_file = "main.f90"
# Position on 'add_numbers' usage (approximately column 13)
definition_location_list = language_server.request_definition(main_file, 6, 13)
if not definition_location_list:
pytest.skip("fortls does not support cross-file go-to-definition for this case")
assert len(definition_location_list) >= 1, "Should find at least one definition"
definition_location = definition_location_list[0]
# The definition should be in modules/math_utils.f90
assert "math_utils.f90" in definition_location.get(
"uri", ""
), f"Expected definition to be in math_utils.f90, but found in: {definition_location.get('uri')}"
# Verify the definition is around the correct line (line 4, 0-indexed)
assert (
definition_location["range"]["start"]["line"] == 4
), f"Expected definition at line 4, but found at line {definition_location['range']['start']['line']}"
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
def test_request_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test finding symbols that reference a function - Serena's high-level API.
This tests request_referencing_symbols which returns not just locations but also
the containing symbols that have the references. This is different from
test_find_references_cross_file which only returns locations.
Note: FortranLanguageServer automatically fixes fortls's incorrect selectionRange.
"""
# Get the add_numbers function symbol from math_utils.f90
file_path = "modules/math_utils.f90"
symbols, _ = language_server.request_document_symbols(file_path)
# Find the add_numbers function
add_numbers_symbol = None
for sym in symbols:
if sym.get("name") == "add_numbers":
add_numbers_symbol = sym
break
assert add_numbers_symbol is not None, "Could not find 'add_numbers' function symbol"
# Use selectionRange to query for referencing symbols
# FortranLanguageServer automatically corrects fortls's incorrect selectionRange
sel_start = add_numbers_symbol["selectionRange"]["start"]
referencing_symbols = language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
# Should find referencing symbols (not just locations, but symbols containing the references)
assert len(referencing_symbols) > 0, "Should find referencing symbols when querying from function name position"
# Extract the symbols from ReferenceInSymbol objects
# This is what makes this test different from test_find_references_cross_file:
# we're testing that we get back SYMBOLS (with name, kind, location) not just locations
ref_symbols = [ref.symbol for ref in referencing_symbols]
# Verify we got valid symbol structures with all required fields
for symbol in ref_symbols:
assert "name" in symbol, f"Symbol should have a name: {symbol}"
assert "kind" in symbol, f"Symbol should have a kind: {symbol}"
# Each symbol should have location information
assert "location" in symbol, f"Symbol should have location: {symbol}"
# Note: fortls may not return all cross-file references through request_referencing_symbols
# because it depends on finding containing symbols for each reference. We verify that
# the API works and returns valid symbols with proper structure.
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
def test_request_defining_symbol(self, language_server: SolidLanguageServer) -> None:
"""Test finding the defining symbol - Serena's high-level API.
This is similar to test_find_definition_cross_file but uses the high-level
request_defining_symbol which returns a full symbol with metadata, not just a location.
"""
# In main.f90, line 7 (0-indexed: line 6) contains: result = add_numbers(5.0, 3.0)
# We want to find the definition of add_numbers
main_file = "main.f90"
# Get the position of add_numbers usage in main.f90
# Position on 'add_numbers' (approximately column 13)
defining_symbol = language_server.request_defining_symbol(main_file, 6, 13)
if defining_symbol is None:
pytest.skip("fortls does not support cross-file go-to-definition for this case")
# Should find the add_numbers function with full symbol information
assert defining_symbol.get("name") == "add_numbers", f"Expected to find 'add_numbers' but got '{defining_symbol.get('name')}'"
# Check if we have location information
if "location" not in defining_symbol or "relativePath" not in defining_symbol["location"]:
pytest.skip("fortls found the symbol but doesn't provide complete location information")
# The definition should be in modules/math_utils.f90
defining_path = defining_symbol["location"]["relativePath"]
assert "math_utils.f90" in defining_path, f"Expected definition to be in math_utils.f90, but found in: {defining_path}"
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
def test_request_containing_symbol(self, language_server: SolidLanguageServer) -> None:
"""Test finding the containing symbol for a position in the code."""
# Test finding the containing symbol for a position inside the add_numbers function
file_path = "modules/math_utils.f90"
# Line 8 (0-indexed: line 7) is inside the add_numbers function: "sum = a + b"
containing_symbol = language_server.request_containing_symbol(file_path, 7, 10, include_body=False)
if containing_symbol is None:
pytest.skip("fortls does not support request_containing_symbol or couldn't find the containing symbol")
# Should find the add_numbers function as the containing symbol
assert (
containing_symbol.get("name") == "add_numbers"
), f"Expected containing symbol 'add_numbers', got '{containing_symbol.get('name')}'"
# Verify the symbol kind is Function
assert (
containing_symbol.get("kind") == SymbolKind.Function.value
), f"Expected Function kind ({SymbolKind.Function.value}), got {containing_symbol.get('kind')}"
# Verify location information exists
assert "location" in containing_symbol, "Containing symbol should have location information"
location = containing_symbol["location"]
assert "range" in location, "Location should contain range information"
assert "start" in location["range"] and "end" in location["range"], "Range should have start and end positions"
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
def test_type_and_interface_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test that type definitions and interfaces are properly recognized with corrected selectionRange.
This verifies that the regex pattern correctly handles:
- Simple type definitions (type Name)
- Type with double colon (type :: Name)
- Type with extends (type, extends(Base) :: Derived)
- Named interfaces
fortls returns these as SymbolKind.Class (11) for types and SymbolKind.Interface (5) for interfaces.
"""
file_path = "modules/geometry.f90"
symbols, _ = language_server.request_document_symbols(file_path)
# Find type and interface symbols
type_names = []
interface_names = []
for sym in symbols:
if sym.get("kind") == SymbolKind.Class.value: # Type definitions
type_names.append(sym.get("name"))
elif sym.get("kind") == SymbolKind.Interface.value: # Interfaces
interface_names.append(sym.get("name"))
# Verify type definitions are found
assert "Point2D" in type_names, f"Simple type 'Point2D' not found. Found types: {type_names}"
assert "Circle" in type_names, f"Type with :: syntax 'Circle' not found. Found types: {type_names}"
assert "Point3D" in type_names, f"Type with extends 'Point3D' not found. Found types: {type_names}"
# Verify interface is found
assert "distance" in interface_names, f"Interface 'distance' not found. Found interfaces: {interface_names}"
# Verify selectionRange is corrected for a type symbol
point3d_symbol = None
for sym in symbols:
if sym.get("name") == "Point3D":
point3d_symbol = sym
break
assert point3d_symbol is not None, "Could not find 'Point3D' type symbol"
# Use corrected selectionRange to find references
# This tests that the fix works for types (not just functions)
sel_start = point3d_symbol["selectionRange"]["start"]
# Verify selectionRange points to identifier name, not line start
# Line for "type, extends(Point2D) :: Point3D" has Point3D at position > 0
assert (
sel_start["character"] > 0
), f"selectionRange should point to identifier, not line start. Got character: {sel_start['character']}"
# Test that we can find references using the corrected position
_refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
# refs might be empty if Point3D isn't used elsewhere, but the call should not fail
# The important thing is that it doesn't error due to wrong character position
Generated
+428 -404
View File
File diff suppressed because it is too large. Load diff