diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8d44d04..656e0bd 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -100,11 +100,10 @@ jobs: override: true - name: Install rust-analyzer - run: | - mkdir -p ~/.local/bin - curl -L https://github.com/rust-analyzer/rust-analyzer/releases/latest/download/rust-analyzer-x86_64-unknown-linux-gnu.gz | gunzip -c - > ~/.local/bin/rust-analyzer - chmod +x ~/.local/bin/rust-analyzer - echo "$HOME/.local/bin" >> $GITHUB_PATH + run: rustup component add rust-analyzer + + - run: rustc --version + - run: rust-analyzer --version - name: Run Rust integration tests run: go test ./integrationtests/languages/rust/... diff --git a/integrationtests/fixtures/snapshots/go/hover/constant.snap b/integrationtests/fixtures/snapshots/go/hover/constant.snap new file mode 100644 index 0000000..911f7e9 --- /dev/null +++ b/integrationtests/fixtures/snapshots/go/hover/constant.snap @@ -0,0 +1,12 @@ +```go +const SharedConstant untyped string = "shared value" +``` + +--- + +SharedConstant is used in multiple files + + +--- + +[`main.SharedConstant` on pkg.go.dev](https://pkg.go.dev/github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace#SharedConstant) \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/go/hover/interface-method-impl.snap b/integrationtests/fixtures/snapshots/go/hover/interface-method-impl.snap new file mode 100644 index 0000000..df6f952 --- /dev/null +++ b/integrationtests/fixtures/snapshots/go/hover/interface-method-impl.snap @@ -0,0 +1,23 @@ +```go +type SharedStruct struct { + ID int + Name string + Value float64 + Constants []string +} +``` + +--- + +SharedStruct is a struct used across multiple files + + +```go +func (s *SharedStruct) GetName() string +func (s *SharedStruct) Method() string +func (s *SharedStruct) Process() error +``` + +--- + +[`main.SharedStruct` on pkg.go.dev](https://pkg.go.dev/github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace#SharedStruct) \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/go/hover/interface-type.snap b/integrationtests/fixtures/snapshots/go/hover/interface-type.snap new file mode 100644 index 0000000..8604fd4 --- /dev/null +++ b/integrationtests/fixtures/snapshots/go/hover/interface-type.snap @@ -0,0 +1,15 @@ +```go +type SharedInterface interface { // size=16 (0x10) + Process() error + GetName() string +} +``` + +--- + +SharedInterface defines behavior implemented across files + + +--- + +[`main.SharedInterface` on pkg.go.dev](https://pkg.go.dev/github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace#SharedInterface) \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/go/hover/no-hover-info.snap b/integrationtests/fixtures/snapshots/go/hover/no-hover-info.snap new file mode 100644 index 0000000..40f321c --- /dev/null +++ b/integrationtests/fixtures/snapshots/go/hover/no-hover-info.snap @@ -0,0 +1,2 @@ +No hover information available for this position on the following line: +import "fmt" diff --git a/integrationtests/fixtures/snapshots/go/hover/outside-file.snap b/integrationtests/fixtures/snapshots/go/hover/outside-file.snap new file mode 100644 index 0000000..67eb0ec --- /dev/null +++ b/integrationtests/fixtures/snapshots/go/hover/outside-file.snap @@ -0,0 +1 @@ +failed to get hover information: request failed: line number 999 out of range 0-40 (code: 0) \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/go/hover/struct-method.snap b/integrationtests/fixtures/snapshots/go/hover/struct-method.snap new file mode 100644 index 0000000..df6f952 --- /dev/null +++ b/integrationtests/fixtures/snapshots/go/hover/struct-method.snap @@ -0,0 +1,23 @@ +```go +type SharedStruct struct { + ID int + Name string + Value float64 + Constants []string +} +``` + +--- + +SharedStruct is a struct used across multiple files + + +```go +func (s *SharedStruct) GetName() string +func (s *SharedStruct) Method() string +func (s *SharedStruct) Process() error +``` + +--- + +[`main.SharedStruct` on pkg.go.dev](https://pkg.go.dev/github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace#SharedStruct) \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/go/hover/struct-type.snap b/integrationtests/fixtures/snapshots/go/hover/struct-type.snap new file mode 100644 index 0000000..08c7820 --- /dev/null +++ b/integrationtests/fixtures/snapshots/go/hover/struct-type.snap @@ -0,0 +1,23 @@ +```go +type SharedStruct struct { // size=56 (0x38) + ID int + Name string + Value float64 + Constants []string +} +``` + +--- + +SharedStruct is a struct used across multiple files + + +```go +func (s *SharedStruct) GetName() string +func (s *SharedStruct) Method() string +func (s *SharedStruct) Process() error +``` + +--- + +[`main.SharedStruct` on pkg.go.dev](https://pkg.go.dev/github.com/isaacphi/mcp-language-server/integrationtests/test-output/go/workspace#SharedStruct) \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/python/definition/class.snap b/integrationtests/fixtures/snapshots/python/definition/class.snap index dd53c50..3c2992d 100644 --- a/integrationtests/fixtures/snapshots/python/definition/class.snap +++ b/integrationtests/fixtures/snapshots/python/definition/class.snap @@ -31,7 +31,7 @@ End Position: Line 59, Column 22 41| return self.value 42| 43| @staticmethod -44| def static_method(items: List[str]) -> Dict[str, int]: +44| def static_method(items: list[str]) -> dict[str, int]: 45| """Convert a list of items to a dictionary with item counts. 46| 47| Args: @@ -40,7 +40,7 @@ End Position: Line 59, Column 22 50| Returns: 51| A dictionary mapping items to their counts 52| """ -53| result: Dict[str, int] = {} +53| result: dict[str, int] = {} 54| for item in items: 55| if item in result: 56| result[item] += 1 diff --git a/integrationtests/fixtures/snapshots/python/definition/method.snap b/integrationtests/fixtures/snapshots/python/definition/method.snap index f80dcf7..e993c44 100644 --- a/integrationtests/fixtures/snapshots/python/definition/method.snap +++ b/integrationtests/fixtures/snapshots/python/definition/method.snap @@ -32,7 +32,7 @@ End Position: Line 59, Column 22 41| return self.value 42| 43| @staticmethod -44| def static_method(items: List[str]) -> Dict[str, int]: +44| def static_method(items: list[str]) -> dict[str, int]: 45| """Convert a list of items to a dictionary with item counts. 46| 47| Args: @@ -41,7 +41,7 @@ End Position: Line 59, Column 22 50| Returns: 51| A dictionary mapping items to their counts 52| """ -53| result: Dict[str, int] = {} +53| result: dict[str, int] = {} 54| for item in items: 55| if item in result: 56| result[item] += 1 diff --git a/integrationtests/fixtures/snapshots/python/definition/static-method.snap b/integrationtests/fixtures/snapshots/python/definition/static-method.snap index 34d9d21..7aa5ca6 100644 --- a/integrationtests/fixtures/snapshots/python/definition/static-method.snap +++ b/integrationtests/fixtures/snapshots/python/definition/static-method.snap @@ -32,7 +32,7 @@ End Position: Line 59, Column 22 41| return self.value 42| 43| @staticmethod -44| def static_method(items: List[str]) -> Dict[str, int]: +44| def static_method(items: list[str]) -> dict[str, int]: 45| """Convert a list of items to a dictionary with item counts. 46| 47| Args: @@ -41,7 +41,7 @@ End Position: Line 59, Column 22 50| Returns: 51| A dictionary mapping items to their counts 52| """ -53| result: Dict[str, int] = {} +53| result: dict[str, int] = {} 54| for item in items: 55| if item in result: 56| result[item] += 1 diff --git a/integrationtests/fixtures/snapshots/python/definition/variable.snap b/integrationtests/fixtures/snapshots/python/definition/variable.snap index 3b4cf40..451228a 100644 --- a/integrationtests/fixtures/snapshots/python/definition/variable.snap +++ b/integrationtests/fixtures/snapshots/python/definition/variable.snap @@ -5,5 +5,5 @@ Kind: Variable Start Position: Line 83, Column 1 End Position: Line 83, Column 14 === -83|test_variable: List[int] = [1, 2, 3, 4, 5] +83|test_variable: list[int] = [1, 2, 3, 4, 5] diff --git a/integrationtests/fixtures/snapshots/python/diagnostics/dependency.snap b/integrationtests/fixtures/snapshots/python/diagnostics/dependency.snap index 5ceab54..5a733f0 100644 --- a/integrationtests/fixtures/snapshots/python/diagnostics/dependency.snap +++ b/integrationtests/fixtures/snapshots/python/diagnostics/dependency.snap @@ -1,18 +1,18 @@ === /TEST_OUTPUT/workspace/consumer_clean.py -Location: Line 10, Column 15 +Location: Line 9, Column 15 Message: Argument missing for parameter "age" Source: Pyright Code: reportCallIssue === - 7|def consumer_function() -> None: - 8| """Function that consumes the helper functions.""" - 9| # Use the helper function -10| message = helper_function("World") -11| print(message) -12| -13| # Get and process items from the helper -14| items = get_items() -15| for item in items: -16| print(f"Processing {item}") + 6|def consumer_function() -> None: + 7| """Function that consumes the helper functions.""" + 8| # Use the helper function + 9| message = helper_function("World") +10| print(message) +11| +12| # Get and process items from the helper +13| items = get_items() +14| for item in items: +15| print(f"Processing {item}") diff --git a/integrationtests/fixtures/snapshots/python/diagnostics/errors.snap b/integrationtests/fixtures/snapshots/python/diagnostics/errors.snap index 7f4a061..d586c37 100644 --- a/integrationtests/fixtures/snapshots/python/diagnostics/errors.snap +++ b/integrationtests/fixtures/snapshots/python/diagnostics/errors.snap @@ -8,7 +8,7 @@ Code: reportReturnType === 25|def function_with_type_error() -> str: 26| """A function with a type error. -27| +27| 28| Returns: 29| Should return a string but actually returns an int 30| """ @@ -23,15 +23,15 @@ Code: reportUndefinedVariable === 34|class ErrorClass: 35| """A class with errors.""" -36| -37| def __init__(self, value: Dict[str, Any]): +36| +37| def __init__(self, value: dict[str, Any]): 38| """Initialize with errors. -39| +39| 40| Args: 41| value: A dictionary 42| """ 43| self.value = value -44| +44| 45| def method_with_undefined_variable(self) -> None: 46| """A method that uses an undefined variable.""" 47| print(undefined_variable) # Error: undefined_variable is not defined diff --git a/integrationtests/fixtures/snapshots/python/hover/class-method.snap b/integrationtests/fixtures/snapshots/python/hover/class-method.snap new file mode 100644 index 0000000..5f48ae5 --- /dev/null +++ b/integrationtests/fixtures/snapshots/python/hover/class-method.snap @@ -0,0 +1,12 @@ +(method) def test_method( + self: Self@TestClass, + increment: int +) -> int + +Increment the value by the given amount. + +Args: + increment: The amount to increment by + +Returns: + The new value \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/python/hover/class.snap b/integrationtests/fixtures/snapshots/python/hover/class.snap new file mode 100644 index 0000000..d6d137b --- /dev/null +++ b/integrationtests/fixtures/snapshots/python/hover/class.snap @@ -0,0 +1,3 @@ +(class) TestClass + +A test class with methods and attributes. \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/python/hover/constant.snap b/integrationtests/fixtures/snapshots/python/hover/constant.snap new file mode 100644 index 0000000..f19d095 --- /dev/null +++ b/integrationtests/fixtures/snapshots/python/hover/constant.snap @@ -0,0 +1 @@ +(constant) TEST_CONSTANT: Literal['test constant'] \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/python/hover/derived-class.snap b/integrationtests/fixtures/snapshots/python/hover/derived-class.snap new file mode 100644 index 0000000..a42b666 --- /dev/null +++ b/integrationtests/fixtures/snapshots/python/hover/derived-class.snap @@ -0,0 +1,3 @@ +(class) DerivedClass + +A class that inherits from BaseClass. \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/python/hover/function.snap b/integrationtests/fixtures/snapshots/python/hover/function.snap new file mode 100644 index 0000000..121de52 --- /dev/null +++ b/integrationtests/fixtures/snapshots/python/hover/function.snap @@ -0,0 +1,9 @@ +(function) def test_function(name: str) -> str + +A simple test function that returns a greeting message. + +Args: + name: The name to greet + +Returns: + A greeting message \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/python/hover/no-hover-info.snap b/integrationtests/fixtures/snapshots/python/hover/no-hover-info.snap new file mode 100644 index 0000000..44e64c1 --- /dev/null +++ b/integrationtests/fixtures/snapshots/python/hover/no-hover-info.snap @@ -0,0 +1,2 @@ +No hover information available for this position on the following line: + diff --git a/integrationtests/fixtures/snapshots/python/hover/outside-file.snap b/integrationtests/fixtures/snapshots/python/hover/outside-file.snap new file mode 100644 index 0000000..f175581 --- /dev/null +++ b/integrationtests/fixtures/snapshots/python/hover/outside-file.snap @@ -0,0 +1 @@ +No hover information available for this position on the following line: diff --git a/integrationtests/fixtures/snapshots/python/hover/static-method.snap b/integrationtests/fixtures/snapshots/python/hover/static-method.snap new file mode 100644 index 0000000..5c88c52 --- /dev/null +++ b/integrationtests/fixtures/snapshots/python/hover/static-method.snap @@ -0,0 +1,9 @@ +(method) def static_method(items: list[str]) -> Unknown + +Convert a list of items to a dictionary with item counts. + +Args: + items: A list of strings + +Returns: + A dictionary mapping items to their counts \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/python/hover/variable.snap b/integrationtests/fixtures/snapshots/python/hover/variable.snap new file mode 100644 index 0000000..480fb20 --- /dev/null +++ b/integrationtests/fixtures/snapshots/python/hover/variable.snap @@ -0,0 +1 @@ +(variable) test_variable: list[int] \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/python/references/class-method.snap b/integrationtests/fixtures/snapshots/python/references/class-method.snap index b0bb256..63dcffa 100644 --- a/integrationtests/fixtures/snapshots/python/references/class-method.snap +++ b/integrationtests/fixtures/snapshots/python/references/class-method.snap @@ -33,28 +33,28 @@ Reference at Line 40, Column 19: /TEST_OUTPUT/workspace/consumer.py References in File: 1 === -Reference at Line 48, Column 41: -35|def consumer_function() -> None: -36| """Function that consumes the helper functions.""" -37| # Use the helper function -38| message = helper_function("World") -39| print(message) -40| -41| # Get and process items from the helper -42| items = get_items() -43| for item in items: -44| print(f"Processing {item}") -45| -46| # Use the shared class -47| shared = SharedClass[str]("consumer", SHARED_CONSTANT) -48| print(f"Using shared class: {shared.get_name()} - {shared.get_value()}") -49| -50| # Use our implementation of the shared interface -51| impl = MyImplementation() -52| result = impl.process(items) -53| print(f"Processed items: {result}") -54| -55| # Use the enum -56| color = Color.RED -57| print(f"Selected color: {color}") +Reference at Line 47, Column 41: +34|def consumer_function() -> None: +35| """Function that consumes the helper functions.""" +36| # Use the helper function +37| message = helper_function("World") +38| print(message) +39| +40| # Get and process items from the helper +41| items = get_items() +42| for item in items: +43| print(f"Processing {item}") +44| +45| # Use the shared class +46| shared = SharedClass[str]("consumer", SHARED_CONSTANT) +47| print(f"Using shared class: {shared.get_name()} - {shared.get_value()}") +48| +49| # Use our implementation of the shared interface +50| impl = MyImplementation() +51| result = impl.process(items) +52| print(f"Processed items: {result}") +53| +54| # Use the enum +55| color = Color.RED +56| print(f"Selected color: {color}") diff --git a/integrationtests/fixtures/snapshots/python/references/color-enum.snap b/integrationtests/fixtures/snapshots/python/references/color-enum.snap index 8aec17b..d164c1e 100644 --- a/integrationtests/fixtures/snapshots/python/references/color-enum.snap +++ b/integrationtests/fixtures/snapshots/python/references/color-enum.snap @@ -33,28 +33,28 @@ Reference at Line 54, Column 13: /TEST_OUTPUT/workspace/consumer.py References in File: 2 === -Reference at Line 56, Column 13: -35|def consumer_function() -> None: -36| """Function that consumes the helper functions.""" -37| # Use the helper function -38| message = helper_function("World") -39| print(message) -40| -41| # Get and process items from the helper -42| items = get_items() -43| for item in items: -44| print(f"Processing {item}") -45| -46| # Use the shared class -47| shared = SharedClass[str]("consumer", SHARED_CONSTANT) -48| print(f"Using shared class: {shared.get_name()} - {shared.get_value()}") -49| -50| # Use our implementation of the shared interface -51| impl = MyImplementation() -52| result = impl.process(items) -53| print(f"Processed items: {result}") -54| -55| # Use the enum -56| color = Color.RED -57| print(f"Selected color: {color}") +Reference at Line 55, Column 13: +34|def consumer_function() -> None: +35| """Function that consumes the helper functions.""" +36| # Use the helper function +37| message = helper_function("World") +38| print(message) +39| +40| # Get and process items from the helper +41| items = get_items() +42| for item in items: +43| print(f"Processing {item}") +44| +45| # Use the shared class +46| shared = SharedClass[str]("consumer", SHARED_CONSTANT) +47| print(f"Using shared class: {shared.get_name()} - {shared.get_value()}") +48| +49| # Use our implementation of the shared interface +50| impl = MyImplementation() +51| result = impl.process(items) +52| print(f"Processed items: {result}") +53| +54| # Use the enum +55| color = Color.RED +56| print(f"Selected color: {color}") diff --git a/integrationtests/fixtures/snapshots/python/references/helper-function.snap b/integrationtests/fixtures/snapshots/python/references/helper-function.snap index 35d32d5..e8745b5 100644 --- a/integrationtests/fixtures/snapshots/python/references/helper-function.snap +++ b/integrationtests/fixtures/snapshots/python/references/helper-function.snap @@ -54,44 +54,44 @@ Reference at Line 50, Column 14: /TEST_OUTPUT/workspace/consumer.py References in File: 2 === -Reference at Line 38, Column 15: -35|def consumer_function() -> None: -36| """Function that consumes the helper functions.""" -37| # Use the helper function -38| message = helper_function("World") -39| print(message) -40| -41| # Get and process items from the helper -42| items = get_items() -43| for item in items: -44| print(f"Processing {item}") -45| -46| # Use the shared class -47| shared = SharedClass[str]("consumer", SHARED_CONSTANT) -48| print(f"Using shared class: {shared.get_name()} - {shared.get_value()}") -49| -50| # Use our implementation of the shared interface -51| impl = MyImplementation() -52| result = impl.process(items) -53| print(f"Processed items: {result}") -54| -55| # Use the enum -56| color = Color.RED -57| print(f"Selected color: {color}") +Reference at Line 37, Column 15: +34|def consumer_function() -> None: +35| """Function that consumes the helper functions.""" +36| # Use the helper function +37| message = helper_function("World") +38| print(message) +39| +40| # Get and process items from the helper +41| items = get_items() +42| for item in items: +43| print(f"Processing {item}") +44| +45| # Use the shared class +46| shared = SharedClass[str]("consumer", SHARED_CONSTANT) +47| print(f"Using shared class: {shared.get_name()} - {shared.get_value()}") +48| +49| # Use our implementation of the shared interface +50| impl = MyImplementation() +51| result = impl.process(items) +52| print(f"Processed items: {result}") +53| +54| # Use the enum +55| color = Color.RED +56| print(f"Selected color: {color}") === /TEST_OUTPUT/workspace/consumer_clean.py References in File: 2 === -Reference at Line 10, Column 15: - 7|def consumer_function() -> None: - 8| """Function that consumes the helper functions.""" - 9| # Use the helper function -10| message = helper_function("World") -11| print(message) -12| -13| # Get and process items from the helper -14| items = get_items() -15| for item in items: -16| print(f"Processing {item}") +Reference at Line 9, Column 15: + 6|def consumer_function() -> None: + 7| """Function that consumes the helper functions.""" + 8| # Use the helper function + 9| message = helper_function("World") +10| print(message) +11| +12| # Get and process items from the helper +13| items = get_items() +14| for item in items: +15| print(f"Processing {item}") diff --git a/integrationtests/fixtures/snapshots/python/references/interface-method.snap b/integrationtests/fixtures/snapshots/python/references/interface-method.snap index 9873d65..8a84865 100644 --- a/integrationtests/fixtures/snapshots/python/references/interface-method.snap +++ b/integrationtests/fixtures/snapshots/python/references/interface-method.snap @@ -2,28 +2,28 @@ /TEST_OUTPUT/workspace/consumer.py References in File: 1 === -Reference at Line 52, Column 19: -35|def consumer_function() -> None: -36| """Function that consumes the helper functions.""" -37| # Use the helper function -38| message = helper_function("World") -39| print(message) -40| -41| # Get and process items from the helper -42| items = get_items() -43| for item in items: -44| print(f"Processing {item}") -45| -46| # Use the shared class -47| shared = SharedClass[str]("consumer", SHARED_CONSTANT) -48| print(f"Using shared class: {shared.get_name()} - {shared.get_value()}") -49| -50| # Use our implementation of the shared interface -51| impl = MyImplementation() -52| result = impl.process(items) -53| print(f"Processed items: {result}") -54| -55| # Use the enum -56| color = Color.RED -57| print(f"Selected color: {color}") +Reference at Line 51, Column 19: +34|def consumer_function() -> None: +35| """Function that consumes the helper functions.""" +36| # Use the helper function +37| message = helper_function("World") +38| print(message) +39| +40| # Get and process items from the helper +41| items = get_items() +42| for item in items: +43| print(f"Processing {item}") +44| +45| # Use the shared class +46| shared = SharedClass[str]("consumer", SHARED_CONSTANT) +47| print(f"Using shared class: {shared.get_name()} - {shared.get_value()}") +48| +49| # Use our implementation of the shared interface +50| impl = MyImplementation() +51| result = impl.process(items) +52| print(f"Processed items: {result}") +53| +54| # Use the enum +55| color = Color.RED +56| print(f"Selected color: {color}") diff --git a/integrationtests/fixtures/snapshots/python/references/shared-class.snap b/integrationtests/fixtures/snapshots/python/references/shared-class.snap index d82821b..c8aa880 100644 --- a/integrationtests/fixtures/snapshots/python/references/shared-class.snap +++ b/integrationtests/fixtures/snapshots/python/references/shared-class.snap @@ -54,28 +54,28 @@ Reference at Line 37, Column 14: /TEST_OUTPUT/workspace/consumer.py References in File: 2 === -Reference at Line 47, Column 14: -35|def consumer_function() -> None: -36| """Function that consumes the helper functions.""" -37| # Use the helper function -38| message = helper_function("World") -39| print(message) -40| -41| # Get and process items from the helper -42| items = get_items() -43| for item in items: -44| print(f"Processing {item}") -45| -46| # Use the shared class -47| shared = SharedClass[str]("consumer", SHARED_CONSTANT) -48| print(f"Using shared class: {shared.get_name()} - {shared.get_value()}") -49| -50| # Use our implementation of the shared interface -51| impl = MyImplementation() -52| result = impl.process(items) -53| print(f"Processed items: {result}") -54| -55| # Use the enum -56| color = Color.RED -57| print(f"Selected color: {color}") +Reference at Line 46, Column 14: +34|def consumer_function() -> None: +35| """Function that consumes the helper functions.""" +36| # Use the helper function +37| message = helper_function("World") +38| print(message) +39| +40| # Get and process items from the helper +41| items = get_items() +42| for item in items: +43| print(f"Processing {item}") +44| +45| # Use the shared class +46| shared = SharedClass[str]("consumer", SHARED_CONSTANT) +47| print(f"Using shared class: {shared.get_name()} - {shared.get_value()}") +48| +49| # Use our implementation of the shared interface +50| impl = MyImplementation() +51| result = impl.process(items) +52| print(f"Processed items: {result}") +53| +54| # Use the enum +55| color = Color.RED +56| print(f"Selected color: {color}") diff --git a/integrationtests/fixtures/snapshots/python/references/shared-constant.snap b/integrationtests/fixtures/snapshots/python/references/shared-constant.snap index 9c679d4..e5204d3 100644 --- a/integrationtests/fixtures/snapshots/python/references/shared-constant.snap +++ b/integrationtests/fixtures/snapshots/python/references/shared-constant.snap @@ -54,28 +54,28 @@ Reference at Line 34, Column 30: /TEST_OUTPUT/workspace/consumer.py References in File: 2 === -Reference at Line 47, Column 43: -35|def consumer_function() -> None: -36| """Function that consumes the helper functions.""" -37| # Use the helper function -38| message = helper_function("World") -39| print(message) -40| -41| # Get and process items from the helper -42| items = get_items() -43| for item in items: -44| print(f"Processing {item}") -45| -46| # Use the shared class -47| shared = SharedClass[str]("consumer", SHARED_CONSTANT) -48| print(f"Using shared class: {shared.get_name()} - {shared.get_value()}") -49| -50| # Use our implementation of the shared interface -51| impl = MyImplementation() -52| result = impl.process(items) -53| print(f"Processed items: {result}") -54| -55| # Use the enum -56| color = Color.RED -57| print(f"Selected color: {color}") +Reference at Line 46, Column 43: +34|def consumer_function() -> None: +35| """Function that consumes the helper functions.""" +36| # Use the helper function +37| message = helper_function("World") +38| print(message) +39| +40| # Get and process items from the helper +41| items = get_items() +42| for item in items: +43| print(f"Processing {item}") +44| +45| # Use the shared class +46| shared = SharedClass[str]("consumer", SHARED_CONSTANT) +47| print(f"Using shared class: {shared.get_name()} - {shared.get_value()}") +48| +49| # Use our implementation of the shared interface +50| impl = MyImplementation() +51| result = impl.process(items) +52| print(f"Processed items: {result}") +53| +54| # Use the enum +55| color = Color.RED +56| print(f"Selected color: {color}") diff --git a/integrationtests/fixtures/snapshots/python/references/shared-interface.snap b/integrationtests/fixtures/snapshots/python/references/shared-interface.snap index cc76489..a083b4d 100644 --- a/integrationtests/fixtures/snapshots/python/references/shared-interface.snap +++ b/integrationtests/fixtures/snapshots/python/references/shared-interface.snap @@ -2,24 +2,24 @@ /TEST_OUTPUT/workspace/consumer.py References in File: 2 === -Reference at Line 14, Column 24: -14|class MyImplementation(SharedInterface): -15| """An implementation of the SharedInterface.""" -16| -17| def process(self, data: List[str]) -> Dict[str, int]: -18| """Process the given data by counting occurrences. -19| -20| Args: -21| data: List of strings to process -22| -23| Returns: -24| Dictionary with counts of each item -25| """ -26| result = {} -27| for item in data: -28| if item in result: -29| result[item] += 1 -30| else: -31| result[item] = 1 -32| return result +Reference at Line 13, Column 24: +13|class MyImplementation(SharedInterface): +14| """An implementation of the SharedInterface.""" +15| +16| def process(self, data: list[str]) -> dict[str, int]: +17| """Process the given data by counting occurrences. +18| +19| Args: +20| data: list of strings to process +21| +22| Returns: +23| dictionary with counts of each item +24| """ +25| result = {} +26| for item in data: +27| if item in result: +28| result[item] += 1 +29| else: +30| result[item] = 1 +31| return result diff --git a/integrationtests/fixtures/snapshots/rust/hover/constant.snap b/integrationtests/fixtures/snapshots/rust/hover/constant.snap new file mode 100644 index 0000000..1e3b89d --- /dev/null +++ b/integrationtests/fixtures/snapshots/rust/hover/constant.snap @@ -0,0 +1,3 @@ +test_workspace::types + +pub const TEST_CONSTANT: &str = "test constant value" \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/rust/hover/function.snap b/integrationtests/fixtures/snapshots/rust/hover/function.snap new file mode 100644 index 0000000..348ccb3 --- /dev/null +++ b/integrationtests/fixtures/snapshots/rust/hover/function.snap @@ -0,0 +1,3 @@ +test_workspace::types + +pub fn test_function() -> String \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/rust/hover/no-hover-info.snap b/integrationtests/fixtures/snapshots/rust/hover/no-hover-info.snap new file mode 100644 index 0000000..eb22712 --- /dev/null +++ b/integrationtests/fixtures/snapshots/rust/hover/no-hover-info.snap @@ -0,0 +1,2 @@ +No hover information available for this position on the following line: +// Types for testing diff --git a/integrationtests/fixtures/snapshots/rust/hover/outside-file.snap b/integrationtests/fixtures/snapshots/rust/hover/outside-file.snap new file mode 100644 index 0000000..14ac8de --- /dev/null +++ b/integrationtests/fixtures/snapshots/rust/hover/outside-file.snap @@ -0,0 +1 @@ +failed to get hover information: request failed: Invalid offset LineCol { line: 999, col: 0 } (line index length: 1645) (code: -32603) \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/rust/hover/struct-method.snap b/integrationtests/fixtures/snapshots/rust/hover/struct-method.snap new file mode 100644 index 0000000..f6e6391 --- /dev/null +++ b/integrationtests/fixtures/snapshots/rust/hover/struct-method.snap @@ -0,0 +1,3 @@ +test_workspace::types::TestStruct + +pub fn method(&self) -> String \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/rust/hover/struct-type.snap b/integrationtests/fixtures/snapshots/rust/hover/struct-type.snap new file mode 100644 index 0000000..ba52117 --- /dev/null +++ b/integrationtests/fixtures/snapshots/rust/hover/struct-type.snap @@ -0,0 +1,9 @@ +test_workspace::types + +pub struct TestStruct { + pub name: String, + pub value: i32, +} + + +size = 32 (0x20), align = 0x8 \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/rust/hover/trait-type.snap b/integrationtests/fixtures/snapshots/rust/hover/trait-type.snap new file mode 100644 index 0000000..80792c7 --- /dev/null +++ b/integrationtests/fixtures/snapshots/rust/hover/trait-type.snap @@ -0,0 +1,6 @@ +test_workspace::types + +pub trait TestInterface + + +Is dyn-compatible \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/rust/hover/type-alias.snap b/integrationtests/fixtures/snapshots/rust/hover/type-alias.snap new file mode 100644 index 0000000..bf05b4e --- /dev/null +++ b/integrationtests/fixtures/snapshots/rust/hover/type-alias.snap @@ -0,0 +1,4 @@ +test_workspace::types + +// size = 24 (0x18), align = 0x8 +pub type TestType = String \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/rust/hover/variable.snap b/integrationtests/fixtures/snapshots/rust/hover/variable.snap new file mode 100644 index 0000000..c4cdfe5 --- /dev/null +++ b/integrationtests/fixtures/snapshots/rust/hover/variable.snap @@ -0,0 +1,3 @@ +test_workspace::types + +pub static TEST_VARIABLE: &str = "test variable value" \ No newline at end of file diff --git a/integrationtests/fixtures/snapshots/typescript/hover/class-method.snap b/integrationtests/fixtures/snapshots/typescript/hover/class-method.snap new file mode 100644 index 0000000..dc3d952 --- /dev/null +++ b/integrationtests/fixtures/snapshots/typescript/hover/class-method.snap @@ -0,0 +1,4 @@ + +```typescript +(method) TestClass.method(): void +``` diff --git a/integrationtests/fixtures/snapshots/typescript/hover/class.snap b/integrationtests/fixtures/snapshots/typescript/hover/class.snap new file mode 100644 index 0000000..195a5a2 --- /dev/null +++ b/integrationtests/fixtures/snapshots/typescript/hover/class.snap @@ -0,0 +1,4 @@ + +```typescript +class TestClass +``` diff --git a/integrationtests/fixtures/snapshots/typescript/hover/constant.snap b/integrationtests/fixtures/snapshots/typescript/hover/constant.snap new file mode 100644 index 0000000..1b39f0b --- /dev/null +++ b/integrationtests/fixtures/snapshots/typescript/hover/constant.snap @@ -0,0 +1,4 @@ + +```typescript +const TestConstant: 42 +``` diff --git a/integrationtests/fixtures/snapshots/typescript/hover/function.snap b/integrationtests/fixtures/snapshots/typescript/hover/function.snap new file mode 100644 index 0000000..eddd413 --- /dev/null +++ b/integrationtests/fixtures/snapshots/typescript/hover/function.snap @@ -0,0 +1,4 @@ + +```typescript +function TestFunction(): string +``` diff --git a/integrationtests/fixtures/snapshots/typescript/hover/interface-type.snap b/integrationtests/fixtures/snapshots/typescript/hover/interface-type.snap new file mode 100644 index 0000000..0fecb7e --- /dev/null +++ b/integrationtests/fixtures/snapshots/typescript/hover/interface-type.snap @@ -0,0 +1,4 @@ + +```typescript +interface TestInterface +``` diff --git a/integrationtests/fixtures/snapshots/typescript/hover/no-hover-info.snap b/integrationtests/fixtures/snapshots/typescript/hover/no-hover-info.snap new file mode 100644 index 0000000..7facdf5 --- /dev/null +++ b/integrationtests/fixtures/snapshots/typescript/hover/no-hover-info.snap @@ -0,0 +1,2 @@ +No hover information available for this position on the following line: +// TestInterface definition diff --git a/integrationtests/fixtures/snapshots/typescript/hover/outside-file.snap b/integrationtests/fixtures/snapshots/typescript/hover/outside-file.snap new file mode 100644 index 0000000..f175581 --- /dev/null +++ b/integrationtests/fixtures/snapshots/typescript/hover/outside-file.snap @@ -0,0 +1 @@ +No hover information available for this position on the following line: diff --git a/integrationtests/fixtures/snapshots/typescript/hover/type.snap b/integrationtests/fixtures/snapshots/typescript/hover/type.snap new file mode 100644 index 0000000..092c3fe --- /dev/null +++ b/integrationtests/fixtures/snapshots/typescript/hover/type.snap @@ -0,0 +1,4 @@ + +```typescript +type TestType = string | number +``` diff --git a/integrationtests/fixtures/snapshots/typescript/hover/variable.snap b/integrationtests/fixtures/snapshots/typescript/hover/variable.snap new file mode 100644 index 0000000..59770f5 --- /dev/null +++ b/integrationtests/fixtures/snapshots/typescript/hover/variable.snap @@ -0,0 +1,4 @@ + +```typescript +const TestVariable: string +``` diff --git a/integrationtests/languages/go/hover/hover_test.go b/integrationtests/languages/go/hover/hover_test.go new file mode 100644 index 0000000..d9b063e --- /dev/null +++ b/integrationtests/languages/go/hover/hover_test.go @@ -0,0 +1,126 @@ +package hover_test + +import ( + "context" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/isaacphi/mcp-language-server/integrationtests/languages/common" + "github.com/isaacphi/mcp-language-server/integrationtests/languages/go/internal" + "github.com/isaacphi/mcp-language-server/internal/tools" +) + +// TestHover tests hover functionality with the Go 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.go file + { + name: "StructType", + file: "types.go", + line: 6, + column: 6, + expectedText: "SharedStruct", + snapshotName: "struct-type", + }, + { + name: "StructMethod", + file: "types.go", + line: 14, + column: 18, + expectedText: "Method", + snapshotName: "struct-method", + }, + { + name: "InterfaceType", + file: "types.go", + line: 19, + column: 6, + expectedText: "SharedInterface", + snapshotName: "interface-type", + }, + { + name: "Constant", + file: "types.go", + line: 25, + column: 7, + expectedText: "SharedConstant", + snapshotName: "constant", + }, + { + name: "InterfaceMethodImplementation", + file: "types.go", + line: 31, + column: 18, + expectedText: "Process", + snapshotName: "interface-method-impl", + }, + // Test for a location without hover info (empty space) + { + name: "NoHoverInfo", + file: "types.go", + line: 3, // Line with just "import" statement + column: 1, // First column (whitespace) + unexpectedText: "func", + snapshotName: "no-hover-info", + }, + // Test for a location outside the file + { + name: "OutsideFile", + file: "types.go", + line: 1000, // Line number beyond file length + column: 1, + unexpectedText: "func", + 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, 5*time.Second) + defer cancel() + + filePath := filepath.Join(suite.WorkspaceDir, tt.file) + err := suite.Client.OpenFile(ctx, filePath) + if err != nil { + t.Fatalf("Failed to open %s: %v", tt.file, err) + } + + // Get hover info + result, err := tools.GetHoverInfo(ctx, suite.Client, filePath, tt.line, tt.column) + if err != nil { + // For the "OutsideFile" test, we expect an error + if tt.name == "OutsideFile" { + // Create a snapshot even for error case + common.SnapshotTest(t, "go", "hover", tt.snapshotName, err.Error()) + return + } + t.Fatalf("GetHoverInfo failed: %v", err) + } + + // Verify expected content + if tt.expectedText != "" && !strings.Contains(result, tt.expectedText) { + t.Errorf("Expected hover info to contain %q but got: %s", tt.expectedText, result) + } + + // Verify unexpected content is absent + if tt.unexpectedText != "" && strings.Contains(result, tt.unexpectedText) { + t.Errorf("Expected hover info NOT to contain %q but it was found: %s", tt.unexpectedText, result) + } + + common.SnapshotTest(t, "go", "hover", tt.snapshotName, result) + }) + } +} diff --git a/integrationtests/languages/python/hover/hover_test.go b/integrationtests/languages/python/hover/hover_test.go new file mode 100644 index 0000000..d41ca03 --- /dev/null +++ b/integrationtests/languages/python/hover/hover_test.go @@ -0,0 +1,142 @@ +package hover_test + +import ( + "context" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/isaacphi/mcp-language-server/integrationtests/languages/common" + "github.com/isaacphi/mcp-language-server/integrationtests/languages/python/internal" + "github.com/isaacphi/mcp-language-server/internal/tools" +) + +// TestHover tests hover functionality with the Python 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 main.py file + { + name: "Function", + file: "main.py", + line: 6, + column: 5, + expectedText: "test_function", + snapshotName: "function", + }, + { + name: "Class", + file: "main.py", + line: 18, + column: 7, + expectedText: "TestClass", + snapshotName: "class", + }, + { + name: "ClassMethod", + file: "main.py", + line: 31, + column: 10, + expectedText: "test_method", + snapshotName: "class-method", + }, + { + name: "StaticMethod", + file: "main.py", + line: 44, + column: 15, + expectedText: "static_method", + snapshotName: "static-method", + }, + { + name: "Constant", + file: "main.py", + line: 79, + column: 5, + expectedText: "TEST_CONSTANT", + snapshotName: "constant", + }, + { + name: "Variable", + file: "main.py", + line: 83, + column: 5, + expectedText: "test_variable", + snapshotName: "variable", + }, + { + name: "DerivedClass", + file: "main.py", + line: 70, + column: 7, + expectedText: "DerivedClass", + snapshotName: "derived-class", + }, + // Test for a location without hover info (empty space) + { + name: "NoHoverInfo", + file: "main.py", + line: 2, // Blank line + column: 1, // First column + unexpectedText: "class", + snapshotName: "no-hover-info", + }, + // Test for a location outside the file + { + name: "OutsideFile", + file: "main.py", + line: 1000, // Line number beyond file length + column: 1, + unexpectedText: "def", + 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, 5*time.Second) + defer cancel() + + filePath := filepath.Join(suite.WorkspaceDir, tt.file) + err := suite.Client.OpenFile(ctx, filePath) + if err != nil { + t.Fatalf("Failed to open %s: %v", tt.file, err) + } + + // Get hover info + result, err := tools.GetHoverInfo(ctx, suite.Client, filePath, tt.line, tt.column) + if err != nil { + // For the "OutsideFile" test, we expect an error + if tt.name == "OutsideFile" { + // Create a snapshot even for error case + common.SnapshotTest(t, "python", "hover", tt.snapshotName, err.Error()) + return + } + t.Fatalf("GetHoverInfo failed: %v", err) + } + + // Verify expected content + if tt.expectedText != "" && !strings.Contains(result, tt.expectedText) { + t.Errorf("Expected hover info to contain %q but got: %s", tt.expectedText, result) + } + + // Verify unexpected content is absent + if tt.unexpectedText != "" && strings.Contains(result, tt.unexpectedText) { + t.Errorf("Expected hover info NOT to contain %q but it was found: %s", tt.unexpectedText, result) + } + + common.SnapshotTest(t, "python", "hover", tt.snapshotName, result) + }) + } +} diff --git a/integrationtests/languages/rust/hover/hover_test.go b/integrationtests/languages/rust/hover/hover_test.go new file mode 100644 index 0000000..a0af4da --- /dev/null +++ b/integrationtests/languages/rust/hover/hover_test.go @@ -0,0 +1,159 @@ +package hover_test + +import ( + "context" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/isaacphi/mcp-language-server/integrationtests/languages/common" + "github.com/isaacphi/mcp-language-server/integrationtests/languages/rust/internal" + "github.com/isaacphi/mcp-language-server/internal/tools" +) + +// TestHover tests hover functionality with the Rust 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 all files to ensure rust-analyzer indexes everything + filesToOpen := []string{ + "src/main.rs", + "src/types.rs", + "src/helper.rs", + "src/consumer.rs", + "src/another_consumer.rs", + "src/clean.rs", + } + + 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) + } + } + } + + 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.rs file + { + name: "Struct", + file: "src/types.rs", + line: 13, + column: 12, + expectedText: "TestStruct", + snapshotName: "struct-type", + }, + { + name: "StructMethod", + file: "src/types.rs", + line: 27, + column: 15, + expectedText: "method", + snapshotName: "struct-method", + }, + { + name: "Trait", + file: "src/types.rs", + line: 33, + column: 12, + expectedText: "TestInterface", + snapshotName: "trait-type", + }, + { + name: "Constant", + file: "src/types.rs", + line: 4, + column: 12, + expectedText: "TEST_CONSTANT", + snapshotName: "constant", + }, + { + name: "Variable", + file: "src/types.rs", + line: 7, + column: 12, + expectedText: "TEST_VARIABLE", + snapshotName: "variable", + }, + { + name: "Function", + file: "src/types.rs", + line: 81, + column: 8, + expectedText: "test_function", + snapshotName: "function", + }, + // Test for a location without hover info (empty space) + { + name: "NoHoverInfo", + file: "src/types.rs", + line: 1, // Comment line + column: 1, // First column + unexpectedText: "fn", + snapshotName: "no-hover-info", + }, + // Test for a location outside the file + { + name: "OutsideFile", + file: "src/types.rs", + line: 1000, // Line number beyond file length + column: 1, + unexpectedText: "fn", + 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, 5*time.Second) + defer cancel() + + // Open all files and wait for rust-analyzer to index them + openAllFilesAndWait(suite, ctx) + + filePath := filepath.Join(suite.WorkspaceDir, tt.file) + err := suite.Client.OpenFile(ctx, filePath) + if err != nil { + t.Fatalf("Failed to open %s: %v", tt.file, err) + } + + // Get hover info + result, err := tools.GetHoverInfo(ctx, suite.Client, filePath, tt.line, tt.column) + if err != nil { + // For the "OutsideFile" test, we expect an error + if tt.name == "OutsideFile" { + // Create a snapshot even for error case + common.SnapshotTest(t, "rust", "hover", tt.snapshotName, err.Error()) + return + } + t.Fatalf("GetHoverInfo failed: %v", err) + } + + // Verify expected content + if tt.expectedText != "" && !strings.Contains(result, tt.expectedText) { + t.Errorf("Expected hover info to contain %q but got: %s", tt.expectedText, result) + } + + // Verify unexpected content is absent + if tt.unexpectedText != "" && strings.Contains(result, tt.unexpectedText) { + t.Errorf("Expected hover info NOT to contain %q but it was found: %s", tt.unexpectedText, result) + } + + common.SnapshotTest(t, "rust", "hover", tt.snapshotName, result) + }) + } +} diff --git a/integrationtests/languages/typescript/hover/hover_test.go b/integrationtests/languages/typescript/hover/hover_test.go new file mode 100644 index 0000000..cc9c8cf --- /dev/null +++ b/integrationtests/languages/typescript/hover/hover_test.go @@ -0,0 +1,142 @@ +package hover_test + +import ( + "context" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/isaacphi/mcp-language-server/integrationtests/languages/common" + "github.com/isaacphi/mcp-language-server/integrationtests/languages/typescript/internal" + "github.com/isaacphi/mcp-language-server/internal/tools" +) + +// TestHover tests hover functionality with the TypeScript 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 main.ts file + { + name: "Function", + file: "main.ts", + line: 2, + column: 17, + expectedText: "TestFunction", + snapshotName: "function", + }, + { + name: "Interface", + file: "main.ts", + line: 8, + column: 18, + expectedText: "TestInterface", + snapshotName: "interface-type", + }, + { + name: "Class", + file: "main.ts", + line: 14, + column: 14, + expectedText: "TestClass", + snapshotName: "class", + }, + { + name: "ClassMethod", + file: "main.ts", + line: 21, + column: 9, + expectedText: "method", + snapshotName: "class-method", + }, + { + name: "Type", + file: "main.ts", + line: 27, + column: 13, + expectedText: "TestType", + snapshotName: "type", + }, + { + name: "Variable", + file: "main.ts", + line: 30, + column: 20, + expectedText: "TestVariable", + snapshotName: "variable", + }, + { + name: "Constant", + file: "main.ts", + line: 33, + column: 20, + expectedText: "TestConstant", + snapshotName: "constant", + }, + // Test for a location without hover info (comment) + { + name: "NoHoverInfo", + file: "main.ts", + line: 7, // Comment line + column: 1, // First column (whitespace) + unexpectedText: "function", + snapshotName: "no-hover-info", + }, + // Test for a location outside the file + { + name: "OutsideFile", + file: "main.ts", + line: 1000, // Line number beyond file length + column: 1, + unexpectedText: "function", + 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, 5*time.Second) + defer cancel() + + filePath := filepath.Join(suite.WorkspaceDir, tt.file) + err := suite.Client.OpenFile(ctx, filePath) + if err != nil { + t.Fatalf("Failed to open %s: %v", tt.file, err) + } + + // Get hover info + result, err := tools.GetHoverInfo(ctx, suite.Client, filePath, tt.line, tt.column) + if err != nil { + // For the "OutsideFile" test, we expect an error + if tt.name == "OutsideFile" { + // Create a snapshot even for error case + common.SnapshotTest(t, "typescript", "hover", tt.snapshotName, err.Error()) + return + } + t.Fatalf("GetHoverInfo failed: %v", err) + } + + // Verify expected content + if tt.expectedText != "" && !strings.Contains(result, tt.expectedText) { + t.Errorf("Expected hover info to contain %q but got: %s", tt.expectedText, result) + } + + // Verify unexpected content is absent + if tt.unexpectedText != "" && strings.Contains(result, tt.unexpectedText) { + t.Errorf("Expected hover info NOT to contain %q but it was found: %s", tt.unexpectedText, result) + } + + common.SnapshotTest(t, "typescript", "hover", tt.snapshotName, result) + }) + } +} diff --git a/integrationtests/workspaces/python/clean.py b/integrationtests/workspaces/python/clean.py index 82c265e..d8aa858 100644 --- a/integrationtests/workspaces/python/clean.py +++ b/integrationtests/workspaces/python/clean.py @@ -1,14 +1,14 @@ """A clean Python module without any errors or warnings.""" -from typing import List, Dict, Optional, Tuple +from typing import Optional, Tuple def clean_function(param: str) -> str: """A clean function without errors. - + Args: param: The input parameter - + Returns: The processed result """ @@ -17,30 +17,30 @@ def clean_function(param: str) -> str: class CleanClass: """A clean class without errors.""" - + def __init__(self, name: str): """Initialize a CleanClass instance. - + Args: name: The name of this instance """ self.name = name - + def get_name(self) -> str: """Get the name of this instance. - + Returns: The name of this instance """ return self.name - + @staticmethod - def utility_method(items: List[int]) -> int: + def utility_method(items: list[int]) -> int: """Calculate the sum of a list of integers. - + Args: items: A list of integers - + Returns: The sum of the integers """ @@ -49,4 +49,4 @@ def utility_method(items: List[int]) -> int: # Clean constants and variables CLEAN_CONSTANT: str = "This is a clean constant" -clean_variable: List[int] = [10, 20, 30, 40, 50] \ No newline at end of file +clean_variable: list[int] = [10, 20, 30, 40, 50] diff --git a/integrationtests/workspaces/python/consumer.py b/integrationtests/workspaces/python/consumer.py index 75a00b4..1397056 100644 --- a/integrationtests/workspaces/python/consumer.py +++ b/integrationtests/workspaces/python/consumer.py @@ -1,27 +1,26 @@ """Consumer module that uses the helper module.""" -from typing import List, Dict from helper import ( - helper_function, - get_items, - SharedClass, - SharedInterface, + helper_function, + get_items, + SharedClass, + SharedInterface, SHARED_CONSTANT, - Color + Color, ) class MyImplementation(SharedInterface): """An implementation of the SharedInterface.""" - - def process(self, data: List[str]) -> Dict[str, int]: + + def process(self, data: list[str]) -> dict[str, int]: """Process the given data by counting occurrences. - + Args: - data: List of strings to process - + data: list of strings to process + Returns: - Dictionary with counts of each item + dictionary with counts of each item """ result = {} for item in data: @@ -37,21 +36,21 @@ def consumer_function() -> None: # Use the helper function message = helper_function("World") print(message) - + # Get and process items from the helper items = get_items() for item in items: print(f"Processing {item}") - + # Use the shared class shared = SharedClass[str]("consumer", SHARED_CONSTANT) print(f"Using shared class: {shared.get_name()} - {shared.get_value()}") - + # Use our implementation of the shared interface impl = MyImplementation() result = impl.process(items) print(f"Processed items: {result}") - + # Use the enum color = Color.RED print(f"Selected color: {color}") @@ -61,11 +60,11 @@ def process_data() -> None: """Process some sample data.""" data = get_items() print(f"Found {len(data)} items") - + # Sort and display the data sorted_data = sorted(data) print(f"Sorted data: {sorted_data}") - + # Count the items counts = {} for item in data: @@ -73,10 +72,11 @@ def process_data() -> None: counts[item] += 1 else: counts[item] = 1 - + print(f"Item counts: {counts}") - + if __name__ == "__main__": consumer_function() - process_data() \ No newline at end of file + process_data() + diff --git a/integrationtests/workspaces/python/consumer_clean.py b/integrationtests/workspaces/python/consumer_clean.py index 5822005..205d710 100644 --- a/integrationtests/workspaces/python/consumer_clean.py +++ b/integrationtests/workspaces/python/consumer_clean.py @@ -1,6 +1,5 @@ """Consumer module that uses the helper module.""" -from typing import List from helper import helper_function, get_items @@ -9,7 +8,7 @@ def consumer_function() -> None: # Use the helper function message = helper_function("World") print(message) - + # Get and process items from the helper items = get_items() for item in items: @@ -20,11 +19,11 @@ def process_data() -> None: """Process some sample data.""" data = get_items() print(f"Found {len(data)} items") - + # Sort and display the data sorted_data = sorted(data) print(f"Sorted data: {sorted_data}") - + # Count the items counts = {} for item in data: @@ -32,10 +31,10 @@ def process_data() -> None: counts[item] += 1 else: counts[item] = 1 - + print(f"Item counts: {counts}") - + if __name__ == "__main__": consumer_function() - process_data() \ No newline at end of file + process_data() diff --git a/integrationtests/workspaces/python/error_file.py b/integrationtests/workspaces/python/error_file.py index 689042e..7edf429 100644 --- a/integrationtests/workspaces/python/error_file.py +++ b/integrationtests/workspaces/python/error_file.py @@ -1,14 +1,14 @@ """A Python module with deliberate errors for testing diagnostics.""" -from typing import List, Dict, Any +from typing import Any def function_with_unreachable_code(value: int) -> str: """A function with unreachable code. - + Args: value: An integer value - + Returns: A string result """ @@ -24,7 +24,7 @@ def function_with_unreachable_code(value: int) -> str: def function_with_type_error() -> str: """A function with a type error. - + Returns: Should return a string but actually returns an int """ @@ -33,19 +33,19 @@ def function_with_type_error() -> str: class ErrorClass: """A class with errors.""" - - def __init__(self, value: Dict[str, Any]): + + def __init__(self, value: dict[str, Any]): """Initialize with errors. - + Args: value: A dictionary """ self.value = value - + def method_with_undefined_variable(self) -> None: """A method that uses an undefined variable.""" print(undefined_variable) # Error: undefined_variable is not defined # Variable with incompatible type annotation -wrong_type: str = 123 # Type error: Incompatible types in assignment \ No newline at end of file +wrong_type: str = 123 # Type error: Incompatible types in assignment diff --git a/integrationtests/workspaces/python/helper.py b/integrationtests/workspaces/python/helper.py index 07675e5..97d6a1e 100644 --- a/integrationtests/workspaces/python/helper.py +++ b/integrationtests/workspaces/python/helper.py @@ -1,6 +1,6 @@ """Helper module that provides utility functions.""" -from typing import List, Dict, TypeVar, Generic +from typing import TypeVar, Generic from enum import Enum @@ -11,40 +11,41 @@ # Enum-like class that will be referenced across files class Color(Enum): """Color enumeration used across files.""" + RED = "red" GREEN = "green" BLUE = "blue" # Generic type variable for SharedClass -T = TypeVar('T') +T = TypeVar("T") # Shared class that will be referenced across files class SharedClass(Generic[T]): """A shared class that is used across multiple files.""" - + def __init__(self, name: str, value: T): """Initialize with a name and value. - + Args: name: The name of this instance value: The value to store """ self.name = name self.value = value - + def get_name(self) -> str: """Get the name of this instance. - + Returns: The name string """ return self.name - + def get_value(self) -> T: """Get the stored value. - + Returns: The stored value """ @@ -54,35 +55,35 @@ def get_value(self) -> T: # Interface-like class (abstract base class in Python) class SharedInterface: """An interface-like class that defines a contract.""" - - def process(self, data: List[str]) -> Dict[str, int]: + + def process(self, data: list[str]) -> dict[str, int]: """Process the given data. - + Args: - data: List of strings to process - + data: list of strings to process + Returns: - Dictionary with processing results + dictionary with processing results """ raise NotImplementedError("Implementations must override process") def helper_function(name: str) -> str: """A helper function that formats a greeting message. - + Args: name: The name to greet - + Returns: A formatted greeting message """ return f"Hello, {name}!" -def get_items() -> List[str]: +def get_items() -> list[str]: """Get a list of sample items. - + Returns: A list of sample strings """ - return ["apple", "banana", "orange", "grape"] \ No newline at end of file + return ["apple", "banana", "orange", "grape"] diff --git a/integrationtests/workspaces/python/main.py b/integrationtests/workspaces/python/main.py index 8a2433b..ddf3601 100644 --- a/integrationtests/workspaces/python/main.py +++ b/integrationtests/workspaces/python/main.py @@ -1,6 +1,6 @@ """Module containing test definitions for Python LSP integration tests.""" -from typing import List, Dict, Optional, Union +from typing import dict, Optional, Union def test_function(name: str) -> str: @@ -41,7 +41,7 @@ def test_method(self, increment: int) -> int: return self.value @staticmethod - def static_method(items: List[str]) -> Dict[str, int]: + def static_method(items: list[str]) -> dict[str, int]: """Convert a list of items to a dictionary with item counts. Args: @@ -50,7 +50,7 @@ def static_method(items: List[str]) -> Dict[str, int]: Returns: A dictionary mapping items to their counts """ - result: Dict[str, int] = {} + result: dict[str, int] = {} for item in items: if item in result: result[item] += 1 @@ -80,7 +80,7 @@ def derived_method(self) -> None: PI: float = 3.14159 # Variables -test_variable: List[int] = [1, 2, 3, 4, 5] +test_variable: list[int] = [1, 2, 3, 4, 5] optional_var: Optional[str] = None union_var: Union[int, str] = "test" @@ -103,4 +103,3 @@ def main() -> None: if __name__ == "__main__": main() - diff --git a/integrationtests/workspaces/rust/src/types.rs b/integrationtests/workspaces/rust/src/types.rs index 6d099a6..ed3bd5d 100644 --- a/integrationtests/workspaces/rust/src/types.rs +++ b/integrationtests/workspaces/rust/src/types.rs @@ -80,4 +80,4 @@ pub const SHARED_CONSTANT: &str = "shared constant value"; // A simple function for testing pub fn test_function() -> String { String::from("test function") -} \ No newline at end of file +} diff --git a/internal/tools/hover.go b/internal/tools/hover.go new file mode 100644 index 0000000..874d527 --- /dev/null +++ b/internal/tools/hover.go @@ -0,0 +1,66 @@ +package tools + +import ( + "context" + "fmt" + "strings" + + "github.com/isaacphi/mcp-language-server/internal/lsp" + "github.com/isaacphi/mcp-language-server/internal/protocol" +) + +// GetHoverInfo retrieves hover information (type, documentation) for a symbol at the specified position +func GetHoverInfo(ctx context.Context, client *lsp.Client, filePath string, line, column int) (string, error) { + // Open the file if not already open + err := client.OpenFile(ctx, filePath) + if err != nil { + return "", fmt.Errorf("could not open file: %v", err) + } + + params := protocol.HoverParams{} + + // Convert 1-indexed line/column to 0-indexed for LSP protocol + position := protocol.Position{ + Line: uint32(line - 1), + Character: uint32(column - 1), + } + uri := protocol.DocumentUri("file://" + filePath) + params.TextDocument = protocol.TextDocumentIdentifier{ + URI: uri, + } + params.Position = position + + // Execute the hover request + hoverResult, err := client.Hover(ctx, params) + if err != nil { + return "", fmt.Errorf("failed to get hover information: %v", err) + } + + var result strings.Builder + + // Process the hover contents based on Markup content + if hoverResult.Contents.Value == "" { + // Extract the line where the hover was requested + lineText, err := ExtractTextFromLocation(protocol.Location{ + URI: uri, + Range: protocol.Range{ + Start: protocol.Position{ + Line: position.Line, + Character: 0, + }, + End: protocol.Position{ + Line: position.Line + 1, + Character: 0, + }, + }, + }) + if err != nil { + toolsLogger.Warn("failed to extract line at position: %v", err) + } + result.WriteString(fmt.Sprintf("No hover information available for this position on the following line:\n%s", lineText)) + } else { + result.WriteString(hoverResult.Contents.Value) + } + + return result.String(), nil +} diff --git a/tools.go b/tools.go index 913a39b..a345de0 100644 --- a/tools.go +++ b/tools.go @@ -38,6 +38,12 @@ type ExecuteCodeLensArgs struct { Index int `json:"index" jsonschema:"required,description=The index of the code lens to execute (from get_codelens output), 1 indexed"` } +type GetHoverArgs struct { + FilePath string `json:"filePath" jsonschema:"required,description=The path to the file to get hover information for"` + Line int `json:"line" jsonschema:"required,description=The line number where the hover is requested (1-indexed)"` + Column int `json:"column" jsonschema:"required,description=The column number where the hover is requested (1-indexed)"` +} + func (s *mcpServer) registerTools() error { coreLogger.Debug("Registering MCP tools") @@ -67,14 +73,14 @@ func (s *mcpServer) registerTools() error { } // Type assert and convert the edits - editsArray, ok := editsArg.([]interface{}) + editsArray, ok := editsArg.([]any) if !ok { return mcp.NewToolResultError("edits must be an array"), nil } var edits []tools.TextEdit for _, editItem := range editsArray { - editMap, ok := editItem.(map[string]interface{}) + editMap, ok := editItem.(map[string]any) if !ok { return mcp.NewToolResultError("each edit must be an object"), nil } @@ -278,6 +284,58 @@ func (s *mcpServer) registerTools() error { return mcp.NewToolResultText(text), nil }) + hoverTool := mcp.NewTool("hover", + mcp.WithDescription("Get hover information (type, documentation) for a symbol at the specified position."), + mcp.WithString("filePath", + mcp.Required(), + mcp.Description("The path to the file to get hover information for"), + ), + mcp.WithNumber("line", + mcp.Required(), + mcp.Description("The line number where the hover is requested (1-indexed)"), + ), + mcp.WithNumber("column", + mcp.Required(), + mcp.Description("The column number where the hover is requested (1-indexed)"), + ), + ) + + s.mcpServer.AddTool(hoverTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Extract arguments + filePath, ok := request.Params.Arguments["filePath"].(string) + if !ok { + return mcp.NewToolResultError("filePath must be a string"), nil + } + + // Handle both float64 and int for line and column due to JSON parsing + var line, column int + switch v := request.Params.Arguments["line"].(type) { + case float64: + line = int(v) + case int: + line = v + default: + return mcp.NewToolResultError("line must be a number"), nil + } + + switch v := request.Params.Arguments["column"].(type) { + case float64: + column = int(v) + case int: + column = v + default: + return mcp.NewToolResultError("column must be a number"), nil + } + + coreLogger.Debug("Executing hover for file: %s line: %d column: %d", filePath, line, column) + text, err := tools.GetHoverInfo(s.ctx, s.lspClient, filePath, line, column) + if err != nil { + coreLogger.Error("Failed to get hover information: %v", err) + return mcp.NewToolResultError(fmt.Sprintf("failed to get hover information: %v", err)), nil + } + return mcp.NewToolResultText(text), nil + }) + coreLogger.Info("Successfully registered all MCP tools") return nil }