Skip to content

Commit 185b551

Browse files
committed
fix(langserver): stabilize workspace diagnostics
1 parent e13641e commit 185b551

File tree

5 files changed

+84
-45
lines changed

5 files changed

+84
-45
lines changed

packages/language_server/src/robotcode/language_server/common/parts/diagnostics.py

Lines changed: 67 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
from concurrent.futures import CancelledError
66
from dataclasses import dataclass, field
77
from enum import Enum
8-
from threading import Event, Lock, RLock, Timer
8+
from threading import Event, Timer
99
from typing import TYPE_CHECKING, Any, Dict, Final, List, Optional, cast
1010

11-
from robotcode.core.concurrent import Task, check_current_task_canceled, run_as_task
11+
from robotcode.core.concurrent import Lock, RLock, Task, check_current_task_canceled, run_as_task
1212
from robotcode.core.event import event
1313
from robotcode.core.lsp.types import (
1414
Diagnostic,
@@ -77,6 +77,7 @@ class DiagnosticsData:
7777
future: Optional[Task[Any]] = None
7878
force: bool = False
7979
single: bool = False
80+
skipped_entries: bool = False
8081

8182

8283
class DiagnosticsProtocolPart(LanguageServerProtocolPart):
@@ -111,15 +112,16 @@ def __init__(self, protocol: "LanguageServerProtocol") -> None:
111112

112113
self._current_diagnostics_task_lock = RLock()
113114
self._current_diagnostics_task: Optional[Task[Any]] = None
115+
self._diagnostics_task_timeout = 300
114116

115117
def server_initialized(self, sender: Any) -> None:
116-
self._workspace_diagnostics_task = run_as_task(self.run_workspace_diagnostics)
117-
118118
if not self.client_supports_pull:
119119
self.parent.documents.did_open.add(self.update_document_diagnostics)
120120
self.parent.documents.did_change.add(self.update_document_diagnostics)
121121
self.parent.documents.did_save.add(self.update_document_diagnostics)
122122

123+
self._workspace_diagnostics_task = run_as_task(self.run_workspace_diagnostics)
124+
123125
def extend_capabilities(self, capabilities: ServerCapabilities) -> None:
124126
if (
125127
self.parent.client_capabilities is not None
@@ -230,7 +232,7 @@ def cancel_workspace_diagnostics_task(self, sender: Any) -> None:
230232

231233
def break_workspace_diagnostics_loop(self) -> None:
232234
self._break_diagnostics_loop_event.set()
233-
with self._current_diagnostics_task_lock:
235+
with self._current_diagnostics_task_lock(timeout=self._diagnostics_task_timeout * 2):
234236
if self._current_diagnostics_task is not None and not self._current_diagnostics_task.done():
235237
self._current_diagnostics_task.cancel()
236238

@@ -256,6 +258,7 @@ def run_workspace_diagnostics(self) -> None:
256258
(data := self.get_diagnostics_data(doc)).force
257259
or doc.version != data.version
258260
or data.future is None
261+
or data.skipped_entries
259262
)
260263
and not data.single
261264
)
@@ -267,12 +270,17 @@ def run_workspace_diagnostics(self) -> None:
267270
check_current_task_canceled(1)
268271
continue
269272

270-
self._logger.info(lambda: f"start collecting workspace diagnostics for {len(documents)} documents")
273+
self._logger.debug(lambda: f"start collecting workspace diagnostics for {len(documents)} documents")
271274

272275
done_something = False
273276

274277
self.on_workspace_diagnostics_analyze(self)
275278

279+
if self._break_diagnostics_loop_event.is_set():
280+
self._logger.debug("break workspace diagnostics loop 1")
281+
self.on_workspace_diagnostics_break(self)
282+
continue
283+
276284
start = time.monotonic()
277285
with self.parent.window.progress(
278286
"Analyze Workspace",
@@ -282,9 +290,11 @@ def run_workspace_diagnostics(self) -> None:
282290
start=False,
283291
) as progress:
284292
for i, document in enumerate(documents):
293+
self._logger.debug(lambda: f"Analyze {document}")
285294
check_current_task_canceled()
286295

287296
if self._break_diagnostics_loop_event.is_set():
297+
self._logger.debug("break workspace diagnostics loop 2")
288298
self.on_workspace_diagnostics_break(self)
289299
break
290300

@@ -312,7 +322,7 @@ def run_workspace_diagnostics(self) -> None:
312322
callback_filter=language_id_filter(document),
313323
return_exceptions=True,
314324
)
315-
self._current_diagnostics_task.result(300)
325+
self._current_diagnostics_task.result(self._diagnostics_task_timeout)
316326
except CancelledError:
317327
self._logger.debug(lambda: f"Analyzing {document} cancelled")
318328
except BaseException as e:
@@ -325,31 +335,53 @@ def run_workspace_diagnostics(self) -> None:
325335
with self._current_diagnostics_task_lock:
326336
self._current_diagnostics_task = None
327337

328-
self._logger.info(
338+
self._logger.debug(
329339
lambda: f"Analyzing workspace for {len(documents)} " f"documents takes {time.monotonic() - start}s"
330340
)
331341

342+
if self._break_diagnostics_loop_event.is_set():
343+
self._logger.debug("break workspace diagnostics loop 3")
344+
self.on_workspace_diagnostics_break(self)
345+
continue
346+
332347
self.on_workspace_diagnostics_collect(self)
333348

349+
documents_to_collect = [
350+
doc
351+
for doc in documents
352+
if doc.opened_in_editor or self.get_diagnostics_mode(document.uri) == DiagnosticsMode.WORKSPACE
353+
]
354+
355+
for document in set(documents) - set(documents_to_collect):
356+
self.get_diagnostics_data(document).force = False
357+
self.get_diagnostics_data(document).version = document.version
358+
self.get_diagnostics_data(document).skipped_entries = False
359+
self.get_diagnostics_data(document).single = False
360+
self.get_diagnostics_data(document).future = Task()
361+
334362
start = time.monotonic()
335363
with self.parent.window.progress(
336364
"Collect Diagnostics",
337365
cancellable=False,
338366
current=0,
339-
max=len(documents),
367+
max=len(documents_to_collect),
340368
start=False,
341369
) as progress:
342-
for i, document in enumerate(documents):
370+
for i, document in enumerate(documents_to_collect):
371+
self._logger.debug(lambda: f"Collect diagnostics for {document}")
343372
check_current_task_canceled()
344373

345374
if self._break_diagnostics_loop_event.is_set():
375+
self._logger.debug("break workspace diagnostics loop 4")
346376
self.on_workspace_diagnostics_break(self)
347377
break
348378

349379
mode = self.get_diagnostics_mode(document.uri)
350380
if mode == DiagnosticsMode.OFF:
351381
self.get_diagnostics_data(document).force = False
352382
self.get_diagnostics_data(document).version = document.version
383+
self.get_diagnostics_data(document).skipped_entries = False
384+
self.get_diagnostics_data(document).single = False
353385
self.get_diagnostics_data(document).future = Task()
354386
continue
355387

@@ -366,15 +398,15 @@ def run_workspace_diagnostics(self) -> None:
366398
progress.report(f"Collect {name}", current=i + 1)
367399
elif analysis_mode == AnalysisProgressMode.SIMPLE:
368400
progress.begin()
369-
progress.report(f"Collect {i+1}/{len(documents)}", current=i + 1)
401+
progress.report(f"Collect {i+1}/{len(documents_to_collect)}", current=i + 1)
370402

371403
try:
372404
with self._current_diagnostics_task_lock:
373405
self._current_diagnostics_task = self.create_document_diagnostics_task(
374406
document,
375407
False,
376408
False,
377-
mode == DiagnosticsMode.WORKSPACE,
409+
mode == DiagnosticsMode.WORKSPACE or document.opened_in_editor,
378410
)
379411
self._current_diagnostics_task.result(300)
380412
except CancelledError:
@@ -392,8 +424,8 @@ def run_workspace_diagnostics(self) -> None:
392424
if not done_something:
393425
check_current_task_canceled(1)
394426

395-
self._logger.info(
396-
lambda: f"collecting workspace diagnostics for {len(documents)} "
427+
self._logger.debug(
428+
lambda: f"collecting workspace diagnostics for {len(documents_to_collect)} "
397429
f"documents takes {time.monotonic() - start}s"
398430
)
399431

@@ -404,13 +436,9 @@ def run_workspace_diagnostics(self) -> None:
404436
finally:
405437
self.on_workspace_diagnostics_end(self)
406438

407-
def _diagnostics_task_done(self, document: TextDocument, data: DiagnosticsData, t: Task[Any]) -> None:
408-
self._logger.debug(lambda: f"diagnostics for {document} task {'canceled' if t.cancelled() else 'ended'}")
409-
410-
data.force = data.single
411-
data.single = False
412-
if data.force:
413-
self.break_workspace_diagnostics_loop()
439+
def _diagnostics_task_done(self, document: TextDocument, data: DiagnosticsData, task: Task[Any]) -> None:
440+
if task.done() and not task.cancelled():
441+
data.single = False
414442

415443
def create_document_diagnostics_task(
416444
self,
@@ -421,11 +449,12 @@ def create_document_diagnostics_task(
421449
) -> Task[Any]:
422450
data = self.get_diagnostics_data(document)
423451

424-
if data.force or document.version != data.version or data.future is None:
452+
if data.force or document.version != data.version or data.future is None or data.skipped_entries:
453+
data.single = single
454+
data.force = False
455+
425456
future = data.future
426457

427-
data.force = False
428-
data.single = single
429458
if future is not None and not future.done():
430459
self._logger.debug(lambda: f"try to cancel diagnostics for {document}")
431460

@@ -441,6 +470,8 @@ def create_document_diagnostics_task(
441470
)
442471

443472
data.future.add_done_callback(functools.partial(self._diagnostics_task_done, document, data))
473+
else:
474+
self._logger.debug(lambda: f"skip diagnostics for {document}")
444475

445476
return data.future
446477

@@ -457,7 +488,7 @@ def _get_diagnostics_for_document(
457488
if debounce:
458489
check_current_task_canceled(0.75)
459490

460-
skipped_collectors = False
491+
data.skipped_entries = False
461492
collected_keys: List[Any] = []
462493
try:
463494
for result in self.collect(
@@ -477,7 +508,7 @@ def _get_diagnostics_for_document(
477508

478509
data.id = str(uuid.uuid4())
479510
if result.skipped:
480-
skipped_collectors = True
511+
data.skipped_entries = True
481512

482513
if result.diagnostics is not None:
483514
for d in result.diagnostics:
@@ -488,7 +519,9 @@ def _get_diagnostics_for_document(
488519
if doc is not None:
489520
r.location.range = doc.range_to_utf16(r.location.range)
490521

491-
data.entries[result.key] = result.diagnostics
522+
if not result.skipped:
523+
data.entries[result.key] = result.diagnostics
524+
492525
if result.diagnostics is not None:
493526
collected_keys.append(result.key)
494527

@@ -503,7 +536,6 @@ def _get_diagnostics_for_document(
503536
finally:
504537
for k in set(data.entries.keys()) - set(collected_keys):
505538
data.entries.pop(k)
506-
data.force = skipped_collectors
507539

508540
def publish_diagnostics(self, document: TextDocument, diagnostics: List[Diagnostic]) -> None:
509541
self.parent.send_notification(
@@ -515,8 +547,11 @@ def publish_diagnostics(self, document: TextDocument, diagnostics: List[Diagnost
515547
),
516548
)
517549

550+
def _update_document_diagnostics(self, document: TextDocument) -> None:
551+
self.create_document_diagnostics_task(document, True).result(self._diagnostics_task_timeout)
552+
518553
def update_document_diagnostics(self, sender: Any, document: TextDocument) -> None:
519-
self.create_document_diagnostics_task(document, True)
554+
run_as_task(self._update_document_diagnostics, document)
520555

521556
@rpc_method(name="textDocument/diagnostic", param_type=DocumentDiagnosticParams, threaded=True)
522557
def _text_document_diagnostic(
@@ -543,19 +578,19 @@ def _text_document_diagnostic(
543578
f"Document {text_document!r} not found.",
544579
)
545580

546-
self.create_document_diagnostics_task(document, True)
581+
self.create_document_diagnostics_task(document, True).result(300)
547582

548583
return RelatedFullDocumentDiagnosticReport([])
549584
except CancelledError:
550585
self._logger.debug("canceled _text_document_diagnostic")
551586
raise
552587

553588
def get_diagnostics_data(self, document: TextDocument) -> DiagnosticsData:
554-
data: DiagnosticsData = document.get_data(self, None)
589+
data: DiagnosticsData = document.get_data(DiagnosticsProtocolPart, None)
555590

556591
if data is None:
557592
data = DiagnosticsData(str(uuid.uuid4())) # type: ignore
558-
document.set_data(self, data)
593+
document.set_data(DiagnosticsProtocolPart, data)
559594

560595
return data
561596

packages/language_server/src/robotcode/language_server/robotframework/parts/diagnostics.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -281,14 +281,14 @@ def _collect_model_errors(self, document: TextDocument) -> DiagnosticsResult:
281281
@language_id("robotframework")
282282
@_logger.call
283283
def collect_unused_keyword_references(self, sender: Any, document: TextDocument) -> DiagnosticsResult:
284-
if not self._collect_unused_references_event.is_set():
285-
return DiagnosticsResult(self.collect_unused_keyword_references, None, True)
286-
287284
config = self.parent.workspace.get_configuration(AnalysisConfig, document.uri)
288285

289286
if not config.find_unused_references:
290287
return DiagnosticsResult(self.collect_unused_keyword_references, [])
291288

289+
if not self._collect_unused_references_event.is_set():
290+
return DiagnosticsResult(self.collect_unused_keyword_references, None, True)
291+
292292
return self._collect_unused_keyword_references(document)
293293

294294
def _collect_unused_keyword_references(self, document: TextDocument) -> DiagnosticsResult:
@@ -338,14 +338,14 @@ def _collect_unused_keyword_references(self, document: TextDocument) -> Diagnost
338338
@language_id("robotframework")
339339
@_logger.call
340340
def collect_unused_variable_references(self, sender: Any, document: TextDocument) -> DiagnosticsResult:
341-
if not self._collect_unused_references_event.is_set():
342-
return DiagnosticsResult(self.collect_unused_variable_references, None, True)
343-
344341
config = self.parent.workspace.get_configuration(AnalysisConfig, document.uri)
345342

346343
if not config.find_unused_references:
347344
return DiagnosticsResult(self.collect_unused_variable_references, [])
348345

346+
if not self._collect_unused_references_event.is_set():
347+
return DiagnosticsResult(self.collect_unused_variable_references, None, True)
348+
349349
return self._collect_unused_variable_references(document)
350350

351351
def _collect_unused_variable_references(self, document: TextDocument) -> DiagnosticsResult:

packages/language_server/src/robotcode/language_server/robotframework/parts/references.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def _find_references_in_workspace(
153153
) -> List[Location]:
154154
result: List[Location] = []
155155

156-
for doc in self.parent.documents.documents:
156+
for doc in filter(lambda d: d.language_id == "robotframework", self.parent.documents.documents):
157157
check_current_task_canceled()
158158

159159
result.extend(func(doc, *args, **kwargs))

tests/robotcode/language_server/robotframework/parts/data/.vscode/settings.json

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
"robotcode.languageServer.extraArgs": [
1616
// "--debugpy",
1717
// "--debugpy-wait-for-client",
18-
// "--log",
19-
// "--log-level", "TRACE",
18+
"--log",
19+
"--log-level", "INFO",
2020
],
21-
"robotcode.robocop.enabled": true,
21+
"robotcode.robocop.enabled": false,
22+
"robotcode.robocop.exclude": ["E0303"],
2223
// "robotcode.debug.groupOutput": false,
2324
// "robotcode.robot.args": [
2425
// "-L", "TRACE"
@@ -27,17 +28,19 @@
2728
// "robotcode.debug.outputMessages": true
2829
//"python.analysis.diagnosticMode": "workspace"
2930
//"robotcode.robot.paths": ["./tests", "./tests1"]
30-
"robotcode.analysis.referencesCodeLens": true,
31+
"robotcode.analysis.referencesCodeLens": false,
3132
"robotcode.analysis.diagnosticMode": "openFilesOnly",
32-
"robotcode.analysis.progressMode": "off",
33-
"robotcode.analysis.findUnusedReferences": false,
33+
"robotcode.analysis.progressMode": "simple",
34+
"robotcode.analysis.findUnusedReferences": true,
3435
"robotcode.analysis.cache.saveLocation": "workspaceFolder",
3536
"robotcode.analysis.cache.ignoredLibraries": [
3637
//"robot.libraries.Remote",
3738
"**/lib/alibrary.py",
3839
"LibraryWithErrors"
3940
],
4041
// "robotcode.extraArgs": [
42+
// "--debugpy",
43+
// "--debugpy-wait-for-client",
4144
// "--log",
4245
// "--log-level",
4346
// "DEBUG",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[tool.robocop]

0 commit comments

Comments
 (0)