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/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/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..340296e --- /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 definition and 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..2dc6e9e --- /dev/null +++ b/integrationtests/snapshots/dart/hover/variable.snap @@ -0,0 +1,4 @@ +```dart +HelperClass helper +``` +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/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..d0b5e5e --- /dev/null +++ b/integrationtests/workspaces/dart/main.dart @@ -0,0 +1,24 @@ +// 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()); + + // 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/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/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/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 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 } 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, "*")