Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/lsap/capability/__init__.py
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions src/lsap/capability/doc.py
Original file line number Diff line number Diff line change
@@ -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)
81 changes: 81 additions & 0 deletions src/lsap/capability/symbol.py
Original file line number Diff line number Diff line change
@@ -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),
),
)
103 changes: 103 additions & 0 deletions src/lsap/schema/doc.py
Original file line number Diff line number Diff line change
@@ -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",
]
82 changes: 82 additions & 0 deletions src/lsap/schema/symbol.py
Original file line number Diff line number Diff line change
@@ -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",
]