diff --git a/src/lsap/capability/__init__.py b/src/lsap/capability/__init__.py index 3613e75..205bb67 100644 --- a/src/lsap/capability/__init__.py +++ b/src/lsap/capability/__init__.py @@ -1,20 +1,22 @@ from typing import TypedDict from .definition import DefinitionCapability -from .inspect import InspectCapability +from .doc import DocCapability from .locate import LocateCapability from .outline import OutlineCapability from .reference import ReferenceCapability from .rename import RenameExecuteCapability, RenamePreviewCapability from .search import SearchCapability +from .symbol import SymbolCapability class Capabilities(TypedDict): definition: DefinitionCapability + doc: DocCapability locate: LocateCapability outline: OutlineCapability references: ReferenceCapability rename_preview: RenamePreviewCapability rename_execute: RenameExecuteCapability search: SearchCapability - inspect: InspectCapability + symbol: SymbolCapability diff --git a/src/lsap/capability/doc.py b/src/lsap/capability/doc.py new file mode 100644 index 0000000..35959de --- /dev/null +++ b/src/lsap/capability/doc.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from functools import cached_property + +from attrs import define +from lsp_client.capability.request import WithRequestHover + +from lsap.schema.doc import DocRequest, DocResponse +from lsap.utils.capability import ensure_capability + +from .abc import Capability +from .locate import LocateCapability + + +@define +class DocCapability(Capability[DocRequest, DocResponse]): + @cached_property + def locate(self) -> LocateCapability: + return LocateCapability(self.client) + + async def __call__(self, req: DocRequest) -> DocResponse | None: + if not (loc_resp := await self.locate(req)): + return None + + file_path, lsp_pos = loc_resp.file_path, loc_resp.position.to_lsp() + hover = await ensure_capability(self.client, WithRequestHover).request_hover( + file_path, lsp_pos + ) + + if hover is None: + return None + + return DocResponse(content=hover.value) diff --git a/src/lsap/capability/symbol.py b/src/lsap/capability/symbol.py new file mode 100644 index 0000000..1495352 --- /dev/null +++ b/src/lsap/capability/symbol.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from functools import cached_property +from pathlib import Path +from typing import override + +from attrs import define +from lsp_client.capability.request import WithRequestDocumentSymbol +from lsprotocol.types import Position as LSPPosition + +from lsap.schema.models import Position, Range, SymbolCodeInfo, SymbolKind +from lsap.schema.symbol import SymbolRequest, SymbolResponse +from lsap.utils.capability import ensure_capability +from lsap.utils.document import DocumentReader +from lsap.utils.symbol import symbol_at + +from .abc import Capability +from .locate import LocateCapability +from .outline import OutlineCapability + + +@define +class SymbolCapability(Capability[SymbolRequest, SymbolResponse]): + @cached_property + def locate(self) -> LocateCapability: + return LocateCapability(self.client) + + @cached_property + def outline(self) -> OutlineCapability: + return OutlineCapability(self.client) + + @override + async def __call__(self, req: SymbolRequest) -> SymbolResponse | None: + location = await self.locate(req) + if not location: + return None + + best_match = await self.resolve( + location.file_path, + location.position.to_lsp(), + ) + + if not best_match: + return None + + return SymbolResponse(**best_match.model_dump()) + + async def resolve( + self, + file_path: Path, + pos: LSPPosition, + ) -> SymbolCodeInfo | None: + symbols = await ensure_capability( + self.client, WithRequestDocumentSymbol + ).request_document_symbol_list(file_path) + if not symbols: + return None + + match = symbol_at(symbols, pos) + if not match: + return None + + path, symbol = match + document = await self.client.read_file(file_path) + reader = DocumentReader(document) + + code: str | None = None + if snippet := reader.read(symbol.range): + code = snippet.content + + return SymbolCodeInfo( + file_path=file_path, + name=symbol.name, + path=path, + kind=SymbolKind.from_lsp(symbol.kind), + code=code, + range=Range( + start=Position.from_lsp(symbol.range.start), + end=Position.from_lsp(symbol.range.end), + ), + ) diff --git a/src/lsap/schema/doc.py b/src/lsap/schema/doc.py new file mode 100644 index 0000000..9572ae8 --- /dev/null +++ b/src/lsap/schema/doc.py @@ -0,0 +1,103 @@ +""" +# Doc API + +The Doc API provides quick access to documentation, type information, or other +relevant metadata for a symbol at a specific location. It's useful for getting +context without navigating to the definition. + +## Example Usage + +### Scenario 1: Getting documentation for a function + +Request: + +```json +{ + "locate": { + "file_path": "src/utils.py", + "find": "def calculate" + } +} +``` + +### Scenario 2: Getting type information for a variable + +Request: + +```json +{ + "locate": { + "file_path": "src/main.py", + "find": "config" + } +} +``` + +### Scenario 3: Getting documentation for a class method + +Request: + +```json +{ + "locate": { + "file_path": "src/models/user.py", + "scope": { + "symbol_path": ["User", "save"] + } + } +} +``` + +### Scenario 4: Getting documentation for an imported module + +Request: + +```json +{ + "locate": { + "file_path": "src/main.py", + "find": "import numpy" + } +} +``` +""" + +from typing import Final + +from pydantic import ConfigDict + +from ._abc import Response +from .locate import LocateRequest + + +class DocRequest(LocateRequest): + """ + Retrieves documentation or type information for a symbol at a specific location. + + Use this to quickly see the documentation, type signature, or other relevant + information for a symbol without jumping to its definition. + """ + + +markdown_template: Final = """ +# Doc Information + +{{ content }} +""" + + +class DocResponse(Response): + content: str + """The documentation content, usually markdown.""" + + model_config = ConfigDict( + json_schema_extra={ + "markdown": markdown_template, + } + ) + + +__all__ = [ + "DocRequest", + "DocResponse", +] diff --git a/src/lsap/schema/symbol.py b/src/lsap/schema/symbol.py new file mode 100644 index 0000000..540700a --- /dev/null +++ b/src/lsap/schema/symbol.py @@ -0,0 +1,82 @@ +""" +# Symbol API + +The Symbol API provides detailed information about a specific code symbol, +including its source code and documentation. It is the primary way for an +Agent to understand the implementation and usage of a function, class, or variable. + +## Example Usage + +### Scenario 1: Getting function documentation and implementation + +Request: + +```json +{ + "locate": { + "file_path": "src/main.py", + "scope": { + "symbol_path": ["calculate_total"] + } + } +} +``` + +### Scenario 2: Getting class information + +Request: + +```json +{ + "locate": { + "file_path": "src/models.py", + "scope": { + "symbol_path": ["User"] + } + } +} +``` +""" + +from typing import Final + +from pydantic import ConfigDict + +from ._abc import Response +from .locate import LocateRequest +from .models import SymbolCodeInfo + + +class SymbolRequest(LocateRequest): + """ + Retrieves detailed information about a symbol at a specific location. + + Use this to get the documentation (hover) and source code implementation + of a symbol to understand its purpose and usage. + """ + + +markdown_template: Final = """ +# Symbol: `{{ path | join: "." }}` (`{{ kind }}`) at `{{ file_path }}` + +{% if code != nil -%} +## Implementation +```{{ file_path.suffix | remove_first: "." }} +{{ code }} +``` +{%- endif %} +""" + + +class SymbolResponse(SymbolCodeInfo, Response): + model_config = ConfigDict( + json_schema_extra={ + "markdown": markdown_template, + } + ) + + +__all__ = [ + "SymbolRequest", + "SymbolResponse", +]