From 993d6606f09a3eaa5d4ebdcc65d2f26424da27f0 Mon Sep 17 00:00:00 2001 From: Jonathan Rudenberg Date: Fri, 8 Aug 2025 12:15:33 -0400 Subject: [PATCH 1/3] Fix Dart LSP hover marshalling issue and add integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Dart Language Server returns hover contents as plain strings in some cases, which was causing JSON unmarshalling errors. According to the LSP specification, the hover contents field can be: - MarkupContent (object with kind and value) - MarkedString (plain string) - MarkedString[] (array of strings or language/value objects) Changes: - Fixed code generation to use Or_Hover_contents union type instead of MarkupContent - Updated hover.go to handle all possible content types from the LSP spec - Added comprehensive Dart integration tests with 13 test cases - Added Dart to GitHub Actions CI workflow - Updated .gitignore for Dart artifacts This ensures the MCP language server correctly handles all valid hover response formats from any LSP server, not just those that return MarkupContent objects. Fixes the issue where Dart hover functionality would fail with: "json: cannot unmarshal string into Go struct field Hover.contents of type protocol.MarkupContent" 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .github/workflows/go.yml | 23 +- .gitignore | 5 + cmd/generate/tables.go | 2 +- .../snapshots/dart/hover/class-method.snap | 7 + .../snapshots/dart/hover/constant.snap | 9 + .../snapshots/dart/hover/enum.snap | 7 + .../snapshots/dart/hover/function.snap | 7 + .../snapshots/dart/hover/global-variable.snap | 9 + .../snapshots/dart/hover/helper-class.snap | 7 + .../snapshots/dart/hover/interface.snap | 7 + .../snapshots/dart/hover/main-function.snap | 4 + .../snapshots/dart/hover/no-hover-info.snap | 2 + .../snapshots/dart/hover/override-method.snap | 4 + .../snapshots/dart/hover/shared-class.snap | 8 + .../snapshots/dart/hover/type-alias.snap | 7 + .../snapshots/dart/hover/variable.snap | 4 + .../tests/dart/hover/hover_test.go | 179 ++++++++ .../tests/dart/internal/helpers.go | 57 +++ integrationtests/workspaces/dart/helper.dart | 30 ++ integrationtests/workspaces/dart/main.dart | 13 + integrationtests/workspaces/dart/pubspec.lock | 389 ++++++++++++++++++ integrationtests/workspaces/dart/pubspec.yaml | 9 + integrationtests/workspaces/dart/types.dart | 37 ++ internal/protocol/tsprotocol.go | 2 +- internal/tools/hover.go | 39 +- 25 files changed, 861 insertions(+), 6 deletions(-) create mode 100644 integrationtests/snapshots/dart/hover/class-method.snap create mode 100644 integrationtests/snapshots/dart/hover/constant.snap create mode 100644 integrationtests/snapshots/dart/hover/enum.snap create mode 100644 integrationtests/snapshots/dart/hover/function.snap create mode 100644 integrationtests/snapshots/dart/hover/global-variable.snap create mode 100644 integrationtests/snapshots/dart/hover/helper-class.snap create mode 100644 integrationtests/snapshots/dart/hover/interface.snap create mode 100644 integrationtests/snapshots/dart/hover/main-function.snap create mode 100644 integrationtests/snapshots/dart/hover/no-hover-info.snap create mode 100644 integrationtests/snapshots/dart/hover/override-method.snap create mode 100644 integrationtests/snapshots/dart/hover/shared-class.snap create mode 100644 integrationtests/snapshots/dart/hover/type-alias.snap create mode 100644 integrationtests/snapshots/dart/hover/variable.snap create mode 100644 integrationtests/tests/dart/hover/hover_test.go create mode 100644 integrationtests/tests/dart/internal/helpers.go create mode 100644 integrationtests/workspaces/dart/helper.dart create mode 100644 integrationtests/workspaces/dart/main.dart create mode 100644 integrationtests/workspaces/dart/pubspec.lock create mode 100644 integrationtests/workspaces/dart/pubspec.yaml create mode 100644 integrationtests/workspaces/dart/types.dart diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c3ca00b..b9157fc 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -55,6 +55,27 @@ jobs: - name: Run Go integration tests run: go test ./integrationtests/tests/go/... + dart-integration-tests: + name: Dart Integration Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.24" + check-latest: true + cache: true + + - name: Set up Dart SDK + uses: dart-lang/setup-dart@v1 + with: + sdk: stable + + - name: Run Dart integration tests + run: go test ./integrationtests/tests/dart/... + python-integration-tests: name: Python Integration Tests runs-on: ubuntu-latest @@ -155,7 +176,7 @@ jobs: sudo ln -s /usr/bin/clangd-16 /usr/bin/clangd clangd-16 --version clangd --version - + - name: Create compile commands run: | cd integrationtests/workspaces/clangd diff --git a/.gitignore b/.gitignore index 92d4648..0faf959 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,11 @@ integrationtests/workspaces/clangd/clean_program integrationtests/workspaces/clangd/program integrationtests/workspaces/clangd/.cache +# Dart +.dart_tool/ +*.dart.js +*.dart.js.map + # Temporary files *~ diff --git a/cmd/generate/tables.go b/cmd/generate/tables.go index 46a68b3..029d2a8 100644 --- a/cmd/generate/tables.go +++ b/cmd/generate/tables.go @@ -67,7 +67,7 @@ var renameProp = map[prop]string{ {"ExecuteCommandParams", "arguments"}: "[]json.RawMessage", {"FoldingRange", "kind"}: "string", - {"Hover", "contents"}: "MarkupContent", + {"Hover", "contents"}: "Or_Hover_contents", {"InlayHint", "label"}: "[]InlayHintLabelPart", {"RelatedFullDocumentDiagnosticReport", "relatedDocuments"}: "map[DocumentUri]interface{}", diff --git a/integrationtests/snapshots/dart/hover/class-method.snap b/integrationtests/snapshots/dart/hover/class-method.snap new file mode 100644 index 0000000..1f2da1c --- /dev/null +++ b/integrationtests/snapshots/dart/hover/class-method.snap @@ -0,0 +1,7 @@ +```dart +String process() +``` +*types.dart* + +--- +A method that processes the value \ No newline at end of file diff --git a/integrationtests/snapshots/dart/hover/constant.snap b/integrationtests/snapshots/dart/hover/constant.snap new file mode 100644 index 0000000..010972b --- /dev/null +++ b/integrationtests/snapshots/dart/hover/constant.snap @@ -0,0 +1,9 @@ +```dart +String SHARED_CONSTANT +``` +Type: `String` + +*types.dart* + +--- +A constant value \ No newline at end of file diff --git a/integrationtests/snapshots/dart/hover/enum.snap b/integrationtests/snapshots/dart/hover/enum.snap new file mode 100644 index 0000000..93a6ff5 --- /dev/null +++ b/integrationtests/snapshots/dart/hover/enum.snap @@ -0,0 +1,7 @@ +```dart +enum Color +``` +*types.dart* + +--- +An enum for testing \ No newline at end of file diff --git a/integrationtests/snapshots/dart/hover/function.snap b/integrationtests/snapshots/dart/hover/function.snap new file mode 100644 index 0000000..e1c3c7d --- /dev/null +++ b/integrationtests/snapshots/dart/hover/function.snap @@ -0,0 +1,7 @@ +```dart +HelperClass createHelper(String name) +``` +*helper.dart* + +--- +A helper function that creates instances \ No newline at end of file diff --git a/integrationtests/snapshots/dart/hover/global-variable.snap b/integrationtests/snapshots/dart/hover/global-variable.snap new file mode 100644 index 0000000..43f16ba --- /dev/null +++ b/integrationtests/snapshots/dart/hover/global-variable.snap @@ -0,0 +1,9 @@ +```dart +HelperClass globalHelper +``` +Type: `HelperClass` + +*helper.dart* + +--- +Global variable for testing \ No newline at end of file diff --git a/integrationtests/snapshots/dart/hover/helper-class.snap b/integrationtests/snapshots/dart/hover/helper-class.snap new file mode 100644 index 0000000..96a3279 --- /dev/null +++ b/integrationtests/snapshots/dart/hover/helper-class.snap @@ -0,0 +1,7 @@ +```dart +class HelperClass implements SharedInterface +``` +*helper.dart* + +--- +Helper class for demonstration \ No newline at end of file diff --git a/integrationtests/snapshots/dart/hover/interface.snap b/integrationtests/snapshots/dart/hover/interface.snap new file mode 100644 index 0000000..913a011 --- /dev/null +++ b/integrationtests/snapshots/dart/hover/interface.snap @@ -0,0 +1,7 @@ +```dart +abstract class SharedInterface +``` +*types.dart* + +--- +An interface for testing \ No newline at end of file diff --git a/integrationtests/snapshots/dart/hover/main-function.snap b/integrationtests/snapshots/dart/hover/main-function.snap new file mode 100644 index 0000000..96a7f8c --- /dev/null +++ b/integrationtests/snapshots/dart/hover/main-function.snap @@ -0,0 +1,4 @@ +```dart +void main() +``` +*main.dart* \ No newline at end of file diff --git a/integrationtests/snapshots/dart/hover/no-hover-info.snap b/integrationtests/snapshots/dart/hover/no-hover-info.snap new file mode 100644 index 0000000..5c3db3c --- /dev/null +++ b/integrationtests/snapshots/dart/hover/no-hover-info.snap @@ -0,0 +1,2 @@ +No hover information available for this position on the following line: +// Main test file for Dart hover tests diff --git a/integrationtests/snapshots/dart/hover/override-method.snap b/integrationtests/snapshots/dart/hover/override-method.snap new file mode 100644 index 0000000..1a0a8bf --- /dev/null +++ b/integrationtests/snapshots/dart/hover/override-method.snap @@ -0,0 +1,4 @@ +```dart +void doSomething() +``` +*helper.dart* \ No newline at end of file diff --git a/integrationtests/snapshots/dart/hover/shared-class.snap b/integrationtests/snapshots/dart/hover/shared-class.snap new file mode 100644 index 0000000..bdbc0ea --- /dev/null +++ b/integrationtests/snapshots/dart/hover/shared-class.snap @@ -0,0 +1,8 @@ +```dart +class SharedClass +``` +*types.dart* + +--- +A shared class used across multiple files +This class demonstrates hover information \ No newline at end of file diff --git a/integrationtests/snapshots/dart/hover/type-alias.snap b/integrationtests/snapshots/dart/hover/type-alias.snap new file mode 100644 index 0000000..d61ae6f --- /dev/null +++ b/integrationtests/snapshots/dart/hover/type-alias.snap @@ -0,0 +1,7 @@ +```dart +typedef ProcessFunction = String Function(int ) +``` +*types.dart* + +--- +A type alias for testing \ No newline at end of file diff --git a/integrationtests/snapshots/dart/hover/variable.snap b/integrationtests/snapshots/dart/hover/variable.snap new file mode 100644 index 0000000..683d576 --- /dev/null +++ b/integrationtests/snapshots/dart/hover/variable.snap @@ -0,0 +1,4 @@ +```dart +InvalidType helper +``` +Type: `InvalidType` \ No newline at end of file diff --git a/integrationtests/tests/dart/hover/hover_test.go b/integrationtests/tests/dart/hover/hover_test.go new file mode 100644 index 0000000..791e742 --- /dev/null +++ b/integrationtests/tests/dart/hover/hover_test.go @@ -0,0 +1,179 @@ +package hover_test + +import ( + "context" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/isaacphi/mcp-language-server/integrationtests/tests/common" + "github.com/isaacphi/mcp-language-server/integrationtests/tests/dart/internal" + "github.com/isaacphi/mcp-language-server/internal/tools" +) + +// TestHover tests hover functionality with the Dart language server +func TestHover(t *testing.T) { + tests := []struct { + name string + file string + line int + column int + expectedText string // Text that should be in the hover result + unexpectedText string // Text that should NOT be in the hover result (optional) + snapshotName string + }{ + // Tests using types.dart file + { + name: "SharedClass", + file: "types.dart", + line: 5, + column: 7, + expectedText: "SharedClass", + snapshotName: "shared-class", + }, + { + name: "ClassMethod", + file: "types.dart", + line: 15, + column: 10, + expectedText: "process", + snapshotName: "class-method", + }, + { + name: "Interface", + file: "types.dart", + line: 21, + column: 16, + expectedText: "SharedInterface", + snapshotName: "interface", + }, + { + name: "TypeAlias", + file: "types.dart", + line: 27, + column: 9, + expectedText: "ProcessFunction", + snapshotName: "type-alias", + }, + { + name: "Constant", + file: "types.dart", + line: 30, + column: 14, + expectedText: "SHARED_CONSTANT", + snapshotName: "constant", + }, + { + name: "Enum", + file: "types.dart", + line: 33, + column: 6, + expectedText: "Color", + snapshotName: "enum", + }, + // Tests using helper.dart file + { + name: "HelperClass", + file: "helper.dart", + line: 4, + column: 7, + expectedText: "HelperClass", + snapshotName: "helper-class", + }, + { + name: "OverrideMethod", + file: "helper.dart", + line: 10, + column: 8, + expectedText: "doSomething", + snapshotName: "override-method", + }, + { + name: "Function", + file: "helper.dart", + line: 25, + column: 14, + expectedText: "createHelper", + snapshotName: "function", + }, + { + name: "GlobalVariable", + file: "helper.dart", + line: 30, + column: 7, + expectedText: "globalHelper", + snapshotName: "global-variable", + }, + // Tests using main.dart file + { + name: "MainFunction", + file: "main.dart", + line: 5, + column: 6, + expectedText: "main", + snapshotName: "main-function", + }, + { + name: "Variable", + file: "main.dart", + line: 8, + column: 7, + expectedText: "helper", + snapshotName: "variable", + }, + } + + suite := internal.GetTestSuite(t) + + // Wait for initialization + time.Sleep(time.Duration(suite.Config.InitializeTimeMs) * time.Millisecond) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filePath := filepath.Join(suite.WorkspaceDir, tt.file) + + // Get hover information + result, err := tools.GetHoverInfo(context.Background(), suite.Client, filePath, tt.line, tt.column) + if err != nil { + t.Fatalf("Failed to get hover info: %v", err) + } + + // Check if expected text is present + if tt.expectedText != "" && !strings.Contains(result, tt.expectedText) { + t.Errorf("Expected text '%s' not found in hover result: %s", tt.expectedText, result) + } + + // Check if unexpected text is absent + if tt.unexpectedText != "" && strings.Contains(result, tt.unexpectedText) { + t.Errorf("Unexpected text '%s' found in hover result: %s", tt.unexpectedText, result) + } + + // Perform snapshot test + common.SnapshotTest(t, suite.LanguageName, "hover", tt.snapshotName, result) + }) + } +} + +// TestHoverNoInfo tests hover on positions without hover information +func TestHoverNoInfo(t *testing.T) { + suite := internal.GetTestSuite(t) + + // Wait for initialization + time.Sleep(time.Duration(suite.Config.InitializeTimeMs) * time.Millisecond) + + // Test hover on an empty line or comment + filePath := filepath.Join(suite.WorkspaceDir, "main.dart") + result, err := tools.GetHoverInfo(context.Background(), suite.Client, filePath, 1, 1) + if err != nil { + t.Fatalf("Failed to get hover info: %v", err) + } + + // Should indicate no hover information + if !strings.Contains(result, "No hover information") { + t.Logf("Result when no hover info expected: %s", result) + } + + // Snapshot test for no hover info case + common.SnapshotTest(t, suite.LanguageName, "hover", "no-hover-info", result) +} diff --git a/integrationtests/tests/dart/internal/helpers.go b/integrationtests/tests/dart/internal/helpers.go new file mode 100644 index 0000000..39f4311 --- /dev/null +++ b/integrationtests/tests/dart/internal/helpers.go @@ -0,0 +1,57 @@ +// Package internal contains shared helpers for Dart tests +package internal + +import ( + "os/exec" + "path/filepath" + "testing" + + "github.com/isaacphi/mcp-language-server/integrationtests/tests/common" +) + +// GetTestSuite returns a test suite for Dart language server tests +func GetTestSuite(t *testing.T) *common.TestSuite { + // Configure Dart LSP + repoRoot, err := filepath.Abs("../../../..") + if err != nil { + t.Fatalf("Failed to get repo root: %v", err) + } + + config := common.LSPTestConfig{ + Name: "dart", + Command: "dart", + Args: []string{"language-server", "--protocol=lsp"}, + WorkspaceDir: filepath.Join(repoRoot, "integrationtests/workspaces/dart"), + InitializeTimeMs: 3000, // 3 seconds - Dart LSP can be slower to initialize + } + + // Create a test suite + suite := common.NewTestSuite(t, config) + + // Set up the suite + err = suite.Setup() + if err != nil { + t.Fatalf("Failed to set up test suite: %v", err) + } + + // Run dart pub get in the workspace to ensure dependencies are available + // This ensures the Dart LSP can index pub packages for workspace symbols + if err := runDartPubGet(suite.WorkspaceDir); err != nil { + t.Logf("Warning: Failed to run dart pub get: %v", err) + // Don't fail the test, as it might still work without dependencies + } + + // Register cleanup + t.Cleanup(func() { + suite.Cleanup() + }) + + return suite +} + +// runDartPubGet runs 'dart pub get' in the specified directory +func runDartPubGet(dir string) error { + cmd := exec.Command("dart", "pub", "get") + cmd.Dir = dir + return cmd.Run() +} diff --git a/integrationtests/workspaces/dart/helper.dart b/integrationtests/workspaces/dart/helper.dart new file mode 100644 index 0000000..6332449 --- /dev/null +++ b/integrationtests/workspaces/dart/helper.dart @@ -0,0 +1,30 @@ +import 'types.dart'; + +/// Helper class for demonstration +class HelperClass implements SharedInterface { + final String name; + + HelperClass(this.name); + + @override + void doSomething() { + print('Doing something with $name'); + } + + @override + String getName() => name; + + /// Process method specific to HelperClass + void process() { + doSomething(); + print('Processing: ${getName()}'); + } +} + +/// A helper function that creates instances +HelperClass createHelper(String name) { + return HelperClass(name); +} + +/// Global variable for testing +final globalHelper = HelperClass('global'); \ No newline at end of file diff --git a/integrationtests/workspaces/dart/main.dart b/integrationtests/workspaces/dart/main.dart new file mode 100644 index 0000000..f98876c --- /dev/null +++ b/integrationtests/workspaces/dart/main.dart @@ -0,0 +1,13 @@ +// Main test file for Dart hover tests +import 'helper.dart'; +import 'types.dart'; + +void main() { + print('Hello, World!'); + + var helper = HelperClass('test'); + helper.process(); + + SharedClass shared = SharedClass(42); + print(shared.getValue()); +} \ No newline at end of file diff --git a/integrationtests/workspaces/dart/pubspec.lock b/integrationtests/workspaces/dart/pubspec.lock new file mode 100644 index 0000000..356acce --- /dev/null +++ b/integrationtests/workspaces/dart/pubspec.lock @@ -0,0 +1,389 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: a169e44fe3506e212963b56a0497aa4e711e68a269a4d645873c1849aabf1515 + url: "https://pub.dev" + source: hosted + version: "87.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "731195f2f1837dc1bf0dcc60bbfca0d769181a7bcf4b98221738b2b61a3f058d" + url: "https://pub.dev" + source: hosted + version: "8.1.0" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + url: "https://pub.dev" + source: hosted + version: "1.26.3" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + test_core: + dependency: transitive + description: + name: test_core + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + url: "https://pub.dev" + source: hosted + version: "0.6.12" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.7.0 <4.0.0" diff --git a/integrationtests/workspaces/dart/pubspec.yaml b/integrationtests/workspaces/dart/pubspec.yaml new file mode 100644 index 0000000..86cf586 --- /dev/null +++ b/integrationtests/workspaces/dart/pubspec.yaml @@ -0,0 +1,9 @@ +name: dart_test_workspace +description: Test workspace for Dart LSP integration tests +publish_to: 'none' + +environment: + sdk: '>=3.0.0 <4.0.0' + +dev_dependencies: + test: ^1.24.0 \ No newline at end of file diff --git a/integrationtests/workspaces/dart/types.dart b/integrationtests/workspaces/dart/types.dart new file mode 100644 index 0000000..b5a620a --- /dev/null +++ b/integrationtests/workspaces/dart/types.dart @@ -0,0 +1,37 @@ +// Types for testing hover functionality + +/// A shared class used across multiple files +/// This class demonstrates hover information +class SharedClass { + final int value; + + /// Creates a new SharedClass instance + SharedClass(this.value); + + /// Gets the current value + int getValue() => value; + + /// A method that processes the value + String process() { + return 'Processed: $value'; + } +} + +/// An interface for testing +abstract class SharedInterface { + void doSomething(); + String getName(); +} + +/// A type alias for testing +typedef ProcessFunction = String Function(int); + +/// A constant value +const String SHARED_CONSTANT = 'shared_value'; + +/// An enum for testing +enum Color { + red, + green, + blue +} \ No newline at end of file diff --git a/internal/protocol/tsprotocol.go b/internal/protocol/tsprotocol.go index 07436c1..2ab6544 100644 --- a/internal/protocol/tsprotocol.go +++ b/internal/protocol/tsprotocol.go @@ -2563,7 +2563,7 @@ type GlobPattern = Or_GlobPattern // (alias) // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#hover type Hover struct { // The hover's content - Contents MarkupContent `json:"contents"` + Contents Or_Hover_contents `json:"contents"` // An optional range inside the text document that is used to // visualize the hover, e.g. by changing the background color. Range Range `json:"range,omitempty"` diff --git a/internal/tools/hover.go b/internal/tools/hover.go index 3c3dd53..2216013 100644 --- a/internal/tools/hover.go +++ b/internal/tools/hover.go @@ -47,8 +47,8 @@ func GetHoverInfo(ctx context.Context, client *lsp.Client, filePath string, line var result strings.Builder - // Process the hover contents based on Markup content - if hoverResult.Contents.Value == "" { + // Process the hover contents based on the union type + if hoverResult.Contents.Value == nil { // Extract the line where the hover was requested lineText, err := ExtractTextFromLocation(protocol.Location{ URI: uri, @@ -68,7 +68,40 @@ func GetHoverInfo(ctx context.Context, client *lsp.Client, filePath string, line } result.WriteString(fmt.Sprintf("No hover information available for this position on the following line:\n%s", lineText)) } else { - result.WriteString(hoverResult.Contents.Value) + // Handle the different types that contents can be + switch v := hoverResult.Contents.Value.(type) { + case protocol.MarkupContent: + result.WriteString(v.Value) + case protocol.MarkedString: + // MarkedString is a union type itself (string | {language, value}) + switch ms := v.Value.(type) { + case string: + result.WriteString(ms) + case protocol.MarkedStringWithLanguage: + // Format as markdown code block + result.WriteString(fmt.Sprintf("```%s\n%s\n```", ms.Language, ms.Value)) + default: + result.WriteString(fmt.Sprintf("%v", ms)) + } + case []protocol.MarkedString: + // Multiple marked strings - concatenate them + for i, ms := range v { + if i > 0 { + result.WriteString("\n") + } + switch msv := ms.Value.(type) { + case string: + result.WriteString(msv) + case protocol.MarkedStringWithLanguage: + result.WriteString(fmt.Sprintf("```%s\n%s\n```", msv.Language, msv.Value)) + default: + result.WriteString(fmt.Sprintf("%v", msv)) + } + } + default: + // Fallback for unexpected types + result.WriteString(fmt.Sprintf("%v", v)) + } } return result.String(), nil From a5a3464aca01a5f262920ada8833d623eeea31aa Mon Sep 17 00:00:00 2001 From: Jonathan Rudenberg Date: Fri, 8 Aug 2025 12:59:17 -0400 Subject: [PATCH 2/3] Fix Dart LSP definition lookup and add comprehensive tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed Dart LSP definition lookup issues: - Handle Dart's function/method naming convention (returns names with parentheses) - Support qualified names like ClassName.methodName - Extract definition text directly from workspace/symbol location - Update hover test snapshot for comment change Added comprehensive test coverage with 23 test cases covering classes, functions, methods, enums, mixins, extensions, and typedefs. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../snapshots/dart/definition/constant.snap | 10 + .../snapshots/dart/definition/enum.snap | 14 ++ .../dart/definition/global-function.snap | 12 ++ .../dart/definition/global-variable.snap | 10 + .../dart/definition/local-class.snap | 28 +++ .../dart/definition/main-function.snap | 73 +++++++ .../dart/definition/method-qualified.snap | 14 ++ .../dart/definition/method-simple.snap | 107 +++++++++++ .../dart/definition/nonexistent-symbol.snap | 1 + .../dart/definition/query-enum-value.snap | 14 ++ .../dart/definition/query-exact-match.snap | 28 +++ .../dart/definition/query-lowercase.snap | 1 + .../snapshots/dart/definition/query-main.snap | 73 +++++++ .../dart/definition/query-method-name.snap | 107 +++++++++++ .../dart/definition/query-partial-match.snap | 1 + .../dart/definition/query-qualified-name.snap | 1 + .../dart/definition/query-regex-pattern.snap | 1 + .../dart/definition/query-shared-class.snap | 24 +++ .../definition/query-wildcard-prefix.snap | 1 + .../dart/definition/shared-class-name.snap | 24 +++ .../dart/definition/shared-interface.snap | 13 ++ .../snapshots/dart/definition/typedef.snap | 10 + .../snapshots/dart/hover/no-hover-info.snap | 2 +- .../snapshots/dart/hover/variable.snap | 4 +- integrationtests/tests/common/helpers.go | 68 +++++++ .../tests/dart/definition/definition_test.go | 180 ++++++++++++++++++ integrationtests/workspaces/dart/main.dart | 19 +- internal/tools/definition.go | 56 +++++- internal/tools/utilities.go | 11 ++ 29 files changed, 893 insertions(+), 14 deletions(-) create mode 100644 integrationtests/snapshots/dart/definition/constant.snap create mode 100644 integrationtests/snapshots/dart/definition/enum.snap create mode 100644 integrationtests/snapshots/dart/definition/global-function.snap create mode 100644 integrationtests/snapshots/dart/definition/global-variable.snap create mode 100644 integrationtests/snapshots/dart/definition/local-class.snap create mode 100644 integrationtests/snapshots/dart/definition/main-function.snap create mode 100644 integrationtests/snapshots/dart/definition/method-qualified.snap create mode 100644 integrationtests/snapshots/dart/definition/method-simple.snap create mode 100644 integrationtests/snapshots/dart/definition/nonexistent-symbol.snap create mode 100644 integrationtests/snapshots/dart/definition/query-enum-value.snap create mode 100644 integrationtests/snapshots/dart/definition/query-exact-match.snap create mode 100644 integrationtests/snapshots/dart/definition/query-lowercase.snap create mode 100644 integrationtests/snapshots/dart/definition/query-main.snap create mode 100644 integrationtests/snapshots/dart/definition/query-method-name.snap create mode 100644 integrationtests/snapshots/dart/definition/query-partial-match.snap create mode 100644 integrationtests/snapshots/dart/definition/query-qualified-name.snap create mode 100644 integrationtests/snapshots/dart/definition/query-regex-pattern.snap create mode 100644 integrationtests/snapshots/dart/definition/query-shared-class.snap create mode 100644 integrationtests/snapshots/dart/definition/query-wildcard-prefix.snap create mode 100644 integrationtests/snapshots/dart/definition/shared-class-name.snap create mode 100644 integrationtests/snapshots/dart/definition/shared-interface.snap create mode 100644 integrationtests/snapshots/dart/definition/typedef.snap create mode 100644 integrationtests/tests/dart/definition/definition_test.go diff --git a/integrationtests/snapshots/dart/definition/constant.snap b/integrationtests/snapshots/dart/definition/constant.snap new file mode 100644 index 0000000..c1c0f5a --- /dev/null +++ b/integrationtests/snapshots/dart/definition/constant.snap @@ -0,0 +1,10 @@ +--- + +Symbol: SHARED_CONSTANT +/TEST_OUTPUT/workspace/types.dart +Kind: Variable +Range: L29:C1 - L30:C46 + +29|/// A constant value +30|const String SHARED_CONSTANT = 'shared_value' + diff --git a/integrationtests/snapshots/dart/definition/enum.snap b/integrationtests/snapshots/dart/definition/enum.snap new file mode 100644 index 0000000..56632c2 --- /dev/null +++ b/integrationtests/snapshots/dart/definition/enum.snap @@ -0,0 +1,14 @@ +--- + +Symbol: Color +/TEST_OUTPUT/workspace/types.dart +Kind: Enum +Range: L32:C1 - L37:C2 + +32|/// An enum for testing +33|enum Color { +34| red, +35| green, +36| blue +37|} + diff --git a/integrationtests/snapshots/dart/definition/global-function.snap b/integrationtests/snapshots/dart/definition/global-function.snap new file mode 100644 index 0000000..203b633 --- /dev/null +++ b/integrationtests/snapshots/dart/definition/global-function.snap @@ -0,0 +1,12 @@ +--- + +Symbol: createHelper(…) +/TEST_OUTPUT/workspace/helper.dart +Kind: Function +Range: L24:C1 - L27:C2 + +24|/// A helper function that creates instances +25|HelperClass createHelper(String name) { +26| return HelperClass(name); +27|} + diff --git a/integrationtests/snapshots/dart/definition/global-variable.snap b/integrationtests/snapshots/dart/definition/global-variable.snap new file mode 100644 index 0000000..1088982 --- /dev/null +++ b/integrationtests/snapshots/dart/definition/global-variable.snap @@ -0,0 +1,10 @@ +--- + +Symbol: globalHelper +/TEST_OUTPUT/workspace/helper.dart +Kind: Variable +Range: L29:C1 - L30:C43 + +29|/// Global variable for testing +30|final globalHelper = HelperClass('global') + diff --git a/integrationtests/snapshots/dart/definition/local-class.snap b/integrationtests/snapshots/dart/definition/local-class.snap new file mode 100644 index 0000000..c753279 --- /dev/null +++ b/integrationtests/snapshots/dart/definition/local-class.snap @@ -0,0 +1,28 @@ +--- + +Symbol: HelperClass +/TEST_OUTPUT/workspace/helper.dart +Kind: Class +Range: L3:C1 - L22:C2 + + 3|/// Helper class for demonstration + 4|class HelperClass implements SharedInterface { + 5| final String name; + 6| + 7| HelperClass(this.name); + 8| + 9| @override +10| void doSomething() { +11| print('Doing something with $name'); +12| } +13| +14| @override +15| String getName() => name; +16| +17| /// Process method specific to HelperClass +18| void process() { +19| doSomething(); +20| print('Processing: ${getName()}'); +21| } +22|} + diff --git a/integrationtests/snapshots/dart/definition/main-function.snap b/integrationtests/snapshots/dart/definition/main-function.snap new file mode 100644 index 0000000..ca0fc53 --- /dev/null +++ b/integrationtests/snapshots/dart/definition/main-function.snap @@ -0,0 +1,73 @@ +--- + +Symbol: main() +/TEST_OUTPUT/workspace/main.dart +Kind: Function +Range: L5:C1 - L24:C2 + + 5|void main() { + 6| print('Hello, World!'); + 7| + 8| var helper = HelperClass('test'); + 9| helper.process(); +10| +11| SharedClass shared = SharedClass(42); +12| print(shared.getValue()); +13| +14| // Use types +15| Color color = Color.red; +16| print(color); +17| +18| // Use constant +19| print(SHARED_CONSTANT); +20| +21| // Use function +22| var newHelper = createHelper('new'); +23| print(newHelper.getName()); +24|} + +--- + +Symbol: main() +File: /DART_SDK/lib/_internal/vm/bin/vmservice_io.dart +Kind: Function +Range: L273:C1 - L309:C2 + +273|@pragma('vm:entry-point', !bool.fromEnvironment('dart.vm.product')) +274|void main() { +275| // Set embedder hooks. +276| VMServiceEmbedderHooks.cleanup = cleanupCallback; +277| VMServiceEmbedderHooks.createTempDir = createTempDirCallback; +278| VMServiceEmbedderHooks.ddsConnected = ddsConnectedCallback; +279| VMServiceEmbedderHooks.ddsDisconnected = ddsDisconnectedCallback; +280| VMServiceEmbedderHooks.deleteDir = deleteDirCallback; +281| VMServiceEmbedderHooks.writeFile = writeFileCallback; +282| VMServiceEmbedderHooks.writeStreamFile = writeStreamFileCallback; +283| VMServiceEmbedderHooks.readFile = readFileCallback; +284| VMServiceEmbedderHooks.listFiles = listFilesCallback; +285| VMServiceEmbedderHooks.serverInformation = serverInformationCallback; +286| VMServiceEmbedderHooks.webServerControl = webServerControlCallback; +287| VMServiceEmbedderHooks.acceptNewWebSocketConnections = +288| webServerAcceptNewWebSocketConnections; +289| VMServiceEmbedderHooks.serveObservatory = serveObservatoryCallback; +290| VMServiceEmbedderHooks.getResidentCompilerInfoFile = +291| _getResidentCompilerInfoFile; +292| +293| server = Server( +294| // Always instantiate the vmservice object so that the exit message +295| // can be delivered and waiting loaders can be cancelled. +296| VMService(), +297| _ip, +298| _port, +299| _originCheckDisabled, +300| _authCodesDisabled, +301| _serviceInfoFilename, +302| _enableServicePortFallback, +303| ); +304| +305| if (_autoStart) { +306| _toggleWebServer(); +307| } +308| _registerSignalHandler(); +309|} + diff --git a/integrationtests/snapshots/dart/definition/method-qualified.snap b/integrationtests/snapshots/dart/definition/method-qualified.snap new file mode 100644 index 0000000..30bc3f3 --- /dev/null +++ b/integrationtests/snapshots/dart/definition/method-qualified.snap @@ -0,0 +1,14 @@ +--- + +Symbol: process() +/TEST_OUTPUT/workspace/helper.dart +Kind: Method +Container Name: HelperClass +Range: L17:C3 - L21:C4 + +17|/// Process method specific to HelperClass +18| void process() { +19| doSomething(); +20| print('Processing: ${getName()}'); +21| } + diff --git a/integrationtests/snapshots/dart/definition/method-simple.snap b/integrationtests/snapshots/dart/definition/method-simple.snap new file mode 100644 index 0000000..04e8d0d --- /dev/null +++ b/integrationtests/snapshots/dart/definition/method-simple.snap @@ -0,0 +1,107 @@ +--- + +Symbol: process +File: /DART_SDK/lib/io/io_resource_info.dart +Kind: Field +Container Name: _SpawnedProcessResourceInfo +Range: L146:C3 - L146:C25 + +146|final _Process process + +--- + +Symbol: process +File: /PUB_CACHE/hosted/pub.dev/test_core-0.6.12/lib/src/runner/vm/test_compiler.dart +Kind: Enum +Range: L246:C1 - L246:C35 + +246|enum VmTestType { isolate, process } + +--- + +Symbol: process() +/TEST_OUTPUT/workspace/helper.dart +Kind: Method +Container Name: HelperClass +Range: L17:C3 - L21:C4 + +17|/// Process method specific to HelperClass +18| void process() { +19| doSomething(); +20| print('Processing: ${getName()}'); +21| } + +--- + +Symbol: process() +/TEST_OUTPUT/workspace/types.dart +Kind: Method +Container Name: SharedClass +Range: L14:C3 - L17:C4 + +14|/// A method that processes the value +15| String process() { +16| return 'Processed: $value'; +17| } + +--- + +Symbol: process() +File: /DART_SDK/lib/ffi/dynamic_library.dart +Kind: Constructor +Container Name: DynamicLibrary +Range: L12:C3 - L16:C45 + +12|/// Creates a [DynamicLibrary] holding all global symbols. +13| /// +14| /// Any symbol in a library currently loaded with global visibility +15| /// (including the executable itself) may be resolved through this library. +16| external factory DynamicLibrary.process(); + +--- + +Symbol: process(…) +File: /DART_SDK/lib/io/data_transformer.dart +Kind: Method +Container Name: RawZLibFilter +Range: L436:C3 - L439:C52 + +436|/// Process a chunk of data. +437| /// +438| /// This method must only be called when [processed] returns `null`. +439| void process(List data, int start, int end); + +--- + +Symbol: process(…) +File: /PUB_CACHE/hosted/pub.dev/analyzer-8.1.0/lib/src/dart/element/replace_top_bottom_visitor.dart +Kind: Method +Container Name: ReplaceTopBottomVisitor +Range: L20:C3 - L44:C4 + +20|TypeImpl process(TypeImpl type, Variance variance) { +21| if (variance.isContravariant) { +22| // ...replacing every occurrence in `T` of a type `S` in a contravariant +23| // position where `S <: Never` by `Object?` +24| if (_typeSystem.isSubtypeOf(type, NeverTypeImpl.instance)) { +25| return _topType; +26| } +27| } else { +28| // ...and every occurrence in `T` of a top type in a position which +29| // is not contravariant by `Never`. +30| if (_typeSystem.isTop(type)) { +31| return _bottomType; +32| } +33| } +34| +35| var alias = type.alias; +36| if (alias != null) { +37| return _instantiatedTypeAlias(type, alias, variance); +38| } else if (type is InterfaceTypeImpl) { +39| return _interfaceType(type, variance); +40| } else if (type is FunctionTypeImpl) { +41| return _functionType(type, variance); +42| } +43| return type; +44| } + diff --git a/integrationtests/snapshots/dart/definition/nonexistent-symbol.snap b/integrationtests/snapshots/dart/definition/nonexistent-symbol.snap new file mode 100644 index 0000000..42d776b --- /dev/null +++ b/integrationtests/snapshots/dart/definition/nonexistent-symbol.snap @@ -0,0 +1 @@ +NonExistentSymbol not found \ No newline at end of file diff --git a/integrationtests/snapshots/dart/definition/query-enum-value.snap b/integrationtests/snapshots/dart/definition/query-enum-value.snap new file mode 100644 index 0000000..56632c2 --- /dev/null +++ b/integrationtests/snapshots/dart/definition/query-enum-value.snap @@ -0,0 +1,14 @@ +--- + +Symbol: Color +/TEST_OUTPUT/workspace/types.dart +Kind: Enum +Range: L32:C1 - L37:C2 + +32|/// An enum for testing +33|enum Color { +34| red, +35| green, +36| blue +37|} + diff --git a/integrationtests/snapshots/dart/definition/query-exact-match.snap b/integrationtests/snapshots/dart/definition/query-exact-match.snap new file mode 100644 index 0000000..c753279 --- /dev/null +++ b/integrationtests/snapshots/dart/definition/query-exact-match.snap @@ -0,0 +1,28 @@ +--- + +Symbol: HelperClass +/TEST_OUTPUT/workspace/helper.dart +Kind: Class +Range: L3:C1 - L22:C2 + + 3|/// Helper class for demonstration + 4|class HelperClass implements SharedInterface { + 5| final String name; + 6| + 7| HelperClass(this.name); + 8| + 9| @override +10| void doSomething() { +11| print('Doing something with $name'); +12| } +13| +14| @override +15| String getName() => name; +16| +17| /// Process method specific to HelperClass +18| void process() { +19| doSomething(); +20| print('Processing: ${getName()}'); +21| } +22|} + diff --git a/integrationtests/snapshots/dart/definition/query-lowercase.snap b/integrationtests/snapshots/dart/definition/query-lowercase.snap new file mode 100644 index 0000000..88e00a1 --- /dev/null +++ b/integrationtests/snapshots/dart/definition/query-lowercase.snap @@ -0,0 +1 @@ +helperclass not found \ No newline at end of file diff --git a/integrationtests/snapshots/dart/definition/query-main.snap b/integrationtests/snapshots/dart/definition/query-main.snap new file mode 100644 index 0000000..ca0fc53 --- /dev/null +++ b/integrationtests/snapshots/dart/definition/query-main.snap @@ -0,0 +1,73 @@ +--- + +Symbol: main() +/TEST_OUTPUT/workspace/main.dart +Kind: Function +Range: L5:C1 - L24:C2 + + 5|void main() { + 6| print('Hello, World!'); + 7| + 8| var helper = HelperClass('test'); + 9| helper.process(); +10| +11| SharedClass shared = SharedClass(42); +12| print(shared.getValue()); +13| +14| // Use types +15| Color color = Color.red; +16| print(color); +17| +18| // Use constant +19| print(SHARED_CONSTANT); +20| +21| // Use function +22| var newHelper = createHelper('new'); +23| print(newHelper.getName()); +24|} + +--- + +Symbol: main() +File: /DART_SDK/lib/_internal/vm/bin/vmservice_io.dart +Kind: Function +Range: L273:C1 - L309:C2 + +273|@pragma('vm:entry-point', !bool.fromEnvironment('dart.vm.product')) +274|void main() { +275| // Set embedder hooks. +276| VMServiceEmbedderHooks.cleanup = cleanupCallback; +277| VMServiceEmbedderHooks.createTempDir = createTempDirCallback; +278| VMServiceEmbedderHooks.ddsConnected = ddsConnectedCallback; +279| VMServiceEmbedderHooks.ddsDisconnected = ddsDisconnectedCallback; +280| VMServiceEmbedderHooks.deleteDir = deleteDirCallback; +281| VMServiceEmbedderHooks.writeFile = writeFileCallback; +282| VMServiceEmbedderHooks.writeStreamFile = writeStreamFileCallback; +283| VMServiceEmbedderHooks.readFile = readFileCallback; +284| VMServiceEmbedderHooks.listFiles = listFilesCallback; +285| VMServiceEmbedderHooks.serverInformation = serverInformationCallback; +286| VMServiceEmbedderHooks.webServerControl = webServerControlCallback; +287| VMServiceEmbedderHooks.acceptNewWebSocketConnections = +288| webServerAcceptNewWebSocketConnections; +289| VMServiceEmbedderHooks.serveObservatory = serveObservatoryCallback; +290| VMServiceEmbedderHooks.getResidentCompilerInfoFile = +291| _getResidentCompilerInfoFile; +292| +293| server = Server( +294| // Always instantiate the vmservice object so that the exit message +295| // can be delivered and waiting loaders can be cancelled. +296| VMService(), +297| _ip, +298| _port, +299| _originCheckDisabled, +300| _authCodesDisabled, +301| _serviceInfoFilename, +302| _enableServicePortFallback, +303| ); +304| +305| if (_autoStart) { +306| _toggleWebServer(); +307| } +308| _registerSignalHandler(); +309|} + diff --git a/integrationtests/snapshots/dart/definition/query-method-name.snap b/integrationtests/snapshots/dart/definition/query-method-name.snap new file mode 100644 index 0000000..04e8d0d --- /dev/null +++ b/integrationtests/snapshots/dart/definition/query-method-name.snap @@ -0,0 +1,107 @@ +--- + +Symbol: process +File: /DART_SDK/lib/io/io_resource_info.dart +Kind: Field +Container Name: _SpawnedProcessResourceInfo +Range: L146:C3 - L146:C25 + +146|final _Process process + +--- + +Symbol: process +File: /PUB_CACHE/hosted/pub.dev/test_core-0.6.12/lib/src/runner/vm/test_compiler.dart +Kind: Enum +Range: L246:C1 - L246:C35 + +246|enum VmTestType { isolate, process } + +--- + +Symbol: process() +/TEST_OUTPUT/workspace/helper.dart +Kind: Method +Container Name: HelperClass +Range: L17:C3 - L21:C4 + +17|/// Process method specific to HelperClass +18| void process() { +19| doSomething(); +20| print('Processing: ${getName()}'); +21| } + +--- + +Symbol: process() +/TEST_OUTPUT/workspace/types.dart +Kind: Method +Container Name: SharedClass +Range: L14:C3 - L17:C4 + +14|/// A method that processes the value +15| String process() { +16| return 'Processed: $value'; +17| } + +--- + +Symbol: process() +File: /DART_SDK/lib/ffi/dynamic_library.dart +Kind: Constructor +Container Name: DynamicLibrary +Range: L12:C3 - L16:C45 + +12|/// Creates a [DynamicLibrary] holding all global symbols. +13| /// +14| /// Any symbol in a library currently loaded with global visibility +15| /// (including the executable itself) may be resolved through this library. +16| external factory DynamicLibrary.process(); + +--- + +Symbol: process(…) +File: /DART_SDK/lib/io/data_transformer.dart +Kind: Method +Container Name: RawZLibFilter +Range: L436:C3 - L439:C52 + +436|/// Process a chunk of data. +437| /// +438| /// This method must only be called when [processed] returns `null`. +439| void process(List data, int start, int end); + +--- + +Symbol: process(…) +File: /PUB_CACHE/hosted/pub.dev/analyzer-8.1.0/lib/src/dart/element/replace_top_bottom_visitor.dart +Kind: Method +Container Name: ReplaceTopBottomVisitor +Range: L20:C3 - L44:C4 + +20|TypeImpl process(TypeImpl type, Variance variance) { +21| if (variance.isContravariant) { +22| // ...replacing every occurrence in `T` of a type `S` in a contravariant +23| // position where `S <: Never` by `Object?` +24| if (_typeSystem.isSubtypeOf(type, NeverTypeImpl.instance)) { +25| return _topType; +26| } +27| } else { +28| // ...and every occurrence in `T` of a top type in a position which +29| // is not contravariant by `Never`. +30| if (_typeSystem.isTop(type)) { +31| return _bottomType; +32| } +33| } +34| +35| var alias = type.alias; +36| if (alias != null) { +37| return _instantiatedTypeAlias(type, alias, variance); +38| } else if (type is InterfaceTypeImpl) { +39| return _interfaceType(type, variance); +40| } else if (type is FunctionTypeImpl) { +41| return _functionType(type, variance); +42| } +43| return type; +44| } + diff --git a/integrationtests/snapshots/dart/definition/query-partial-match.snap b/integrationtests/snapshots/dart/definition/query-partial-match.snap new file mode 100644 index 0000000..419d411 --- /dev/null +++ b/integrationtests/snapshots/dart/definition/query-partial-match.snap @@ -0,0 +1 @@ +Helper not found \ No newline at end of file diff --git a/integrationtests/snapshots/dart/definition/query-qualified-name.snap b/integrationtests/snapshots/dart/definition/query-qualified-name.snap new file mode 100644 index 0000000..b82afd8 --- /dev/null +++ b/integrationtests/snapshots/dart/definition/query-qualified-name.snap @@ -0,0 +1 @@ +helper.HelperClass not found \ No newline at end of file diff --git a/integrationtests/snapshots/dart/definition/query-regex-pattern.snap b/integrationtests/snapshots/dart/definition/query-regex-pattern.snap new file mode 100644 index 0000000..d32178d --- /dev/null +++ b/integrationtests/snapshots/dart/definition/query-regex-pattern.snap @@ -0,0 +1 @@ +^Helper.* not found \ No newline at end of file diff --git a/integrationtests/snapshots/dart/definition/query-shared-class.snap b/integrationtests/snapshots/dart/definition/query-shared-class.snap new file mode 100644 index 0000000..ec01869 --- /dev/null +++ b/integrationtests/snapshots/dart/definition/query-shared-class.snap @@ -0,0 +1,24 @@ +--- + +Symbol: SharedClass +/TEST_OUTPUT/workspace/types.dart +Kind: Class +Range: L3:C1 - L18:C2 + + 3|/// A shared class used across multiple files + 4|/// This class demonstrates hover information + 5|class SharedClass { + 6| final int value; + 7| + 8| /// Creates a new SharedClass instance + 9| SharedClass(this.value); +10| +11| /// Gets the current value +12| int getValue() => value; +13| +14| /// A method that processes the value +15| String process() { +16| return 'Processed: $value'; +17| } +18|} + diff --git a/integrationtests/snapshots/dart/definition/query-wildcard-prefix.snap b/integrationtests/snapshots/dart/definition/query-wildcard-prefix.snap new file mode 100644 index 0000000..012ad5d --- /dev/null +++ b/integrationtests/snapshots/dart/definition/query-wildcard-prefix.snap @@ -0,0 +1 @@ +*Helper not found \ No newline at end of file diff --git a/integrationtests/snapshots/dart/definition/shared-class-name.snap b/integrationtests/snapshots/dart/definition/shared-class-name.snap new file mode 100644 index 0000000..ec01869 --- /dev/null +++ b/integrationtests/snapshots/dart/definition/shared-class-name.snap @@ -0,0 +1,24 @@ +--- + +Symbol: SharedClass +/TEST_OUTPUT/workspace/types.dart +Kind: Class +Range: L3:C1 - L18:C2 + + 3|/// A shared class used across multiple files + 4|/// This class demonstrates hover information + 5|class SharedClass { + 6| final int value; + 7| + 8| /// Creates a new SharedClass instance + 9| SharedClass(this.value); +10| +11| /// Gets the current value +12| int getValue() => value; +13| +14| /// A method that processes the value +15| String process() { +16| return 'Processed: $value'; +17| } +18|} + diff --git a/integrationtests/snapshots/dart/definition/shared-interface.snap b/integrationtests/snapshots/dart/definition/shared-interface.snap new file mode 100644 index 0000000..9da4344 --- /dev/null +++ b/integrationtests/snapshots/dart/definition/shared-interface.snap @@ -0,0 +1,13 @@ +--- + +Symbol: SharedInterface +/TEST_OUTPUT/workspace/types.dart +Kind: Class +Range: L20:C1 - L24:C2 + +20|/// An interface for testing +21|abstract class SharedInterface { +22| void doSomething(); +23| String getName(); +24|} + diff --git a/integrationtests/snapshots/dart/definition/typedef.snap b/integrationtests/snapshots/dart/definition/typedef.snap new file mode 100644 index 0000000..256375c --- /dev/null +++ b/integrationtests/snapshots/dart/definition/typedef.snap @@ -0,0 +1,10 @@ +--- + +Symbol: ProcessFunction +/TEST_OUTPUT/workspace/types.dart +Kind: Class +Range: L26:C1 - L27:C48 + +26|/// A type alias for testing +27|typedef ProcessFunction = String Function(int); + diff --git a/integrationtests/snapshots/dart/hover/no-hover-info.snap b/integrationtests/snapshots/dart/hover/no-hover-info.snap index 5c3db3c..340296e 100644 --- a/integrationtests/snapshots/dart/hover/no-hover-info.snap +++ b/integrationtests/snapshots/dart/hover/no-hover-info.snap @@ -1,2 +1,2 @@ No hover information available for this position on the following line: -// Main test file for Dart hover tests +// Main test file for Dart definition and hover tests diff --git a/integrationtests/snapshots/dart/hover/variable.snap b/integrationtests/snapshots/dart/hover/variable.snap index 683d576..2dc6e9e 100644 --- a/integrationtests/snapshots/dart/hover/variable.snap +++ b/integrationtests/snapshots/dart/hover/variable.snap @@ -1,4 +1,4 @@ ```dart -InvalidType helper +HelperClass helper ``` -Type: `InvalidType` \ No newline at end of file +Type: `HelperClass` \ No newline at end of file diff --git a/integrationtests/tests/common/helpers.go b/integrationtests/tests/common/helpers.go index 84ade98..e03b492 100644 --- a/integrationtests/tests/common/helpers.go +++ b/integrationtests/tests/common/helpers.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "sort" "strings" "testing" ) @@ -140,11 +141,73 @@ func normalizePaths(_ *testing.T, input string) string { lines[i] = "/GOROOT" + parts[1] } } + + // Normalize Dart SDK paths (various possible locations) + // Handle paths like /Users/*/fvm/versions/*/bin/cache/dart-sdk/ + // or /opt/hostedtoolcache/dart/*/x64/ + // or /home/*/.cache/dart-sdk/ + if strings.Contains(line, "/dart-sdk/") || strings.Contains(line, "/lib/") { + // Look for common Dart SDK path patterns + patterns := []struct { + contains string + prefix string + }{ + {"/fvm/versions/", "/DART_SDK/"}, + {"/hostedtoolcache/dart/", "/DART_SDK/"}, + {"/.cache/dart-sdk/", "/DART_SDK/"}, + {"/bin/cache/dart-sdk/", "/DART_SDK/"}, + } + + for _, pattern := range patterns { + if strings.Contains(line, pattern.contains) { + // Find where the SDK lib directory starts + if idx := strings.Index(line, "/lib/"); idx != -1 { + before := line[:idx] + after := line[idx+1:] // Skip the leading slash to avoid double slash + // Replace everything before /lib/ with /DART_SDK + if strings.Contains(before, "File: ") { + lines[i] = "File: " + pattern.prefix + after + } else { + lines[i] = pattern.prefix + after + } + break + } + } + } + } + + // Normalize pub-cache paths + // Handle paths like /Users/*/.pub-cache/ or /home/*/.pub-cache/ + if strings.Contains(line, "/.pub-cache/") { + parts := strings.Split(line, "/.pub-cache/") + if len(parts) > 1 { + // Replace with a simple placeholder path + if strings.Contains(line, "File: ") { + lines[i] = "File: /PUB_CACHE/" + parts[1] + } else { + lines[i] = "/PUB_CACHE/" + parts[1] + } + } + } } return strings.Join(lines, "\n") } +// sortDefinitions sorts multi-definition results for deterministic ordering +func sortDefinitions(input string) string { + // Split by definition separators (---\n\n) + definitions := strings.Split(input, "---\n\n") + if len(definitions) <= 1 { + return input // Single definition or no definitions + } + + // Sort definitions as raw strings + sort.Strings(definitions) + + return strings.Join(definitions, "---\n\n") +} + // FindRepoRoot locates the repository root by looking for specific indicators // Exported so it can be used by other packages func FindRepoRoot() (string, error) { @@ -178,6 +241,11 @@ func SnapshotTest(t *testing.T, languageName, toolName, testName, actualResult s // Normalize paths in the result to avoid system-specific paths in snapshots actualResult = normalizePaths(t, actualResult) + // Sort definitions for Dart tests to ensure deterministic ordering + if languageName == "dart" { + actualResult = sortDefinitions(actualResult) + } + // Get the absolute path to the snapshots directory repoRoot, err := FindRepoRoot() if err != nil { diff --git a/integrationtests/tests/dart/definition/definition_test.go b/integrationtests/tests/dart/definition/definition_test.go new file mode 100644 index 0000000..cd4cee2 --- /dev/null +++ b/integrationtests/tests/dart/definition/definition_test.go @@ -0,0 +1,180 @@ +package definition_test + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + "github.com/isaacphi/mcp-language-server/integrationtests/tests/common" + "github.com/isaacphi/mcp-language-server/integrationtests/tests/dart/internal" + "github.com/isaacphi/mcp-language-server/internal/tools" +) + +// TestDartDefinition tests definition lookup functionality with the Dart language server +func TestDartDefinition(t *testing.T) { + tests := []struct { + name string + symbolName string + found bool + description string + snapshotName string + }{ + { + name: "LocalClass", + symbolName: "HelperClass", + found: true, + description: "Local class defined in helper.dart", + snapshotName: "local-class", + }, + { + name: "SharedClassName", + symbolName: "SharedClass", + found: true, + description: "Shared class in types.dart", + snapshotName: "shared-class-name", + }, + { + name: "SharedInterface", + symbolName: "SharedInterface", + found: true, + description: "Shared interface in types.dart", + snapshotName: "shared-interface", + }, + { + name: "GlobalFunction", + symbolName: "createHelper", + found: true, + description: "Global function in helper.dart", + snapshotName: "global-function", + }, + { + name: "GlobalVariable", + symbolName: "globalHelper", + found: true, + description: "Global variable in helper.dart", + snapshotName: "global-variable", + }, + { + name: "Enum", + symbolName: "Color", + found: true, + description: "Enum in types.dart", + snapshotName: "enum", + }, + { + name: "MainFunction", + symbolName: "main", + found: true, + description: "Main function in main.dart", + snapshotName: "main-function", + }, + { + name: "MethodQualified", + symbolName: "HelperClass.process", + found: true, + description: "Method with qualified name", + snapshotName: "method-qualified", + }, + { + name: "MethodSimple", + symbolName: "process", + found: true, + description: "Method with simple name", + snapshotName: "method-simple", + }, + { + name: "NonexistentSymbol", + symbolName: "NonExistentSymbol", + found: false, + description: "Symbol that doesn't exist", + snapshotName: "nonexistent-symbol", + }, + { + name: "Typedef", + symbolName: "ProcessFunction", + found: true, + description: "Type alias in types.dart", + snapshotName: "typedef", + }, + { + name: "Constant", + symbolName: "SHARED_CONSTANT", + found: true, + description: "Constant in types.dart", + snapshotName: "constant", + }, + } + + suite := internal.GetTestSuite(t) + + // Wait for initialization + time.Sleep(time.Duration(suite.Config.InitializeTimeMs) * time.Millisecond) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := tools.ReadDefinition(context.Background(), suite.Client, tt.symbolName) + if err != nil { + t.Fatalf("Failed to read definition: %v", err) + } + + // Check if the symbol was found + notFoundMsg := fmt.Sprintf("%s not found", tt.symbolName) + if !tt.found { + // Symbol should not be found + if result != notFoundMsg { + t.Errorf("Expected symbol %s to not be found, but got: %s", tt.symbolName, result) + } + } else { + // Symbol should be found + if result == notFoundMsg { + t.Errorf("Expected symbol %s to be found, but got: %s", tt.symbolName, result) + } else if !strings.Contains(result, "Symbol:") { + t.Errorf("Result should contain symbol information, got: %s", result) + } + } + + // Snapshot test for consistent behavior + common.SnapshotTest(t, suite.LanguageName, "definition", tt.snapshotName, result) + }) + } +} + +// TestDartDefinitionWorkspaceSymbolBehavior tests the workspace/symbol behavior specifically +func TestDartDefinitionWorkspaceSymbolBehavior(t *testing.T) { + suite := internal.GetTestSuite(t) + + // Wait for initialization + time.Sleep(time.Duration(suite.Config.InitializeTimeMs) * time.Millisecond) + + // Test different query patterns to understand workspace/symbol behavior + queryTests := []struct { + name string + query string + }{ + {"exact-match", "HelperClass"}, + {"partial-match", "Helper"}, + {"lowercase", "helperclass"}, + {"wildcard-prefix", "*Helper"}, + {"regex-pattern", "^Helper.*"}, + {"qualified-name", "helper.HelperClass"}, + {"method-name", "process"}, + {"main", "main"}, + {"shared-class", "SharedClass"}, + {"enum-value", "Color"}, + } + + for _, tt := range queryTests { + t.Run("query-"+tt.name, func(t *testing.T) { + result, err := tools.ReadDefinition(context.Background(), suite.Client, tt.query) + if err != nil { + t.Fatalf("Failed to read definition: %v", err) + } + + // Document the current behavior for each query pattern + t.Logf("Query '%s' result: %s", tt.query, result) + common.SnapshotTest(t, suite.LanguageName, "definition", "query-"+tt.name, result) + }) + } +} diff --git a/integrationtests/workspaces/dart/main.dart b/integrationtests/workspaces/dart/main.dart index f98876c..d0b5e5e 100644 --- a/integrationtests/workspaces/dart/main.dart +++ b/integrationtests/workspaces/dart/main.dart @@ -1,13 +1,24 @@ -// Main test file for Dart hover tests +// Main test file for Dart definition and hover tests import 'helper.dart'; import 'types.dart'; void main() { print('Hello, World!'); - + var helper = HelperClass('test'); helper.process(); - + SharedClass shared = SharedClass(42); print(shared.getValue()); -} \ No newline at end of file + + // Use types + Color color = Color.red; + print(color); + + // Use constant + print(SHARED_CONSTANT); + + // Use function + var newHelper = createHelper('new'); + print(newHelper.getName()); +} diff --git a/internal/tools/definition.go b/internal/tools/definition.go index 9595b49..987db76 100644 --- a/internal/tools/definition.go +++ b/internal/tools/definition.go @@ -34,9 +34,30 @@ func ReadDefinition(ctx context.Context, client *lsp.Client, symbolName string) return true } + // Handle Dart LSP's function/method naming convention + // Dart returns names like "functionName()" or "functionName(…)" + if strings.HasPrefix(thisName, symbolName+"(") { + return true + } + + // Also handle the reverse - if user searches for "functionName()" but symbol is "functionName" + if strings.HasPrefix(symbolName, thisName+"(") { + return true + } + // Handle different matching strategies based on the search term if strings.Contains(symbolName, ".") { - // For qualified names like "Type.Method", don't do fuzzy match + // For qualified names like "Type.Method", handle Dart's naming + parts := strings.Split(symbolName, ".") + if len(parts) == 2 { + className := parts[0] + methodName := parts[1] + // Check if this is a method in the specified class + if vContainerName == className && + (thisName == methodName || strings.HasPrefix(thisName, methodName+"(")) { + return true + } + } } else if vKind == protocol.Method { // For methods, only match if the method name matches exactly Type.symbolName or Type::symbolName or symbolName @@ -71,6 +92,12 @@ func ReadDefinition(ctx context.Context, client *lsp.Client, symbolName string) toolsLogger.Debug("Found symbol: %s", symbol.GetName()) loc := symbol.GetLocation() + // Check if location has a valid URI + if string(loc.URI) == "" { + toolsLogger.Error("Symbol %s has empty URI, skipping", symbol.GetName()) + continue + } + err := client.OpenFile(ctx, loc.URI.Path()) if err != nil { toolsLogger.Error("Error opening file: %v", err) @@ -78,7 +105,27 @@ func ReadDefinition(ctx context.Context, client *lsp.Client, symbolName string) } banner := "---\n\n" - definition, loc, _, err := GetFullDefinition(ctx, client, loc) + + // Try to get the full definition + // For Go, workspace/symbol returns just the symbol name range, so GetFullDefinition is needed + // For Dart, workspace/symbol returns the full definition range + definition := "" + + // Try GetFullDefinition first for languages like Go that need it + def, newLoc, _, err := GetFullDefinition(ctx, client, loc) + if err == nil { + definition = def + loc = newLoc + } else { + // Fall back to extracting text directly from the location + // This works for Dart where workspace/symbol returns the full range + definition, err = ExtractTextFromLocation(loc) + if err != nil { + toolsLogger.Error("Error getting definition: %v", err) + continue + } + } + locationInfo := fmt.Sprintf( "Symbol: %s\n"+ "File: %s\n"+ @@ -93,11 +140,6 @@ func ReadDefinition(ctx context.Context, client *lsp.Client, symbolName string) loc.Range.End.Character+1, ) - if err != nil { - toolsLogger.Error("Error getting definition: %v", err) - continue - } - definition = addLineNumbers(definition, int(loc.Range.Start.Line)+1) definitions = append(definitions, banner+locationInfo+definition+"\n") diff --git a/internal/tools/utilities.go b/internal/tools/utilities.go index 841d360..f696f14 100644 --- a/internal/tools/utilities.go +++ b/internal/tools/utilities.go @@ -197,5 +197,16 @@ func QuerySymbol(ctx context.Context, client *lsp.Client, symbolName string) (st } } + // For qualified names like "ClassName.methodName", try searching for just the method name + // The filtering logic in ReadDefinition will handle matching the container + if len(results) == 0 && strings.Contains(symbolName, ".") { + parts := strings.Split(symbolName, ".") + if len(parts) == 2 { + // Search for just the method/field name + results, err = doQuerySymbol(ctx, client, parts[1]) + // Keep the original symbolName for filtering + } + } + return symbolName, results, err } From 5095722c45431beb583fb869a033c65ac7823e2a Mon Sep 17 00:00:00 2001 From: Jonathan Rudenberg Date: Fri, 8 Aug 2025 12:59:31 -0400 Subject: [PATCH 3/3] Fix watcher glob pattern matching for **/* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pattern **/* should match all files, but the matching logic was incorrectly checking if paths ended with a literal asterisk character. Added special case handling to make **/* match everything, which fixes the watcher tests. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- internal/watcher/watcher.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index bd405e8..b310969 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -374,6 +374,11 @@ func matchesSimpleGlob(pattern, path string) bool { if strings.HasPrefix(pattern, "**/") { rest := strings.TrimPrefix(pattern, "**/") + // Special case: **/* matches everything + if rest == "*" { + return true + } + // If the rest is a simple file extension pattern like *.go if strings.HasPrefix(rest, "*.") { ext := strings.TrimPrefix(rest, "*")