Other
diff --git a/integrationtests/snapshots/clangd/definition/class.snap b/integrationtests/snapshots/clangd/definition/class.snap
new file mode 100644
index 0000000..8b1a97b
--- /dev/null
+++ b/integrationtests/snapshots/clangd/definition/class.snap
@@ -0,0 +1,16 @@
+---
+
+Symbol: TestClass
+/TEST_OUTPUT/workspace/clangd/src/consumer.cpp
+Range: L7:C1 - L15:C2
+
+ 7|class TestClass {
+ 8| public:
+ 9| /**
+10| * @brief A method that takes an integer parameter.
+11| *
+12| * @param param The integer parameter to be processed.
+13| */
+14| void method(int param) { helperFunction(); }
+15|};
+
diff --git a/integrationtests/snapshots/clangd/definition/constant.snap b/integrationtests/snapshots/clangd/definition/constant.snap
new file mode 100644
index 0000000..90253d9
--- /dev/null
+++ b/integrationtests/snapshots/clangd/definition/constant.snap
@@ -0,0 +1,8 @@
+---
+
+Symbol: TEST_CONSTANT
+/TEST_OUTPUT/workspace/clangd/src/helper.cpp
+Range: L4:C1 - L4:C29
+
+4|const int TEST_CONSTANT = 42;
+
diff --git a/integrationtests/snapshots/clangd/definition/foobar.snap b/integrationtests/snapshots/clangd/definition/foobar.snap
new file mode 100644
index 0000000..0ad3994
--- /dev/null
+++ b/integrationtests/snapshots/clangd/definition/foobar.snap
@@ -0,0 +1,11 @@
+---
+
+Symbol: foo_bar
+/TEST_OUTPUT/workspace/src/main.cpp
+Range: L5:C1 - L8:C2
+
+5|void foo_bar() {
+6| std::cout << "Hello, World!" << std::endl;
+7| return;
+8|}
+
diff --git a/integrationtests/snapshots/clangd/definition/helperFunction.snap b/integrationtests/snapshots/clangd/definition/helperFunction.snap
new file mode 100644
index 0000000..4b9d978
--- /dev/null
+++ b/integrationtests/snapshots/clangd/definition/helperFunction.snap
@@ -0,0 +1,8 @@
+---
+
+Symbol: helperFunction
+/TEST_OUTPUT/workspace/clangd/src/helper.cpp
+Range: L7:C1 - L7:C71
+
+7|void helperFunction() { std::cout << "Helper function" << std::endl; }
+
diff --git a/integrationtests/snapshots/clangd/definition/method.snap b/integrationtests/snapshots/clangd/definition/method.snap
new file mode 100644
index 0000000..1ec50b2
--- /dev/null
+++ b/integrationtests/snapshots/clangd/definition/method.snap
@@ -0,0 +1,16 @@
+---
+
+Symbol: method
+/TEST_OUTPUT/workspace/clangd/src/consumer.cpp
+Range: L7:C1 - L15:C2
+
+ 7|class TestClass {
+ 8| public:
+ 9| /**
+10| * @brief A method that takes an integer parameter.
+11| *
+12| * @param param The integer parameter to be processed.
+13| */
+14| void method(int param) { helperFunction(); }
+15|};
+
diff --git a/integrationtests/snapshots/clangd/definition/struct.snap b/integrationtests/snapshots/clangd/definition/struct.snap
new file mode 100644
index 0000000..ff5711d
--- /dev/null
+++ b/integrationtests/snapshots/clangd/definition/struct.snap
@@ -0,0 +1,10 @@
+---
+
+Symbol: TestStruct
+/TEST_OUTPUT/workspace/clangd/src/types.cpp
+Range: L6:C1 - L8:C2
+
+6|struct TestStruct {
+7| int value;
+8|};
+
diff --git a/integrationtests/snapshots/clangd/definition/type.snap b/integrationtests/snapshots/clangd/definition/type.snap
new file mode 100644
index 0000000..c9bb0fc
--- /dev/null
+++ b/integrationtests/snapshots/clangd/definition/type.snap
@@ -0,0 +1,8 @@
+---
+
+Symbol: TestType
+/TEST_OUTPUT/workspace/clangd/src/types.cpp
+Range: L10:C1 - L10:C21
+
+10|using TestType = int;
+
diff --git a/integrationtests/snapshots/clangd/definition/variable.snap b/integrationtests/snapshots/clangd/definition/variable.snap
new file mode 100644
index 0000000..c9d1442
--- /dev/null
+++ b/integrationtests/snapshots/clangd/definition/variable.snap
@@ -0,0 +1,8 @@
+---
+
+Symbol: TEST_VARIABLE
+/TEST_OUTPUT/workspace/clangd/src/helper.cpp
+Range: L5:C1 - L5:C24
+
+5|int TEST_VARIABLE = 100; // A test variable used for integration testing purposes.
+
diff --git a/integrationtests/snapshots/clangd/diagnostics/clean.snap b/integrationtests/snapshots/clangd/diagnostics/clean.snap
new file mode 100644
index 0000000..a9dfdff
--- /dev/null
+++ b/integrationtests/snapshots/clangd/diagnostics/clean.snap
@@ -0,0 +1 @@
+/TEST_OUTPUT/workspace/src/clean.cpp
\ No newline at end of file
diff --git a/integrationtests/snapshots/clangd/diagnostics/unreachable.snap b/integrationtests/snapshots/clangd/diagnostics/unreachable.snap
new file mode 100644
index 0000000..5b8372b
--- /dev/null
+++ b/integrationtests/snapshots/clangd/diagnostics/unreachable.snap
@@ -0,0 +1,11 @@
+/TEST_OUTPUT/workspace/src/main.cpp
+Diagnostics in File: 1
+WARNING at L14:C3: Code will never be executed (Source: clang, Code: -Wunreachable-code)
+
+10|int main() {
+...
+12| return 0;
+13|
+14| foo_bar();
+15|
+16| // Intentional error: unreachable code
diff --git a/integrationtests/snapshots/clangd/hover/class-method.snap b/integrationtests/snapshots/clangd/hover/class-method.snap
new file mode 100644
index 0000000..4b6f4ac
--- /dev/null
+++ b/integrationtests/snapshots/clangd/hover/class-method.snap
@@ -0,0 +1,10 @@
+instance-method method
+
+→ void
+Parameters:
+- int param
+@brief A method that takes an integer parameter.
+@param param The integer parameter to be processed.
+
+// In TestClass
+public: void method(int param)
\ No newline at end of file
diff --git a/integrationtests/snapshots/clangd/hover/class-type.snap b/integrationtests/snapshots/clangd/hover/class-type.snap
new file mode 100644
index 0000000..da19143
--- /dev/null
+++ b/integrationtests/snapshots/clangd/hover/class-type.snap
@@ -0,0 +1,5 @@
+class TestClass
+
+Size: 1 byte
+
+class TestClass {}
\ No newline at end of file
diff --git a/integrationtests/snapshots/clangd/hover/function-definition-cpp.snap b/integrationtests/snapshots/clangd/hover/function-definition-cpp.snap
new file mode 100644
index 0000000..11114a0
--- /dev/null
+++ b/integrationtests/snapshots/clangd/hover/function-definition-cpp.snap
@@ -0,0 +1,5 @@
+function helperFunction
+
+→ void
+
+void helperFunction()
\ No newline at end of file
diff --git a/integrationtests/snapshots/clangd/hover/function-main.snap b/integrationtests/snapshots/clangd/hover/function-main.snap
new file mode 100644
index 0000000..ee683d4
--- /dev/null
+++ b/integrationtests/snapshots/clangd/hover/function-main.snap
@@ -0,0 +1,6 @@
+function foo_bar
+
+→ void
+FooBar is a simple function for testing
+
+void foo_bar()
\ No newline at end of file
diff --git a/integrationtests/snapshots/clangd/hover/no-hover-info-comment.snap b/integrationtests/snapshots/clangd/hover/no-hover-info-comment.snap
new file mode 100644
index 0000000..218ceb1
--- /dev/null
+++ b/integrationtests/snapshots/clangd/hover/no-hover-info-comment.snap
@@ -0,0 +1,2 @@
+No hover information available for this position on the following line:
+// FooBar is a simple function for testing
diff --git a/integrationtests/snapshots/clangd/hover/outside-file.snap b/integrationtests/snapshots/clangd/hover/outside-file.snap
new file mode 100644
index 0000000..f175581
--- /dev/null
+++ b/integrationtests/snapshots/clangd/hover/outside-file.snap
@@ -0,0 +1 @@
+No hover information available for this position on the following line:
diff --git a/integrationtests/snapshots/clangd/hover/variable.snap b/integrationtests/snapshots/clangd/hover/variable.snap
new file mode 100644
index 0000000..cae3b23
--- /dev/null
+++ b/integrationtests/snapshots/clangd/hover/variable.snap
@@ -0,0 +1,7 @@
+variable TEST_VARIABLE
+
+Type: int
+Value = 100 (0x64)
+A test variable used for integration testing purposes.
+
+int TEST_VARIABLE = 100
\ No newline at end of file
diff --git a/integrationtests/snapshots/clangd/references/foobar-function-references.snap b/integrationtests/snapshots/clangd/references/foobar-function-references.snap
new file mode 100644
index 0000000..855a974
--- /dev/null
+++ b/integrationtests/snapshots/clangd/references/foobar-function-references.snap
@@ -0,0 +1,15 @@
+---
+
+/TEST_OUTPUT/workspace/clangd/src/main.cpp
+References in File: 1
+At: L14:C3
+
+10|int main() {
+11| helperFunction();
+12| return 0;
+13|
+14| foo_bar();
+15|
+16| // Intentional error: unreachable code
+17| std::cout << "This is unreachable" << std::endl;
+18|}
diff --git a/integrationtests/snapshots/clangd/references/helper-function-references.snap b/integrationtests/snapshots/clangd/references/helper-function-references.snap
new file mode 100644
index 0000000..e678ff4
--- /dev/null
+++ b/integrationtests/snapshots/clangd/references/helper-function-references.snap
@@ -0,0 +1,31 @@
+---
+
+/TEST_OUTPUT/workspace/clangd/src/consumer.cpp
+References in File: 1
+At: L14:C28
+
+ 9| /**
+10| * @brief A method that takes an integer parameter.
+11| *
+12| * @param param The integer parameter to be processed.
+13| */
+14| void method(int param) { helperFunction(); }
+15|};
+
+---
+
+/TEST_OUTPUT/workspace/clangd/src/main.cpp
+References in File: 1
+At: L11:C3
+
+ 6| std::cout << "Hello, World!" << std::endl;
+ 7| return;
+ 8|}
+ 9|
+10|int main() {
+11| helperFunction();
+12| return 0;
+13|
+14| foo_bar();
+15|
+16| // Intentional error: unreachable code
diff --git a/integrationtests/tests/clangd/README.md b/integrationtests/tests/clangd/README.md
new file mode 100644
index 0000000..30753bc
--- /dev/null
+++ b/integrationtests/tests/clangd/README.md
@@ -0,0 +1,19 @@
+\
+# Clangd Integration Tests
+
+This directory contains integration tests for `clangd`, the C/C++ language server.
+
+## Prerequisites
+
+Before running these tests, you must generate the `compile_commands.json` file in the `integrationtests/workspaces/clangd` directory. This can typically be done by navigating to that directory and running a tool like `bear` with your build command (e.g., `bear -- make`).
+
+The GitHub Actions workflow for these tests uses the following command from the root of the repository:
+```bash
+cd integrationtests/workspaces/clangd
+bear -- make
+cd ../../../..
+```
+
+## Clangd Version
+
+These tests are currently run against **clangd version 16**. While they may pass with other versions of clangd, compatibility is not guaranteed.
diff --git a/integrationtests/tests/clangd/definition/definition_test.go b/integrationtests/tests/clangd/definition/definition_test.go
new file mode 100644
index 0000000..72d82d5
--- /dev/null
+++ b/integrationtests/tests/clangd/definition/definition_test.go
@@ -0,0 +1,171 @@
+package definition_test
+
+import (
+ "context"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/isaacphi/mcp-language-server/integrationtests/tests/clangd/internal"
+ "github.com/isaacphi/mcp-language-server/integrationtests/tests/common"
+ "github.com/isaacphi/mcp-language-server/internal/tools"
+)
+
+// TestReadDefinition tests the ReadDefinition tool with various C++ type definitions
+func TestReadDefinition(t *testing.T) {
+ // Helper function to open all files and wait for indexing
+ openAllFilesAndWait := func(suite *common.TestSuite, ctx context.Context) {
+ // Open one file so that clangd loads compiles commands and begins indexing
+ filesToOpen := []string{
+ "src/main.cpp",
+ }
+
+ for _, file := range filesToOpen {
+ filePath := filepath.Join(suite.WorkspaceDir, file)
+ err := suite.Client.OpenFile(ctx, filePath)
+ if err != nil {
+ // Don't fail the test, some files might not exist in certain tests
+ t.Logf("Note: Failed to open %s: %v", file, err)
+ }
+ }
+ // Wait for indexing to complete. clangd won't index files until they are opened.
+ time.Sleep(10 * time.Second)
+ }
+
+ suite := internal.GetTestSuite(t)
+
+ ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second)
+ defer cancel()
+
+ // Open all files and wait for clangd to index them
+ openAllFilesAndWait(suite, ctx)
+
+ tests := []struct {
+ name string
+ symbolName string
+ expectedText string
+ snapshotName string
+ }{
+ {
+ name: "Function",
+ symbolName: "foo_bar",
+ expectedText: "void foo_bar()",
+ snapshotName: "foobar",
+ },
+ {
+ name: "Class",
+ symbolName: "TestClass",
+ expectedText: "class TestClass",
+ snapshotName: "class",
+ },
+ {
+ name: "Method",
+ symbolName: "method",
+ expectedText: "void method(int param)",
+ snapshotName: "method",
+ },
+ {
+ name: "Struct",
+ symbolName: "TestStruct",
+ expectedText: "struct TestStruct",
+ snapshotName: "struct",
+ },
+ {
+ name: "Type",
+ symbolName: "TestType",
+ expectedText: "using TestType",
+ snapshotName: "type",
+ },
+ {
+ name: "Constant",
+ symbolName: "TEST_CONSTANT",
+ expectedText: "const int TEST_CONSTANT",
+ snapshotName: "constant",
+ },
+ {
+ name: "Variable",
+ symbolName: "TEST_VARIABLE",
+ expectedText: "int TEST_VARIABLE",
+ snapshotName: "variable",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ // Call the ReadDefinition tool
+ result, err := tools.ReadDefinition(ctx, suite.Client, tc.symbolName)
+ if err != nil {
+ t.Fatalf("Failed to read definition: %v", err)
+ }
+
+ // Check that the result contains relevant information
+ if !strings.Contains(result, tc.expectedText) {
+ t.Errorf("Definition does not contain expected text: %s", tc.expectedText)
+ }
+
+ // Use snapshot testing to verify exact output
+ common.SnapshotTest(t, "clangd", "definition", tc.snapshotName, result)
+ })
+ }
+}
+
+func TestReadDefinitionInAnotherFile(t *testing.T) {
+ // Helper function to open all files and wait for indexing
+ openAllFilesAndWait := func(suite *common.TestSuite, ctx context.Context) {
+ // Open all files to ensure clangd indexes everything
+ filesToOpen := []string{
+ "src/main.cpp",
+ }
+
+ for _, file := range filesToOpen {
+ filePath := filepath.Join(suite.WorkspaceDir, file)
+ err := suite.Client.OpenFile(ctx, filePath)
+ if err != nil {
+ // Don't fail the test, some files might not exist in certain tests
+ t.Logf("Note: Failed to open %s: %v", file, err)
+ }
+ }
+ time.Sleep(5 * time.Second)
+ }
+
+ suite := internal.GetTestSuite(t)
+
+ ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second)
+ defer cancel()
+
+ // Open all files and wait for clangd to index them
+ openAllFilesAndWait(suite, ctx)
+
+ tests := []struct {
+ name string
+ symbolName string
+ expectedText string
+ snapshotName string
+ }{
+ {
+ name: "Function",
+ symbolName: "helperFunction",
+ expectedText: "void helperFunction()",
+ snapshotName: "helperFunction",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ // Call the ReadDefinition tool
+ result, err := tools.ReadDefinition(ctx, suite.Client, tc.symbolName)
+ if err != nil {
+ t.Fatalf("Failed to read definition: %v", err)
+ }
+
+ // Check that the result contains relevant information
+ if !strings.Contains(result, tc.expectedText) {
+ t.Errorf("Definition does not contain expected text: %s", tc.expectedText)
+ }
+
+ // Use snapshot testing to verify exact output
+ common.SnapshotTest(t, "clangd", "definition", tc.snapshotName, result)
+ })
+ }
+}
diff --git a/integrationtests/tests/clangd/diagnostics/diagnostics_test.go b/integrationtests/tests/clangd/diagnostics/diagnostics_test.go
new file mode 100644
index 0000000..29ea864
--- /dev/null
+++ b/integrationtests/tests/clangd/diagnostics/diagnostics_test.go
@@ -0,0 +1,92 @@
+package diagnostics_test
+
+// note: clangd doesn't support pull diagnostics (textdocument/diagnostic)
+// see: https://github.com/clangd/clangd/issues/2108
+import (
+ "context"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/isaacphi/mcp-language-server/integrationtests/tests/clangd/internal"
+ "github.com/isaacphi/mcp-language-server/integrationtests/tests/common"
+ "github.com/isaacphi/mcp-language-server/internal/tools"
+)
+
+// TestDiagnostics tests diagnostics functionality with the Clangd language server
+func TestDiagnostics(t *testing.T) {
+ // Helper function to open all files and wait for indexing
+ openAllFilesAndWait := func(suite *common.TestSuite, ctx context.Context) {
+ // Open one file so that clangd loads compiles commands and begins indexing
+ filesToOpen := []string{
+ "src/main.cpp",
+ }
+
+ for _, file := range filesToOpen {
+ filePath := filepath.Join(suite.WorkspaceDir, file)
+ err := suite.Client.OpenFile(ctx, filePath)
+ if err != nil {
+ // Don't fail the test, some files might not exist in certain tests
+ t.Logf("Note: Failed to open %s: %v", file, err)
+ }
+ }
+ // Wait for indexing to complete. clangd won't index files until they are opened.
+ time.Sleep(10 * time.Second)
+
+ }
+
+ // Test with a clean file
+ t.Run("CleanFile", func(t *testing.T) {
+ // Get a test suite with clean code
+ suite := internal.GetTestSuite(t)
+
+ ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second)
+ defer cancel()
+
+ // Open all files and wait for clangd to index them
+ openAllFilesAndWait(suite, ctx)
+
+ filePath := filepath.Join(suite.WorkspaceDir, "src/clean.cpp")
+ result, err := tools.GetDiagnosticsForFile(ctx, suite.Client, filePath, 2, true)
+ if err != nil {
+ t.Fatalf("GetDiagnosticsForFile failed: %v", err)
+ }
+
+ // Verify we have no diagnostics
+ if !strings.Contains(result, "No diagnostics found") {
+ t.Errorf("Expected no diagnostics but got: %s", result)
+ }
+
+ common.SnapshotTest(t, "clangd", "diagnostics", "clean", result)
+ })
+
+ // Test with a file containing an error
+ t.Run("FileWithError", func(t *testing.T) {
+ // Get a test suite with code that contains errors
+ suite := internal.GetTestSuite(t)
+
+ ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second)
+ defer cancel()
+
+ // Open all files and wait for clangd to index them
+ openAllFilesAndWait(suite, ctx)
+
+ filePath := filepath.Join(suite.WorkspaceDir, "src/main.cpp")
+ result, err := tools.GetDiagnosticsForFile(ctx, suite.Client, filePath, 2, true)
+ if err != nil {
+ t.Fatalf("GetDiagnosticsForFile failed: %v", err)
+ }
+
+ // Verify we have diagnostics about unreachable code
+ if strings.Contains(result, "No diagnostics found") {
+ t.Errorf("Expected diagnostics but got none")
+ }
+
+ if !strings.Contains(result, "unreachable") {
+ t.Errorf("Expected unreachable code error but got: %s", result)
+ }
+
+ common.SnapshotTest(t, "clangd", "diagnostics", "unreachable", result)
+ })
+}
diff --git a/integrationtests/tests/clangd/hover/hover_test.go b/integrationtests/tests/clangd/hover/hover_test.go
new file mode 100644
index 0000000..a0f14a9
--- /dev/null
+++ b/integrationtests/tests/clangd/hover/hover_test.go
@@ -0,0 +1,150 @@
+package hover_test
+
+import (
+ "context"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/isaacphi/mcp-language-server/integrationtests/tests/clangd/internal"
+ "github.com/isaacphi/mcp-language-server/integrationtests/tests/common"
+ "github.com/isaacphi/mcp-language-server/internal/tools"
+)
+
+// TestHover tests hover functionality with the Clangd language server
+func TestHover(t *testing.T) {
+ // Helper function to open all files and wait for indexing
+ openAllFilesAndWait := func(suite *common.TestSuite, ctx context.Context) {
+ // Open one file so that clangd loads compiles commands and begins indexing
+ filesToOpen := []string{
+ "src/main.cpp",
+ }
+
+ for _, file := range filesToOpen {
+ filePath := filepath.Join(suite.WorkspaceDir, file)
+ err := suite.Client.OpenFile(ctx, filePath)
+ if err != nil {
+ // Don't fail the test, some files might not exist in certain tests
+ t.Logf("Note: Failed to open %s: %v", file, err)
+ }
+ }
+ // Wait for indexing to complete. clangd won't index files until they are opened.
+ time.Sleep(5 * time.Second)
+ }
+
+ 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.cpp file
+ {
+ name: "Class",
+ file: "src/consumer.cpp",
+ line: 7, // Assuming TestClass definition
+ column: 7, // "TestClass"
+ expectedText: "class TestClass",
+ snapshotName: "class-type",
+ },
+ {
+ name: "Method in Class",
+ file: "src/consumer.cpp",
+ line: 14, // Assuming method definition within TestClass
+ column: 10, // "method"
+ expectedText: "public: void method(int param)",
+ snapshotName: "class-method",
+ },
+ {
+ name: "Variable", // Global variable in helper.cpp with inline comment
+ file: "src/helper.cpp",
+ line: 5, // Assuming TEST_VARIABLE definition
+ column: 5, // "TEST_VARIABLE"
+ expectedText: "int TEST_VARIABLE",
+ snapshotName: "variable",
+ },
+ {
+ name: "Function in main.cpp",
+ file: "src/main.cpp",
+ line: 14, // Assuming foo_bar use
+ column: 6, // "foo_bar"
+ expectedText: "function foo_bar",
+ snapshotName: "function-main",
+ },
+ {
+ name: "Function definition in helper.cpp",
+ file: "src/main.cpp",
+ line: 11, // Assuming helperFunction use
+ column: 7, // "helperFunction"
+ expectedText: "function helperFunction",
+ snapshotName: "function-definition-cpp",
+ },
+ // Test for a location without hover info (empty space or comment)
+ {
+ name: "NoHoverInfoComment",
+ file: "src/main.cpp",
+ line: 4, // Comment line
+ column: 1,
+ unexpectedText: "void", // Should not find any specific code hover
+ snapshotName: "no-hover-info-comment",
+ },
+ // Test for a location outside the file - expect an error or no result
+ {
+ name: "OutsideFile",
+ file: "src/main.cpp",
+ line: 1000, // Line number beyond file length
+ column: 1,
+ unexpectedText: "void", // Should not find any specific code hover
+ snapshotName: "outside-file",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Get a test suite
+ suite := internal.GetTestSuite(t)
+
+ ctx, cancel := context.WithTimeout(suite.Context, 10*time.Second)
+ defer cancel()
+
+ // Open all files and wait for clangd to index them
+ openAllFilesAndWait(suite, ctx)
+
+ filePath := filepath.Join(suite.WorkspaceDir, tt.file)
+
+ // Get hover info
+ result, err := tools.GetHoverInfo(ctx, suite.Client, filePath, tt.line, tt.column)
+ if err != nil {
+ // For the "OutsideFile" test or "NoHoverInfo" we might expect an error or empty result
+ if tt.name == "OutsideFile" || strings.HasPrefix(tt.name, "NoHoverInfo") {
+ // Create a snapshot even for error case or empty result
+ snapshotContent := "No hover information expected or error occurred."
+ if err != nil {
+ snapshotContent = err.Error()
+ } else if result != "" {
+ snapshotContent = result
+ }
+ common.SnapshotTest(t, "clangd", "hover", tt.snapshotName, snapshotContent)
+ return
+ }
+ t.Fatalf("GetHoverInfo failed for %s: %v. Result: %s", tt.name, err, result)
+ }
+
+ // Verify expected content
+ if tt.expectedText != "" && !strings.Contains(result, tt.expectedText) {
+ t.Errorf("Test %s: Expected hover info to contain %q but got: %s", tt.name, tt.expectedText, result)
+ }
+
+ // Verify unexpected content is absent
+ if tt.unexpectedText != "" && strings.Contains(result, tt.unexpectedText) {
+ t.Errorf("Test %s: Expected hover info NOT to contain %q but it was found: %s", tt.name, tt.unexpectedText, result)
+ }
+
+ common.SnapshotTest(t, "clangd", "hover", tt.snapshotName, result)
+ })
+ }
+}
diff --git a/integrationtests/tests/clangd/internal/helpers.go b/integrationtests/tests/clangd/internal/helpers.go
new file mode 100644
index 0000000..cccf4e6
--- /dev/null
+++ b/integrationtests/tests/clangd/internal/helpers.go
@@ -0,0 +1,41 @@
+// Package internal contains shared helpers for Clangd tests
+package internal
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/isaacphi/mcp-language-server/integrationtests/tests/common"
+)
+
+// GetTestSuite returns a test suite for Clangd language server tests
+func GetTestSuite(t *testing.T) *common.TestSuite {
+ // Configure Clangd LSP
+ repoRoot, err := filepath.Abs("../../../..")
+ if err != nil {
+ t.Fatalf("Failed to get repo root: %v", err)
+ }
+
+ config := common.LSPTestConfig{
+ Name: "clangd",
+ Command: "clangd",
+ Args: []string{"--compile-commands-dir=" + filepath.Join(repoRoot, "integrationtests/workspaces/clangd")},
+ WorkspaceDir: filepath.Join(repoRoot, "integrationtests/workspaces/clangd"),
+ InitializeTimeMs: 2000,
+ }
+
+ // Create a test suite
+ suite := common.NewTestSuite(t, config)
+
+ // Set up the suite
+ if err := suite.Setup(); err != nil {
+ t.Fatalf("Failed to set up test suite: %v", err)
+ }
+
+ // Register cleanup
+ t.Cleanup(func() {
+ suite.Cleanup()
+ })
+
+ return suite
+}
diff --git a/integrationtests/tests/clangd/references/references_test.go b/integrationtests/tests/clangd/references/references_test.go
new file mode 100644
index 0000000..9d8049d
--- /dev/null
+++ b/integrationtests/tests/clangd/references/references_test.go
@@ -0,0 +1,165 @@
+package references_test
+
+import (
+ "context"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/isaacphi/mcp-language-server/integrationtests/tests/clangd/internal"
+ "github.com/isaacphi/mcp-language-server/integrationtests/tests/common"
+ "github.com/isaacphi/mcp-language-server/internal/tools"
+)
+
+// TestFindReferences tests the FindReferences tool with C++ symbols
+// that have references across different files
+func TestFindReferences(t *testing.T) {
+ // Helper function to open all files and wait for indexing
+ openAllFilesAndWait := func(suite *common.TestSuite, ctx context.Context) {
+ // Open one file so that clangd loads compiles commands and begins indexing
+ filesToOpen := []string{
+ "src/main.cpp",
+ }
+
+ for _, file := range filesToOpen {
+ filePath := filepath.Join(suite.WorkspaceDir, file)
+ err := suite.Client.OpenFile(ctx, filePath)
+ if err != nil {
+ // Don't fail the test, some files might not exist in certain tests
+ t.Logf("Note: Failed to open %s: %v", file, err)
+ }
+ }
+ // Wait for indexing to complete. clangd won't index files until they are opened.
+ time.Sleep(30 * time.Second)
+ }
+
+ suite := internal.GetTestSuite(t)
+
+ ctx, cancel := context.WithTimeout(suite.Context, 30*time.Second) // Increased timeout for clangd references
+ defer cancel()
+
+ // Open all files and wait for clangd to index them
+ openAllFilesAndWait(suite, ctx)
+
+ tests := []struct {
+ name string
+ symbolName string // The symbol to find references for. For methods, use Class::Method.
+ fileHint string // File where the symbol definition is likely located (optional, can speed up).
+ lineHint int // Line number where the symbol is used/defined (optional, for focusing search).
+ colHint int // Column number (optional).
+ expectedText string // Text expected in one of the reference locations.
+ expectedFiles int // Minimum number of files where references should be found.
+ snapshotName string
+ }{
+ {
+ name: "Function with references across files",
+ symbolName: "helperFunction", // used in main.cpp, consumer.cpp. Clangd seems to treat definitations as declarations, so the definition in helper.cpp is not included.
+ fileHint: "src/helper.cpp",
+ lineHint: 7, // Definition line
+ colHint: 6,
+ expectedText: "main.cpp", // Expect a reference in main.cpp
+ expectedFiles: 2, // main.cpp, consumer.cpp
+ snapshotName: "helper-function-references",
+ },
+ {
+ name: "Function with reference in same file",
+ symbolName: "foo_bar", // Defined and used in main.cpp
+ fileHint: "src/main.cpp",
+ lineHint: 5, // Definition line
+ colHint: 6,
+ expectedText: "main.cpp",
+ expectedFiles: 1, // main.cpp (definition and usage)
+ snapshotName: "foobar-function-references",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ // Call the FindReferences tool
+ result, err := tools.FindReferences(ctx, suite.Client, tc.symbolName)
+ if err != nil {
+ t.Fatalf("Failed to find references for %s: %v. Result: %s", tc.symbolName, err, result)
+ }
+
+ // Check that the result contains relevant information
+ if !strings.Contains(result, tc.expectedText) {
+ t.Errorf("References for %s do not contain expected text %q in result: %s", tc.symbolName, tc.expectedText, result)
+ }
+
+ // Count how many different files are mentioned in the result
+ fileCount := countFilesInResult(result, suite.WorkspaceDir)
+ if fileCount < tc.expectedFiles {
+ t.Errorf("Expected references for %s in at least %d files, but found in %d files. Result:\n%s",
+ tc.symbolName, tc.expectedFiles, fileCount, result)
+ }
+
+ // Use snapshot testing to verify exact output
+ common.SnapshotTest(t, "clangd", "references", tc.snapshotName, result)
+ })
+ }
+}
+
+// countFilesInResult counts the number of unique files mentioned in the result
+// It now normalizes paths relative to the workspace for more robust counting.
+func countFilesInResult(result string, workspaceDir string) int {
+ fileMap := make(map[string]bool)
+
+ // Normalize workspaceDir path for string matching
+ normalizedWorkspaceDir := filepath.ToSlash(workspaceDir)
+
+ lines := strings.Split(result, "\n")
+ for _, line := range lines {
+ // A line representing a file path typically contains ".cpp" or ".hpp"
+ // and is an absolute path or relative to the workspace view.
+ if strings.Contains(line, ".cpp") || strings.Contains(line, ".hpp") {
+ // Attempt to extract a clean, relative path or a consistent absolute path part
+ var pathKey string
+ if strings.Contains(line, normalizedWorkspaceDir) {
+ relPath, err := filepath.Rel(normalizedWorkspaceDir, strings.Fields(line)[0]) // Assuming path is the first part
+ if err == nil {
+ pathKey = filepath.ToSlash(relPath)
+ } else {
+ // Fallback if Rel fails, use a snippet after a known part
+ pathKey = extractPathSegment(line)
+ }
+ } else {
+ // If not a full workspace path, it might be a relative path already or just a filename
+ pathKey = extractPathSegment(line) // A more generic extraction
+ }
+ if pathKey != "" {
+ fileMap[pathKey] = true
+ }
+ }
+ }
+ return len(fileMap)
+}
+
+// extractPathSegment tries to get a consistent file path identifier from a line of text.
+func extractPathSegment(line string) string {
+ // Look for common C++ file extensions
+ var ext string
+ if strings.Contains(line, ".cpp") {
+ ext = ".cpp"
+ } else if strings.Contains(line, ".hpp") {
+ ext = ".hpp"
+ } else {
+ return "" // Not a C++ source/header file line we can easily parse
+ }
+
+ fields := strings.Fields(line)
+ for _, field := range fields {
+ if strings.HasSuffix(field, ext) {
+ // Attempt to clean common prefixes like "uri: file://" or line numbers
+ cleanedPath := strings.TrimPrefix(field, "uri:")
+ cleanedPath = strings.TrimPrefix(cleanedPath, "file://")
+ // Remove trailing colons or line/char numbers like ":10:5"
+ parts := strings.Split(cleanedPath, ":")
+ if len(parts) > 0 {
+ return parts[0] // Return the part before the first colon, assuming it's the path
+ }
+ return cleanedPath
+ }
+ }
+ return ""
+}
diff --git a/integrationtests/tests/common/helpers.go b/integrationtests/tests/common/helpers.go
index 76ea59a..3d7243c 100644
--- a/integrationtests/tests/common/helpers.go
+++ b/integrationtests/tests/common/helpers.go
@@ -107,6 +107,15 @@ func normalizePaths(_ *testing.T, input string) string {
lines[i] = "/TEST_OUTPUT/workspace/" + parts[1]
}
}
+ // Some tests, e.g. clangd, may include fully qualified paths to the base /workspaces/ directory
+ if strings.Contains(line, "/workspaces/") {
+ // Extract everything after /workspace/
+ parts := strings.Split(line, "/workspaces/")
+ if len(parts) > 1 {
+ // Replace with a simple placeholder path
+ lines[i] = "/TEST_OUTPUT/workspace/" + parts[1]
+ }
+ }
}
return strings.Join(lines, "\n")
diff --git a/integrationtests/workspaces/clangd/Makefile b/integrationtests/workspaces/clangd/Makefile
new file mode 100644
index 0000000..dd96ef3
--- /dev/null
+++ b/integrationtests/workspaces/clangd/Makefile
@@ -0,0 +1,80 @@
+CXX = clang++
+LD = clang++
+
+# --- Include Path Configuration (from your original Makefile) ---
+# This section attempts to get all system include directories from clang++
+# and add them explicitly so clang tidy can find them.
+CXX_INCLUDE_DIRS := $(shell clang++ -E -x c++ - -v < /dev/null 2>&1 | grep -A 20 '#include <...>' | grep '^ ' | sed 's/^ //' | grep -v '(framework directory)')
+CXX_INCLUDE_FLAGS := $(foreach dir,$(CXX_INCLUDE_DIRS),-isystem $(dir))
+CXXFLAGS = -std=c++17 -I./include -Wunreachable-code -Wall -Wextra -Wno-error $(CXX_INCLUDE_FLAGS)
+
+# --- Source and Object File Definitions ---
+SRCDIR = src
+# Place object files in the same directory as sources, or a separate build/obj directory
+OBJDIR = $(SRCDIR)
+
+# Automatically find all .cpp files in the source directory
+SOURCES = $(wildcard $(SRCDIR)/*.cpp)
+
+# Create a list of object file names based on sources, placing them in OBJDIR
+# Example: src/main.cpp -> src/main.o
+OBJECTS = $(patsubst $(SRCDIR)/%.cpp,$(OBJDIR)/%.o,$(SOURCES))
+
+# --- Target Executables ---
+TARGET_PROGRAM = program
+TARGET_CLEAN_PROGRAM = clean_program # Assuming this is another program to be built from clean.cpp
+
+# --- Specific Object Files (if needed for explicit dependencies) ---
+# These should be correctly generated by the OBJECTS variable and pattern rule below.
+# Listing them explicitly for clarity in target dependencies.
+OBJ_MAIN = $(OBJDIR)/main.o
+# Add other specific object files your 'program' executable depends on
+OTHER_OBJS = $(OBJDIR)/helper.o $(OBJDIR)/types.o $(OBJDIR)/consumer.o $(OBJDIR)/another_consumer.o
+OBJ_FOR_CLEAN_PROGRAM = $(OBJDIR)/clean.o
+
+
+# --- Build Rules ---
+
+# Default target: build all specified programs
+all: $(TARGET_PROGRAM) $(TARGET_CLEAN_PROGRAM)
+
+# Rule to link the main program
+$(TARGET_PROGRAM): $(OBJ_MAIN) $(OTHER_OBJS)
+ @echo "Linking $@..."
+ $(LD) $^ -o $@ $(LDFLAGS)
+
+# Rule to build and link the 'clean_program'
+# This assumes clean.cpp is one of the files in $(SRCDIR)
+$(TARGET_CLEAN_PROGRAM): $(OBJ_FOR_CLEAN_PROGRAM)
+ @echo "Building and linking $@..."
+ $(LD) $^ -o $@ $(LDFLAGS)
+
+# --- Generic Pattern Rule for Compilation ---
+# This rule tells Make how to build any .o file in OBJDIR from a .cpp file in SRCDIR.
+# It crucially uses $(CXX) (clang++) and $(CXXFLAGS).
+$(OBJDIR)/%.o: $(SRCDIR)/%.cpp
+ @echo "Compiling $< to $@..."
+ $(CXX) $(CXXFLAGS) -c $< -o $@
+
+# --- Cleaning Rule ---
+clean:
+ @echo "Cleaning up object files and executables..."
+ rm -f $(OBJDIR)/*.o $(TARGET_PROGRAM) $(TARGET_CLEAN_PROGRAM)
+
+# --- Debugging Rule (optional) ---
+debug:
+ @echo "--- Debug Info ---"
+ @echo "CXX: $(CXX)"
+ @echo "CXXFLAGS: $(CXXFLAGS)"
+ @echo "LDFLAGS: $(LDFLAGS)"
+ @echo "SRCDIR: $(SRCDIR)"
+ @echo "OBJDIR: $(OBJDIR)"
+ @echo "SOURCES: $(SOURCES)"
+ @echo "OBJECTS: $(OBJECTS)"
+ @echo "TARGET_PROGRAM: $(TARGET_PROGRAM)"
+ @echo "TARGET_CLEAN_PROGRAM: $(TARGET_CLEAN_PROGRAM)"
+ @echo "--- End Debug Info ---"
+
+# --- Phony Targets ---
+# Declare targets that are not actual files
+.PHONY: all clean debug
\ No newline at end of file
diff --git a/integrationtests/workspaces/clangd/include/helper.hpp b/integrationtests/workspaces/clangd/include/helper.hpp
new file mode 100644
index 0000000..48fc6fc
--- /dev/null
+++ b/integrationtests/workspaces/clangd/include/helper.hpp
@@ -0,0 +1 @@
+void helperFunction();
\ No newline at end of file
diff --git a/integrationtests/workspaces/clangd/src/another_consumer.cpp b/integrationtests/workspaces/clangd/src/another_consumer.cpp
new file mode 100644
index 0000000..9013d61
--- /dev/null
+++ b/integrationtests/workspaces/clangd/src/another_consumer.cpp
@@ -0,0 +1,4 @@
+// Placeholder file for another_consumer.cpp
+#include
+
+void anotherConsume() { std::cout << "Another consume function" << std::endl; }
\ No newline at end of file
diff --git a/integrationtests/workspaces/clangd/src/clean.cpp b/integrationtests/workspaces/clangd/src/clean.cpp
new file mode 100644
index 0000000..bbb511f
--- /dev/null
+++ b/integrationtests/workspaces/clangd/src/clean.cpp
@@ -0,0 +1,7 @@
+// Placeholder file for clean.cpp
+#include
+
+int main() {
+ std::cout << "Clean file" << std::endl;
+ return 0;
+}
\ No newline at end of file
diff --git a/integrationtests/workspaces/clangd/src/consumer.cpp b/integrationtests/workspaces/clangd/src/consumer.cpp
new file mode 100644
index 0000000..5e162f2
--- /dev/null
+++ b/integrationtests/workspaces/clangd/src/consumer.cpp
@@ -0,0 +1,15 @@
+// Placeholder file for consumer.cpp
+#include
+#include "helper.hpp"
+
+void consume() { std::cout << "Consume function" << std::endl; }
+
+class TestClass {
+ public:
+ /**
+ * @brief A method that takes an integer parameter.
+ *
+ * @param param The integer parameter to be processed.
+ */
+ void method(int param) { helperFunction(); }
+};
\ No newline at end of file
diff --git a/integrationtests/workspaces/clangd/src/helper.cpp b/integrationtests/workspaces/clangd/src/helper.cpp
new file mode 100644
index 0000000..9397ea3
--- /dev/null
+++ b/integrationtests/workspaces/clangd/src/helper.cpp
@@ -0,0 +1,7 @@
+// Placeholder file for helper.cpp
+#include
+#include "helper.hpp"
+const int TEST_CONSTANT = 42;
+int TEST_VARIABLE = 100; // A test variable used for integration testing purposes.
+
+void helperFunction() { std::cout << "Helper function" << std::endl; }
\ No newline at end of file
diff --git a/integrationtests/workspaces/clangd/src/main.cpp b/integrationtests/workspaces/clangd/src/main.cpp
new file mode 100644
index 0000000..050bff8
--- /dev/null
+++ b/integrationtests/workspaces/clangd/src/main.cpp
@@ -0,0 +1,18 @@
+#include
+#include "helper.hpp"
+
+// FooBar is a simple function for testing
+void foo_bar() {
+ std::cout << "Hello, World!" << std::endl;
+ return;
+}
+
+int main() {
+ helperFunction();
+ return 0;
+
+ foo_bar();
+
+ // Intentional error: unreachable code
+ std::cout << "This is unreachable" << std::endl;
+}
\ No newline at end of file
diff --git a/integrationtests/workspaces/clangd/src/types.cpp b/integrationtests/workspaces/clangd/src/types.cpp
new file mode 100644
index 0000000..798ad7f
--- /dev/null
+++ b/integrationtests/workspaces/clangd/src/types.cpp
@@ -0,0 +1,10 @@
+// Placeholder file for types.cpp
+#include
+
+void printType() { std::cout << "Type function" << std::endl; }
+
+struct TestStruct {
+ int value;
+};
+
+using TestType = int;
\ No newline at end of file
diff --git a/internal/tools/references.go b/internal/tools/references.go
index 4af25da..cb424e5 100644
--- a/internal/tools/references.go
+++ b/internal/tools/references.go
@@ -66,7 +66,12 @@ func FindReferences(ctx context.Context, client *lsp.Client, symbolName string)
IncludeDeclaration: false,
},
}
-
+ // File is likely to be opened already, but may not be.
+ err := client.OpenFile(ctx, loc.URI.Path())
+ if err != nil {
+ toolsLogger.Error("Error opening file: %v", err)
+ continue
+ }
refs, err := client.References(ctx, refsParams)
if err != nil {
return "", fmt.Errorf("failed to get references: %v", err)