Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 6 additions & 2 deletions src/lsap/capability/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
from functools import cached_property
from typing import override

import anyio
import asyncer
from attrs import define
from attrs import define, field
from lsp_client.capability.request import (
WithRequestDeclaration,
WithRequestDefinition,
Expand All @@ -24,6 +25,8 @@

@define
class DefinitionCapability(Capability[DefinitionRequest, DefinitionResponse]):
resolve_sem: anyio.Semaphore = field(default=anyio.Semaphore(32))

@cached_property
def locate(self) -> LocateCapability:
return LocateCapability(self.client)
Expand Down Expand Up @@ -69,7 +72,8 @@ async def __call__(self, req: DefinitionRequest) -> DefinitionResponse | None:
async with asyncer.create_task_group() as tg:

async def resolve_item(loc: Location) -> SymbolCodeInfo | None:
target_file_path = self.client.from_uri(loc.uri)
async with self.resolve_sem:
target_file_path = self.client.from_uri(loc.uri)
Comment on lines +75 to +76
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semaphore only protects a synchronous URI-to-path conversion (line 76), while the actual async work (resolving symbol information on lines 77-80) happens outside the semaphore's protection. This defeats the purpose of limiting concurrency. The semaphore should wrap the entire function body to properly limit concurrent execution of this method.

Copilot uses AI. Check for mistakes.
if symbol_info := await self.symbol.resolve(
target_file_path,
loc.range.start,
Expand Down
14 changes: 9 additions & 5 deletions src/lsap/capability/outline.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
from pathlib import Path
from typing import override

import anyio
import asyncer
from attrs import define
from attrs import define, field
from lsp_client.capability.request import WithRequestDocumentSymbol, WithRequestHover
from lsprotocol.types import DocumentSymbol
from lsprotocol.types import Position as LSPPosition
Expand All @@ -22,6 +23,8 @@

@define
class OutlineCapability(Capability[OutlineRequest, OutlineResponse]):
hover_sem: anyio.Semaphore = field(default=anyio.Semaphore(32))

@override
async def __call__(self, req: OutlineRequest) -> OutlineResponse | None:
symbols = await ensure_capability(
Expand Down Expand Up @@ -117,7 +120,8 @@ def _make_item(
)

async def _fill_hover(self, item: SymbolDetailInfo, pos: LSPPosition) -> None:
if hover := await ensure_capability(
self.client, WithRequestHover
).request_hover(item.file_path, pos):
item.hover = clean_hover_content(hover.value)
async with self.hover_sem:
if hover := await ensure_capability(
self.client, WithRequestHover
).request_hover(item.file_path, pos):
item.hover = clean_hover_content(hover.value)
7 changes: 5 additions & 2 deletions src/lsap/capability/reference.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from functools import cached_property

import anyio
import asyncer
from attrs import Factory, define
from attrs import Factory, define, field
from lsp_client.capability.request import (
WithRequestDocumentSymbol,
WithRequestHover,
Expand Down Expand Up @@ -29,6 +30,7 @@
@define
class ReferenceCapability(Capability[ReferenceRequest, ReferenceResponse]):
_cache: PaginationCache[ReferenceItem] = Factory(PaginationCache)
process_sem: anyio.Semaphore = field(default=anyio.Semaphore(32))

@cached_property
def locate(self) -> LocateCapability:
Expand Down Expand Up @@ -92,7 +94,8 @@ async def _process_reference(
context_lines: int,
items: list[ReferenceItem],
) -> None:
file_path = self.client.from_uri(loc.uri)
async with self.process_sem:
file_path = self.client.from_uri(loc.uri)
Comment on lines +97 to +98
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semaphore only protects a synchronous URI-to-path conversion (line 98), while the actual I/O operation (reading the file on line 99) and all subsequent processing (lines 100-149) happen outside the semaphore's protection. This defeats the purpose of limiting concurrency. The semaphore should wrap the entire function body to properly limit concurrent execution of this method.

Copilot uses AI. Check for mistakes.
content = await self.client.read_file(file_path)
reader = DocumentReader(content)

Expand Down
29 changes: 18 additions & 11 deletions src/lsap/capability/rename.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
from pathlib import Path
from typing import override

import anyio
import asyncer
from attrs import define
from attrs import define, field
from lsp_client.capability.request import WithRequestRename
from lsp_client.protocol import CapabilityClientProtocol
from lsp_client.utils.types import lsp_type
Expand Down Expand Up @@ -141,6 +142,8 @@ def should_exclude(uri: str) -> bool:

@define
class RenamePreviewCapability(Capability[RenamePreviewRequest, RenamePreviewResponse]):
file_sem: anyio.Semaphore = field(default=anyio.Semaphore(32))

@cached_property
def locate(self) -> LocateCapability:
return LocateCapability(self.client)
Expand Down Expand Up @@ -208,11 +211,12 @@ async def _to_file_change(
*,
reader: DocumentReader | None = None,
) -> RenameFileChange | None:
if reader is None:
content = await self.client.read_file(
self.client.from_uri(uri, relative=False)
)
reader = DocumentReader(content)
async with self.file_sem:
if reader is None:
content = await self.client.read_file(
self.client.from_uri(uri, relative=False)
)
reader = DocumentReader(content)
Comment on lines +214 to +219
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semaphore only protects the file reading operation but releases before processing the edits (lines 221-244). This means that all the subsequent work (creating diffs, processing edits) happens outside the semaphore's protection, which defeats the purpose of limiting concurrency. The semaphore should wrap the entire function body to properly limit concurrent execution of this method.

Copilot uses AI. Check for mistakes.

diffs: list[RenameDiff] = []
for edit in edits:
Expand Down Expand Up @@ -246,6 +250,8 @@ async def _to_file_change(

@define
class RenameExecuteCapability(Capability[RenameExecuteRequest, RenameExecuteResponse]):
file_sem: anyio.Semaphore = field(default=anyio.Semaphore(32))

@override
async def __call__(self, req: RenameExecuteRequest) -> RenameExecuteResponse | None:
cached = _preview_cache.get(req.rename_id)
Expand Down Expand Up @@ -301,11 +307,12 @@ async def _to_file_change(
*,
reader: DocumentReader | None = None,
) -> RenameFileChange | None:
if reader is None:
content = await self.client.read_file(
self.client.from_uri(uri, relative=False)
)
reader = DocumentReader(content)
async with self.file_sem:
if reader is None:
content = await self.client.read_file(
self.client.from_uri(uri, relative=False)
)
reader = DocumentReader(content)
Comment on lines +314 to +319
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semaphore only protects the file reading operation but releases before processing the edits (lines 317-340). This means that all the subsequent work (creating diffs, processing edits) happens outside the semaphore's protection, which defeats the purpose of limiting concurrency. The semaphore should wrap the entire function body to properly limit concurrent execution of this method.

Copilot uses AI. Check for mistakes.

diffs: list[RenameDiff] = []
for edit in edits:
Expand Down
Loading