Skip to content

Commit e137a6e

Browse files
authored
feat: add semaphore to limit concurrency in capability requests (#23)
* feat: add semaphore to limit concurrency in capability requests * refactor: expand semaphore scope to cover entire processing blocks
1 parent 21e183b commit e137a6e

File tree

4 files changed

+151
-125
lines changed

4 files changed

+151
-125
lines changed

src/lsap/capability/definition.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
from functools import cached_property
55
from typing import override
66

7+
import anyio
78
import asyncer
8-
from attrs import define
9+
from attrs import define, field
910
from lsp_client.capability.request import (
1011
WithRequestDeclaration,
1112
WithRequestDefinition,
@@ -24,6 +25,8 @@
2425

2526
@define
2627
class DefinitionCapability(Capability[DefinitionRequest, DefinitionResponse]):
28+
resolve_sem: anyio.Semaphore = field(default=anyio.Semaphore(32))
29+
2730
@cached_property
2831
def locate(self) -> LocateCapability:
2932
return LocateCapability(self.client)
@@ -69,12 +72,13 @@ async def __call__(self, req: DefinitionRequest) -> DefinitionResponse | None:
6972
async with asyncer.create_task_group() as tg:
7073

7174
async def resolve_item(loc: Location) -> SymbolCodeInfo | None:
72-
target_file_path = self.client.from_uri(loc.uri)
73-
if symbol_info := await self.symbol.resolve(
74-
target_file_path,
75-
loc.range.start,
76-
):
77-
return symbol_info
75+
async with self.resolve_sem:
76+
target_file_path = self.client.from_uri(loc.uri)
77+
if symbol_info := await self.symbol.resolve(
78+
target_file_path,
79+
loc.range.start,
80+
):
81+
return symbol_info
7882
return None
7983

8084
infos = [tg.soonify(resolve_item)(loc) for loc in locations]

src/lsap/capability/outline.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
from pathlib import Path
55
from typing import override
66

7+
import anyio
78
import asyncer
8-
from attrs import define
9+
from attrs import define, field
910
from lsp_client.capability.request import WithRequestDocumentSymbol, WithRequestHover
1011
from lsprotocol.types import DocumentSymbol
1112
from lsprotocol.types import Position as LSPPosition
@@ -22,6 +23,8 @@
2223

2324
@define
2425
class OutlineCapability(Capability[OutlineRequest, OutlineResponse]):
26+
hover_sem: anyio.Semaphore = field(default=anyio.Semaphore(32))
27+
2528
@override
2629
async def __call__(self, req: OutlineRequest) -> OutlineResponse | None:
2730
symbols = await ensure_capability(
@@ -117,7 +120,8 @@ def _make_item(
117120
)
118121

119122
async def _fill_hover(self, item: SymbolDetailInfo, pos: LSPPosition) -> None:
120-
if hover := await ensure_capability(
121-
self.client, WithRequestHover
122-
).request_hover(item.file_path, pos):
123-
item.hover = clean_hover_content(hover.value)
123+
async with self.hover_sem:
124+
if hover := await ensure_capability(
125+
self.client, WithRequestHover
126+
).request_hover(item.file_path, pos):
127+
item.hover = clean_hover_content(hover.value)

src/lsap/capability/reference.py

Lines changed: 51 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from functools import cached_property
22

3+
import anyio
34
import asyncer
4-
from attrs import Factory, define
5+
from attrs import Factory, define, field
56
from lsp_client.capability.request import (
67
WithRequestDocumentSymbol,
78
WithRequestHover,
@@ -29,6 +30,7 @@
2930
@define
3031
class ReferenceCapability(Capability[ReferenceRequest, ReferenceResponse]):
3132
_cache: PaginationCache[ReferenceItem] = Factory(PaginationCache)
33+
process_sem: anyio.Semaphore = field(default=anyio.Semaphore(32))
3234

3335
@cached_property
3436
def locate(self) -> LocateCapability:
@@ -92,56 +94,57 @@ async def _process_reference(
9294
context_lines: int,
9395
items: list[ReferenceItem],
9496
) -> None:
95-
file_path = self.client.from_uri(loc.uri)
96-
content = await self.client.read_file(file_path)
97-
reader = DocumentReader(content)
98-
99-
range = loc.range
100-
context_range = LSPRange(
101-
start=LSPPosition(
102-
line=max(0, range.start.line - context_lines), character=0
103-
),
104-
end=LSPPosition(line=range.end.line + context_lines + 1, character=0),
105-
)
106-
if not (snippet := reader.read(context_range, trim_empty=True)):
107-
return
108-
109-
symbol: SymbolDetailInfo | None = None
110-
if (
111-
symbols := await ensure_capability(
112-
self.client, WithRequestDocumentSymbol
113-
).request_document_symbol_list(file_path)
114-
) and (match := symbol_at(symbols, range.start)):
115-
path, sym = match
116-
kind = SymbolKind.from_lsp(sym.kind)
117-
118-
symbol = SymbolDetailInfo(
119-
file_path=file_path,
120-
name=sym.name,
121-
path=path,
122-
kind=kind,
123-
detail=sym.detail,
124-
range=Range(
125-
start=Position.from_lsp(sym.range.start),
126-
end=Position.from_lsp(sym.range.end),
97+
async with self.process_sem:
98+
file_path = self.client.from_uri(loc.uri)
99+
content = await self.client.read_file(file_path)
100+
reader = DocumentReader(content)
101+
102+
range = loc.range
103+
context_range = LSPRange(
104+
start=LSPPosition(
105+
line=max(0, range.start.line - context_lines), character=0
127106
),
107+
end=LSPPosition(line=range.end.line + context_lines + 1, character=0),
128108
)
129-
130-
if hover := await ensure_capability(
131-
self.client, WithRequestHover
132-
).request_hover(file_path, range.start):
133-
symbol.hover = clean_hover_content(hover.value)
134-
135-
items.append(
136-
ReferenceItem(
137-
location=LSAPLocation(
109+
if not (snippet := reader.read(context_range, trim_empty=True)):
110+
return
111+
112+
symbol: SymbolDetailInfo | None = None
113+
if (
114+
symbols := await ensure_capability(
115+
self.client, WithRequestDocumentSymbol
116+
).request_document_symbol_list(file_path)
117+
) and (match := symbol_at(symbols, range.start)):
118+
path, sym = match
119+
kind = SymbolKind.from_lsp(sym.kind)
120+
121+
symbol = SymbolDetailInfo(
138122
file_path=file_path,
123+
name=sym.name,
124+
path=path,
125+
kind=kind,
126+
detail=sym.detail,
139127
range=Range(
140-
start=Position.from_lsp(range.start),
141-
end=Position.from_lsp(range.end),
128+
start=Position.from_lsp(sym.range.start),
129+
end=Position.from_lsp(sym.range.end),
142130
),
143-
),
144-
code=snippet.content,
145-
symbol=symbol,
131+
)
132+
133+
if hover := await ensure_capability(
134+
self.client, WithRequestHover
135+
).request_hover(file_path, range.start):
136+
symbol.hover = clean_hover_content(hover.value)
137+
138+
items.append(
139+
ReferenceItem(
140+
location=LSAPLocation(
141+
file_path=file_path,
142+
range=Range(
143+
start=Position.from_lsp(range.start),
144+
end=Position.from_lsp(range.end),
145+
),
146+
),
147+
code=snippet.content,
148+
symbol=symbol,
149+
)
146150
)
147-
)

src/lsap/capability/rename.py

Lines changed: 80 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
from pathlib import Path
77
from typing import override
88

9+
import anyio
910
import asyncer
10-
from attrs import define
11+
from attrs import define, field
1112
from lsp_client.capability.request import WithRequestRename
1213
from lsp_client.protocol import CapabilityClientProtocol
1314
from lsp_client.utils.types import lsp_type
@@ -141,6 +142,8 @@ def should_exclude(uri: str) -> bool:
141142

142143
@define
143144
class RenamePreviewCapability(Capability[RenamePreviewRequest, RenamePreviewResponse]):
145+
file_sem: anyio.Semaphore = field(default=anyio.Semaphore(32))
146+
144147
@cached_property
145148
def locate(self) -> LocateCapability:
146149
return LocateCapability(self.client)
@@ -208,44 +211,51 @@ async def _to_file_change(
208211
*,
209212
reader: DocumentReader | None = None,
210213
) -> RenameFileChange | None:
211-
if reader is None:
212-
content = await self.client.read_file(
213-
self.client.from_uri(uri, relative=False)
214-
)
215-
reader = DocumentReader(content)
216-
217-
diffs: list[RenameDiff] = []
218-
for edit in edits:
219-
start, end = edit.range.start, edit.range.end
220-
line_raw = reader.get_line(start.line, keepends=True)
221-
if line_raw is None:
222-
continue
223-
224-
original_line = line_raw.rstrip("\r\n")
225-
new_text = get_edit_text(edit)
226-
227-
if start.line == end.line:
228-
modified_line = (
229-
line_raw[: start.character] + new_text + line_raw[end.character :]
230-
).rstrip("\r\n")
231-
else:
232-
modified_line = new_text
233-
234-
diffs.append(
235-
RenameDiff(
236-
line=start.line + 1,
237-
original=original_line,
238-
modified=modified_line,
214+
async with self.file_sem:
215+
if reader is None:
216+
content = await self.client.read_file(
217+
self.client.from_uri(uri, relative=False)
218+
)
219+
reader = DocumentReader(content)
220+
221+
diffs: list[RenameDiff] = []
222+
for edit in edits:
223+
start, end = edit.range.start, edit.range.end
224+
line_raw = reader.get_line(start.line, keepends=True)
225+
if line_raw is None:
226+
continue
227+
228+
original_line = line_raw.rstrip("\r\n")
229+
new_text = get_edit_text(edit)
230+
231+
if start.line == end.line:
232+
modified_line = (
233+
line_raw[: start.character]
234+
+ new_text
235+
+ line_raw[end.character :]
236+
).rstrip("\r\n")
237+
else:
238+
modified_line = new_text
239+
240+
diffs.append(
241+
RenameDiff(
242+
line=start.line + 1,
243+
original=original_line,
244+
modified=modified_line,
245+
)
239246
)
240-
)
241247

242-
if diffs:
243-
return RenameFileChange(file_path=self.client.from_uri(uri), diffs=diffs)
244-
return None
248+
if diffs:
249+
return RenameFileChange(
250+
file_path=self.client.from_uri(uri), diffs=diffs
251+
)
252+
return None
245253

246254

247255
@define
248256
class RenameExecuteCapability(Capability[RenameExecuteRequest, RenameExecuteResponse]):
257+
file_sem: anyio.Semaphore = field(default=anyio.Semaphore(32))
258+
249259
@override
250260
async def __call__(self, req: RenameExecuteRequest) -> RenameExecuteResponse | None:
251261
cached = _preview_cache.get(req.rename_id)
@@ -301,37 +311,42 @@ async def _to_file_change(
301311
*,
302312
reader: DocumentReader | None = None,
303313
) -> RenameFileChange | None:
304-
if reader is None:
305-
content = await self.client.read_file(
306-
self.client.from_uri(uri, relative=False)
307-
)
308-
reader = DocumentReader(content)
309-
310-
diffs: list[RenameDiff] = []
311-
for edit in edits:
312-
start, end = edit.range.start, edit.range.end
313-
line_raw = reader.get_line(start.line, keepends=True)
314-
if line_raw is None:
315-
continue
316-
317-
original_line = line_raw.rstrip("\r\n")
318-
new_text = get_edit_text(edit)
319-
320-
if start.line == end.line:
321-
modified_line = (
322-
line_raw[: start.character] + new_text + line_raw[end.character :]
323-
).rstrip("\r\n")
324-
else:
325-
modified_line = new_text
326-
327-
diffs.append(
328-
RenameDiff(
329-
line=start.line + 1,
330-
original=original_line,
331-
modified=modified_line,
314+
async with self.file_sem:
315+
if reader is None:
316+
content = await self.client.read_file(
317+
self.client.from_uri(uri, relative=False)
318+
)
319+
reader = DocumentReader(content)
320+
321+
diffs: list[RenameDiff] = []
322+
for edit in edits:
323+
start, end = edit.range.start, edit.range.end
324+
line_raw = reader.get_line(start.line, keepends=True)
325+
if line_raw is None:
326+
continue
327+
328+
original_line = line_raw.rstrip("\r\n")
329+
new_text = get_edit_text(edit)
330+
331+
if start.line == end.line:
332+
modified_line = (
333+
line_raw[: start.character]
334+
+ new_text
335+
+ line_raw[end.character :]
336+
).rstrip("\r\n")
337+
else:
338+
modified_line = new_text
339+
340+
diffs.append(
341+
RenameDiff(
342+
line=start.line + 1,
343+
original=original_line,
344+
modified=modified_line,
345+
)
332346
)
333-
)
334347

335-
if diffs:
336-
return RenameFileChange(file_path=self.client.from_uri(uri), diffs=diffs)
337-
return None
348+
if diffs:
349+
return RenameFileChange(
350+
file_path=self.client.from_uri(uri), diffs=diffs
351+
)
352+
return None

0 commit comments

Comments
 (0)