Skip to content
This repository was archived by the owner on Jan 13, 2026. It is now read-only.

Commit e79e64f

Browse files
committed
update
1 parent 1d18c0c commit e79e64f

File tree

4 files changed

+74
-136
lines changed

4 files changed

+74
-136
lines changed

src/lsp_cli/__main__.py

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ def create_locate(
7070
find: str | None = None,
7171
marker: str = "<HERE>",
7272
) -> Locate:
73+
if line is not None and symbol:
74+
console.print("[red]Error:[/red] --line and --symbol are mutually exclusive")
75+
raise typer.Exit(1)
76+
7377
scope = None
7478
if line is not None:
7579
scope = LineScope(line=line)
@@ -118,9 +122,9 @@ def main(
118122
@cli_syncify
119123
async def get_definition(
120124
file_path: op.FileArg,
121-
line: Annotated[int | None, op.LineOpt] = None,
125+
line: op.LineOpt = None,
122126
symbol: op.SymbolPathOpt = None,
123-
find: Annotated[str | None, op.FindOpt] = None,
127+
find: op.FindOpt = None,
124128
marker: op.MarkerOpt = "<HERE>",
125129
mode: Annotated[
126130
Literal["definition", "declaration", "type_definition"],
@@ -149,13 +153,13 @@ async def get_definition(
149153
@cli_syncify
150154
async def get_completion(
151155
file_path: op.FileArg,
152-
line: Annotated[int | None, op.LineOpt] = None,
156+
line: op.LineOpt = None,
153157
symbol: op.SymbolPathOpt = None,
154-
find: Annotated[str | None, op.FindOpt] = None,
158+
find: op.FindOpt = None,
155159
marker: op.MarkerOpt = "<HERE>",
156-
max_items: Annotated[int | None, op.MaxItemsOpt] = 15,
157-
start_index: Annotated[int, op.StartIndexOpt] = 0,
158-
pagination_id: Annotated[str | None, op.PaginationIdOpt] = None,
160+
max_items: op.MaxItemsOpt = 15,
161+
start_index: op.StartIndexOpt = 0,
162+
pagination_id: op.PaginationIdOpt = None,
159163
):
160164
locate = create_locate(file_path, line, symbol, find, marker)
161165

@@ -180,9 +184,9 @@ async def get_completion(
180184
@cli_syncify
181185
async def get_hover(
182186
file_path: op.FileArg,
183-
line: Annotated[int | None, op.LineOpt] = None,
187+
line: op.LineOpt = None,
184188
symbol: op.SymbolPathOpt = None,
185-
find: Annotated[str | None, op.FindOpt] = None,
189+
find: op.FindOpt = None,
186190
marker: op.MarkerOpt = "<HERE>",
187191
):
188192
locate = create_locate(file_path, line, symbol, find, marker)
@@ -204,18 +208,25 @@ async def get_hover(
204208
@cli_syncify
205209
async def get_reference(
206210
file_path: op.FileArg,
207-
line: op.LineOpt,
211+
line: op.LineOpt = None,
208212
symbol: op.SymbolPathOpt = None,
209-
find: Annotated[str | None, op.FindOpt] = None,
213+
find: op.FindOpt = None,
210214
marker: op.MarkerOpt = "<HERE>",
211215
mode: Annotated[
212216
Literal["references", "implementations"],
213217
typer.Option("--mode", "-m", help="Search mode"),
214218
] = "references",
215-
context_lines: Annotated[int, op.ContextLinesOpt] = 2,
216-
max_items: Annotated[int | None, op.MaxItemsOpt] = None,
217-
start_index: Annotated[int, op.StartIndexOpt] = 0,
218-
pagination_id: Annotated[str | None, op.PaginationIdOpt] = None,
219+
context_lines: Annotated[
220+
int,
221+
typer.Option(
222+
"--context-lines",
223+
"-C",
224+
help="Lines of context around match",
225+
),
226+
] = 2,
227+
max_items: op.MaxItemsOpt = None,
228+
start_index: op.StartIndexOpt = 0,
229+
pagination_id: op.PaginationIdOpt = None,
219230
):
220231
locate = create_locate(file_path, line, symbol, find, marker)
221232

@@ -260,9 +271,9 @@ async def get_outline(file_path: op.FileArg):
260271
@cli_syncify
261272
async def get_symbol(
262273
file_path: op.FileArg,
263-
line: Annotated[int | None, op.LineOpt] = None,
274+
line: op.LineOpt = None,
264275
symbol: op.SymbolPathOpt = None,
265-
find: Annotated[str | None, op.FindOpt] = None,
276+
find: op.FindOpt = None,
266277
marker: op.MarkerOpt = "<HERE>",
267278
):
268279
locate = create_locate(file_path, line, symbol, find, marker)
@@ -287,9 +298,9 @@ async def get_symbol(
287298
async def search_workspace_symbol(
288299
query: Annotated[str, typer.Argument(help="Symbol name to search")],
289300
path: op.FileArg,
290-
max_items: Annotated[int | None, op.MaxItemsOpt] = None,
291-
start_index: Annotated[int, op.StartIndexOpt] = 0,
292-
pagination_id: Annotated[str | None, op.PaginationIdOpt] = None,
301+
max_items: op.MaxItemsOpt = None,
302+
start_index: op.StartIndexOpt = 0,
303+
pagination_id: op.PaginationIdOpt = None,
293304
):
294305
async with init_client(path) as client:
295306
if not isinstance(client, WorkspaceSymbolClient):

src/lsp_cli/manager/client.py

Lines changed: 26 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -104,18 +104,28 @@ def get_client_id(target: TargetClient) -> str:
104104
@define
105105
class ManagedClient:
106106
target: TargetClient
107-
_server: uvicorn.Server | None = field(init=False, default=None)
108-
_timeout_scope: anyio.CancelScope | None = field(init=False, default=None)
109-
_server_scope: anyio.CancelScope | None = field(init=False, default=None)
107+
_server: uvicorn.Server = field(init=False)
108+
_timeout_scope: anyio.CancelScope = field(init=False)
109+
_server_scope: anyio.CancelScope = field(init=False)
110110

111111
_deadline: float = Factory(lambda: anyio.current_time() + settings.idle_timeout)
112112
_should_exit: bool = False
113113

114-
_log_handler_id: int | None = field(init=False, default=None)
115-
116114
@cached_property
117115
def logger(self) -> loguru.Logger:
118-
return logger.bind(client_id=self.id)
116+
logger.bind(client_id=self.id)
117+
118+
client_log_dir = LOG_DIR / "clients"
119+
client_log_dir.mkdir(parents=True, exist_ok=True)
120+
logger.add(
121+
client_log_dir / f"{self.id}.log",
122+
rotation="10 MB",
123+
retention="1 day",
124+
level="DEBUG",
125+
filter=lambda record: record["extra"].get("client_id") == self.id,
126+
)
127+
128+
return logger
119129

120130
@property
121131
def id(self) -> str:
@@ -136,33 +146,19 @@ def info(self) -> ManagedClientInfo:
136146
def stop(self) -> None:
137147
self.logger.info("Stopping managed client {}", self.id)
138148
self._should_exit = True
139-
if self._server:
140-
self._server.should_exit = True
141-
if self._server_scope:
142-
self._server_scope.cancel()
143-
if self._timeout_scope:
144-
self._timeout_scope.cancel()
149+
self._server.should_exit = True
150+
self._server_scope.cancel()
151+
self._timeout_scope.cancel()
145152

146153
async def run(self) -> None:
147-
client_log_dir = LOG_DIR / "clients"
148-
client_log_dir.mkdir(parents=True, exist_ok=True)
149-
self._log_handler_id = logger.add(
150-
client_log_dir / f"{self.id}.log",
151-
rotation="10 MB",
152-
retention="1 day",
153-
level="DEBUG",
154-
filter=lambda record: record["extra"].get("client_id") == self.id,
155-
)
156-
157154
self.logger.info(
158155
"Starting managed client {} for project {} at {}",
159156
self.id,
160157
self.target.project_path,
161158
self.uds_path,
162159
)
163160
async with AsyncExitStack() as stack:
164-
if self._log_handler_id is not None:
165-
stack.callback(lambda: logger.remove(self._log_handler_id))
161+
stack.callback(lambda: logger.remove(self._log_handler_id))
166162

167163
uds_path = anyio.Path(self.uds_path)
168164
await uds_path.unlink(missing_ok=True)
@@ -228,11 +224,8 @@ async def handle_package(
228224
self._server = uvicorn.Server(config)
229225

230226
def cleanup_server():
231-
self._server = None
232-
if self._timeout_scope:
233-
self._timeout_scope.cancel()
234-
if self._server_scope:
235-
self._server_scope.cancel()
227+
self._timeout_scope.cancel()
228+
self._server_scope.cancel()
236229

237230
stack.callback(cleanup_server)
238231

@@ -244,12 +237,11 @@ def cleanup_server():
244237

245238
def _reset_timeout(self) -> None:
246239
self._deadline = anyio.current_time() + settings.idle_timeout
247-
if self._timeout_scope:
248-
self._timeout_scope.cancel()
240+
self._timeout_scope.cancel()
249241

250242
async def _timeout_loop(self) -> None:
251243
while not self._should_exit:
252-
if self._server and self._server.should_exit:
244+
if self._server.should_exit:
253245
break
254246
remaining = self._deadline - anyio.current_time()
255247
if remaining <= 0:
@@ -258,7 +250,5 @@ async def _timeout_loop(self) -> None:
258250
self._timeout_scope = scope
259251
await anyio.sleep(remaining)
260252

261-
if self._server:
262-
self._server.should_exit = True
263-
if self._server_scope:
264-
self._server_scope.cancel()
253+
self._server.should_exit = True
254+
self._server_scope.cancel()

src/lsp_cli/options.py

Lines changed: 9 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from pathlib import Path
2-
from typing import Annotated, Literal
2+
from typing import Annotated
33

44
import typer
55

@@ -16,6 +16,7 @@
1616
"--line",
1717
"-l",
1818
help="Line number (1-indexed)",
19+
rich_help_panel="Scope",
1920
),
2021
]
2122

@@ -25,15 +26,16 @@
2526
"--find",
2627
"-f",
2728
help="Text snippet to find",
29+
rich_help_panel="Search",
2830
),
2931
]
3032

3133
MarkerOpt = Annotated[
3234
str,
3335
typer.Option(
34-
"<HERE>",
3536
"--marker",
3637
help="Position marker in find pattern",
38+
rich_help_panel="Search",
3739
),
3840
]
3941

@@ -43,109 +45,36 @@
4345
"--symbol",
4446
"-s",
4547
help="Symbol path (multiple for hierarchy)",
46-
),
47-
]
48-
49-
HoverOpt = Annotated[
50-
bool,
51-
typer.Option(
52-
True,
53-
"--hover/--no-hover",
54-
help="Include hover information",
55-
),
56-
]
57-
58-
CodeOpt = Annotated[
59-
bool,
60-
typer.Option(
61-
True,
62-
"--code/--no-code",
63-
"-c",
64-
help="Include source code",
48+
rich_help_panel="Scope",
6549
),
6650
]
6751

6852
MaxItemsOpt = Annotated[
6953
int | None,
7054
typer.Option(
71-
None,
7255
"--max-items",
7356
"-n",
7457
help="Max items to return",
58+
rich_help_panel="Pagination",
7559
),
7660
]
7761

7862
StartIndexOpt = Annotated[
7963
int,
8064
typer.Option(
81-
0,
8265
"--start-index",
83-
"-s",
66+
"-i",
8467
help="Pagination offset",
68+
rich_help_panel="Pagination",
8569
),
8670
]
8771

8872
PaginationIdOpt = Annotated[
8973
str | None,
9074
typer.Option(
91-
None,
9275
"--pagination-id",
9376
"-p",
9477
help="Pagination token",
95-
),
96-
]
97-
98-
# New options for alignment with lsap-schema
99-
100-
ContextLinesOpt = Annotated[
101-
int,
102-
typer.Option(
103-
"--context-lines",
104-
"-C",
105-
help="Lines of context around match",
106-
),
107-
]
108-
109-
DepthOpt = Annotated[
110-
int,
111-
typer.Option(
112-
"--depth",
113-
"-D",
114-
help="How many hops to trace in hierarchy",
115-
),
116-
]
117-
118-
DirectionOpt = Annotated[
119-
Literal["incoming", "outgoing", "both"],
120-
typer.Option(
121-
"--direction",
122-
"-d",
123-
help="Trace direction for hierarchy",
124-
),
125-
]
126-
127-
IncludeExternalOpt = Annotated[
128-
bool,
129-
typer.Option(
130-
"--include-external/--no-include-external",
131-
help="Include calls to/from external libraries",
132-
),
133-
]
134-
135-
NewNameOpt = Annotated[
136-
str,
137-
typer.Option(
138-
"--new-name",
139-
"-N",
140-
help="The new name to apply",
141-
),
142-
]
143-
144-
MinSeverityOpt = Annotated[
145-
Literal["Error", "Warning", "Information", "Hint"],
146-
typer.Option(
147-
"--min-severity",
148-
"-m",
149-
help="Minimum severity to include",
78+
rich_help_panel="Pagination",
15079
),
15180
]

tests/test_main_e2e.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,14 @@ def test_symbol_e2e(mock_init_client, mock_client):
304304
assert "my_var" in result.stdout
305305

306306

307+
def test_mutual_exclusive_scope():
308+
result = runner.invoke(
309+
app, ["definition", "test.py", "--line", "5", "--symbol", "MyClass"]
310+
)
311+
assert result.exit_code != 0
312+
assert "mutually exclusive" in result.stdout
313+
314+
307315
def test_workspace_symbol_e2e(mock_init_client, mock_client):
308316
with (
309317
patch("lsp_cli.__main__.WorkspaceSymbolCapability") as mock_cap_cls,

0 commit comments

Comments
 (0)