Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Fix Dart LSP hover marshalling issue and add integration tests
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 <[email protected]>
  • Loading branch information
titanous and claude committed Aug 8, 2025
commit 993d6606f09a3eaa5d4ebdcc65d2f26424da27f0
23 changes: 22 additions & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
*~
Expand Down
2 changes: 1 addition & 1 deletion cmd/generate/tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}",
Expand Down
7 changes: 7 additions & 0 deletions integrationtests/snapshots/dart/hover/class-method.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```dart
String process()
```
*types.dart*

---
A method that processes the value
9 changes: 9 additions & 0 deletions integrationtests/snapshots/dart/hover/constant.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
```dart
String SHARED_CONSTANT
```
Type: `String`

*types.dart*

---
A constant value
7 changes: 7 additions & 0 deletions integrationtests/snapshots/dart/hover/enum.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```dart
enum Color
```
*types.dart*

---
An enum for testing
7 changes: 7 additions & 0 deletions integrationtests/snapshots/dart/hover/function.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```dart
HelperClass createHelper(String name)
```
*helper.dart*

---
A helper function that creates instances
9 changes: 9 additions & 0 deletions integrationtests/snapshots/dart/hover/global-variable.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
```dart
HelperClass globalHelper
```
Type: `HelperClass`

*helper.dart*

---
Global variable for testing
7 changes: 7 additions & 0 deletions integrationtests/snapshots/dart/hover/helper-class.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```dart
class HelperClass implements SharedInterface
```
*helper.dart*

---
Helper class for demonstration
7 changes: 7 additions & 0 deletions integrationtests/snapshots/dart/hover/interface.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```dart
abstract class SharedInterface
```
*types.dart*

---
An interface for testing
4 changes: 4 additions & 0 deletions integrationtests/snapshots/dart/hover/main-function.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
```dart
void main()
```
*main.dart*
2 changes: 2 additions & 0 deletions integrationtests/snapshots/dart/hover/no-hover-info.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
No hover information available for this position on the following line:
// Main test file for Dart hover tests
4 changes: 4 additions & 0 deletions integrationtests/snapshots/dart/hover/override-method.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
```dart
void doSomething()
```
*helper.dart*
8 changes: 8 additions & 0 deletions integrationtests/snapshots/dart/hover/shared-class.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
```dart
class SharedClass
```
*types.dart*

---
A shared class used across multiple files
This class demonstrates hover information
7 changes: 7 additions & 0 deletions integrationtests/snapshots/dart/hover/type-alias.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```dart
typedef ProcessFunction = String Function(int )
```
*types.dart*

---
A type alias for testing
4 changes: 4 additions & 0 deletions integrationtests/snapshots/dart/hover/variable.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
```dart
InvalidType helper
```
Type: `InvalidType`
179 changes: 179 additions & 0 deletions integrationtests/tests/dart/hover/hover_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading