Skip to content

Commit 9001401

Browse files
committed
rework loading and handling sources, implement workspace wide diagnostics
1 parent 3fa22b1 commit 9001401

31 files changed

+823
-332
lines changed

package.json

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"name": "Daniel Biehl",
1010
"url": "https://github.com/d-biehl/"
1111
},
12-
"homepage": "https://github.com/d-biehl/robotcode",
12+
"homepage": "https://robotcode.io",
1313
"repository": {
1414
"type": "git",
1515
"url": "https://github.com/d-biehl/robotcode"
@@ -413,21 +413,6 @@
413413
"description": "Defines if the test report should be opened a run session automatically.",
414414
"scope": "resource"
415415
},
416-
"robotcode.workspace.excludePatterns": {
417-
"type": "array",
418-
"default": [
419-
"**/.git/**",
420-
"**/node_modules/**",
421-
"**/.pytest_cache/**",
422-
"**/__pycache__/**",
423-
"**/.mypy_cache/**"
424-
],
425-
"items": {
426-
"type": "string"
427-
},
428-
"description": "Specifies glob patterns for excluding files and folders from analysing by the language server.",
429-
"scope": "resource"
430-
},
431416
"robotcode.robocop.enabled": {
432417
"type": "boolean",
433418
"default": true,
@@ -466,6 +451,55 @@
466451
"default": true,
467452
"markdownDescription": "Enables 'robotidy' code formatting, if installed. See [robotidy](https://github.com/MarketSquare/robotframework-tidy)",
468453
"scope": "resource"
454+
},
455+
"robotcode.analysis.diagnosticMode": {
456+
"type": "string",
457+
"enum": [
458+
"openFilesOnly",
459+
"workspace"
460+
],
461+
"default": "openFilesOnly",
462+
"enumDescriptions": [
463+
"Analyzes and reports errors only on open files.",
464+
"Analyzes and reports errors on all files in the workspace."
465+
],
466+
"description": "Analysis mode for diagnostics.",
467+
"scope": "resource"
468+
},
469+
"robotcode.analysis.progressMode": {
470+
"type": "string",
471+
"enum": [
472+
"simple",
473+
"detailed"
474+
],
475+
"default": "simple",
476+
"enumDescriptions": [
477+
"Show only simple progress messages.",
478+
"Show detailed progress messages."
479+
],
480+
"description": "Analysis mode for diagnostics.",
481+
"scope": "resource"
482+
},
483+
"robotcode.workspace.excludePatterns": {
484+
"type": "array",
485+
"default": [
486+
"**/.git/**",
487+
"**/node_modules/**",
488+
"**/.pytest_cache/**",
489+
"**/__pycache__/**",
490+
"**/.mypy_cache/**"
491+
],
492+
"items": {
493+
"type": "string"
494+
},
495+
"description": "Specifies glob patterns for excluding files and folders from analysing by the language server.",
496+
"scope": "resource"
497+
},
498+
"robotcode.analysis.maxProjectFileCount": {
499+
"type": "integer",
500+
"default": 1000,
501+
"description": "Specifies the maximum number of files for which diagnostics are reported for the whole project/workspace folder. Specifies 0 or less to disable the limit completely.",
502+
"scope": "resource"
469503
}
470504
}
471505
}
@@ -520,7 +554,7 @@
520554
"debuggers": [
521555
{
522556
"type": "robotcode",
523-
"label": "RobotCode Debugger",
557+
"label": "RobotCode",
524558
"languages": [
525559
"robotframework"
526560
],

robotcode/jsonrpc2/protocol.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,10 @@ def __init__(self) -> None:
341341
self._message_buf = bytes()
342342
self._loop: Optional[asyncio.AbstractEventLoop] = None
343343

344+
@property
345+
def loop(self) -> Optional[asyncio.AbstractEventLoop]:
346+
return self._loop
347+
344348
@async_event
345349
async def on_connection_made(sender, transport: asyncio.BaseTransport) -> None:
346350
...

robotcode/language_server/common/decorators.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Callable, List, Protocol, TypeVar, runtime_checkable
1+
from typing import Any, Callable, List, Protocol, TypeVar, Union, runtime_checkable
22

33
from .text_document import TextDocument
44

@@ -57,8 +57,12 @@ class HasAllCommitCharacters(Protocol):
5757
__all_commit_characters__: List[str]
5858

5959

60-
def language_id_filter(document: TextDocument) -> Callable[[Any], bool]:
60+
def language_id_filter(language_id_or_document: Union[str, TextDocument]) -> Callable[[Any], bool]:
6161
def filter(c: Any) -> bool:
62-
return not isinstance(c, HasLanguageId) or c.__language_id__ == document.language_id
62+
return not isinstance(c, HasLanguageId) or c.__language_id__ == (
63+
language_id_or_document.language_id
64+
if isinstance(language_id_or_document, TextDocument)
65+
else language_id_or_document
66+
)
6367

6468
return filter

robotcode/language_server/common/lsp_types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
from ...utils.dataclasses import to_camel_case, to_snake_case
1010

11-
ProgressToken = Union[str, int]
1211
DocumentUri = str
1312
URI = str
13+
ProgressToken = Union[str, int]
1414

1515

1616
@dataclass(repr=False)

robotcode/language_server/common/parts/diagnostics.py

Lines changed: 129 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import asyncio
44
import itertools
55
from dataclasses import dataclass
6+
from enum import Enum
67
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, cast
78

89
from ....utils.async_tools import (
910
Lock,
11+
async_event,
1012
async_tasking_event_iterator,
1113
check_canceled,
1214
create_sub_task,
@@ -25,7 +27,16 @@
2527

2628
__all__ = ["DiagnosticsProtocolPart", "DiagnosticsResult"]
2729

28-
DIAGNOSTICS_DEBOUNCE = 0.75
30+
31+
class DiagnosticsMode(Enum):
32+
WORKSPACE = "workspace"
33+
OPENFILESONLY = "openFilesOnly"
34+
35+
36+
DOCUMENT_DIAGNOSTICS_DEBOUNCE = 0.75
37+
WORKSPACE_DIAGNOSTICS_DEBOUNCE = 2
38+
39+
WORKSPACE_URI = Uri("workspace:/")
2940

3041

3142
class PublishDiagnosticsEntry:
@@ -38,6 +49,7 @@ def __init__(
3849
factory: Callable[..., asyncio.Future[Any]],
3950
done_callback: Callable[[PublishDiagnosticsEntry], Any],
4051
no_wait: bool = False,
52+
wait_time: float = DOCUMENT_DIAGNOSTICS_DEBOUNCE,
4153
) -> None:
4254

4355
self.uri = uri
@@ -50,6 +62,7 @@ def __init__(
5062

5163
self.done = False
5264
self.no_wait = no_wait
65+
self.wait_time = wait_time
5366

5467
def _done(t: asyncio.Future[Any]) -> None:
5568
self.done = True
@@ -60,7 +73,7 @@ def _done(t: asyncio.Future[Any]) -> None:
6073

6174
async def _wait_and_run(self) -> None:
6275
if not self.no_wait:
63-
await asyncio.sleep(DIAGNOSTICS_DEBOUNCE)
76+
await asyncio.sleep(self.wait_time)
6477

6578
await self._factory()
6679

@@ -110,6 +123,12 @@ class DiagnosticsResult:
110123
diagnostics: Optional[List[Diagnostic]] = None
111124

112125

126+
@dataclass
127+
class WorkspaceDocumentsResult:
128+
name: Optional[str]
129+
document: TextDocument
130+
131+
113132
class DiagnosticsProtocolPart(LanguageServerProtocolPart):
114133
_logger = LoggingDescriptor()
115134

@@ -119,8 +138,10 @@ def __init__(self, protocol: LanguageServerProtocol) -> None:
119138
self._running_diagnostics: Dict[Uri, PublishDiagnosticsEntry] = {}
120139
self._tasks_lock = Lock()
121140

141+
self.parent.on_initialized.add(self.on_initialized)
122142
self.parent.on_connection_lost.add(self.on_connection_lost)
123143
self.parent.on_shutdown.add(self.on_shutdown)
144+
self.parent.documents.did_append_document.add(self.on_did_append_document)
124145
self.parent.documents.did_open.add(self.on_did_open)
125146
self.parent.documents.did_change.add(self.on_did_change)
126147
self.parent.documents.did_close.add(self.on_did_close)
@@ -130,6 +151,10 @@ def __init__(self, protocol: LanguageServerProtocol) -> None:
130151
async def collect(sender, document: TextDocument) -> DiagnosticsResult: # NOSONAR
131152
...
132153

154+
@async_tasking_event_iterator
155+
async def collect_workspace_documents(sender) -> List[WorkspaceDocumentsResult]: # NOSONAR
156+
...
157+
133158
@_logger.call
134159
async def on_connection_lost(self, sender: Any, exc: Optional[BaseException]) -> None:
135160
await self._cancel_all_tasks()
@@ -159,19 +184,49 @@ async def _cancel_entry(self, entry: Optional[PublishDiagnosticsEntry]) -> None:
159184
if not entry.done:
160185
await entry.cancel()
161186

187+
async def create_publish_document_diagnostics_task(
188+
self, document: TextDocument, no_wait: bool = False, wait_time: float = DOCUMENT_DIAGNOSTICS_DEBOUNCE
189+
) -> Optional[asyncio.Task[Any]]:
190+
mode = await self.get_diagnostics_mode(document.uri)
191+
if mode == DiagnosticsMode.WORKSPACE or document.opened_in_editor:
192+
return create_sub_task(
193+
self.start_publish_document_diagnostics_task(document, no_wait, wait_time), loop=self.parent.loop
194+
)
195+
return None
196+
197+
@language_id("robotframework")
198+
@_logger.call
199+
async def on_did_append_document(self, sender: Any, document: TextDocument) -> None:
200+
# self.create_publish_document_diagnostics_task(document)
201+
pass
202+
162203
@language_id("robotframework")
163204
@_logger.call
164205
async def on_did_open(self, sender: Any, document: TextDocument) -> None:
165-
create_sub_task(self.start_publish_diagnostics_task(document))
206+
await self.create_publish_document_diagnostics_task(document)
166207

167208
@language_id("robotframework")
168209
@_logger.call
169210
async def on_did_save(self, sender: Any, document: TextDocument) -> None:
170-
create_sub_task(self.start_publish_diagnostics_task(document))
211+
await self.create_publish_document_diagnostics_task(document)
212+
213+
@async_event
214+
async def on_get_diagnostics_mode(sender, uri: Uri) -> Optional[DiagnosticsMode]: # NOSONAR
215+
...
216+
217+
async def get_diagnostics_mode(self, uri: Uri) -> DiagnosticsMode:
218+
for e in await self.on_get_diagnostics_mode(self, uri):
219+
if e is not None:
220+
return cast(DiagnosticsMode, e)
221+
222+
return DiagnosticsMode.OPENFILESONLY
171223

172224
@language_id("robotframework")
173225
@_logger.call
174226
async def on_did_close(self, sender: Any, document: TextDocument) -> None:
227+
if await self.get_diagnostics_mode(document.uri) == DiagnosticsMode.WORKSPACE:
228+
return
229+
175230
try:
176231
await self._cancel_entry(self._running_diagnostics.get(document.uri, None))
177232
finally:
@@ -184,17 +239,49 @@ async def on_did_close(self, sender: Any, document: TextDocument) -> None:
184239
),
185240
)
186241

242+
@_logger.call
243+
async def on_initialized(self, sender: Any) -> None:
244+
self.create_publish_workspace_diagnostics_task()
245+
187246
@_logger.call
188247
async def on_did_change(self, sender: Any, document: TextDocument) -> None:
189-
create_sub_task(self.start_publish_diagnostics_task(document))
248+
await self.create_publish_document_diagnostics_task(document)
190249

191250
@_logger.call
192251
def _delete_entry(self, e: PublishDiagnosticsEntry) -> None:
193252
if e.uri in self._running_diagnostics and self._running_diagnostics[e.uri] == e:
194253
self._running_diagnostics.pop(e.uri, None)
195254

255+
def create_publish_workspace_diagnostics_task(self, no_wait: bool = False) -> asyncio.Task[Any]:
256+
return create_sub_task(self.start_publish_workspace_diagnostics_task(no_wait), loop=self.parent.loop)
257+
196258
@_logger.call
197-
async def start_publish_diagnostics_task(self, document: TextDocument, no_wait: bool = False) -> None:
259+
async def start_publish_workspace_diagnostics_task(self, no_wait: bool = False) -> None:
260+
async with self._tasks_lock:
261+
entry = self._running_diagnostics.get(WORKSPACE_URI, None)
262+
263+
await self._cancel_entry(entry)
264+
265+
self._running_diagnostics[WORKSPACE_URI] = PublishDiagnosticsEntry(
266+
WORKSPACE_URI,
267+
None,
268+
lambda: run_coroutine_in_thread(self.publish_workspace_diagnostics),
269+
self._delete_entry,
270+
no_wait,
271+
WORKSPACE_DIAGNOSTICS_DEBOUNCE,
272+
)
273+
274+
@_logger.call
275+
async def start_publish_document_diagnostics_task(
276+
self,
277+
document: TextDocument,
278+
no_wait: bool = False,
279+
wait_time: float = DOCUMENT_DIAGNOSTICS_DEBOUNCE,
280+
) -> None:
281+
mode = await self.get_diagnostics_mode(document.uri)
282+
if mode != DiagnosticsMode.WORKSPACE and not document.opened_in_editor:
283+
return
284+
198285
async with self._tasks_lock:
199286
entry = self._running_diagnostics.get(document.uri, None)
200287

@@ -206,14 +293,14 @@ async def start_publish_diagnostics_task(self, document: TextDocument, no_wait:
206293
self._running_diagnostics[document.uri] = PublishDiagnosticsEntry(
207294
document.uri,
208295
document.version,
209-
lambda: run_coroutine_in_thread(self.publish_diagnostics, document.document_uri),
210-
# lambda: create_sub_task(self.publish_diagnostics(document.document_uri)),
296+
lambda: run_coroutine_in_thread(self.publish_document_diagnostics, document.document_uri),
211297
self._delete_entry,
212298
no_wait,
299+
wait_time,
213300
)
214301

215302
@_logger.call
216-
async def publish_diagnostics(self, document_uri: DocumentUri) -> None:
303+
async def publish_document_diagnostics(self, document_uri: DocumentUri) -> None:
217304
document = await self.parent.documents.get(document_uri)
218305
if document is None:
219306
return
@@ -254,3 +341,36 @@ async def publish_diagnostics(self, document_uri: DocumentUri) -> None:
254341
diagnostics.pop(k)
255342

256343
document.set_data(self, diagnostics)
344+
345+
@_logger.call
346+
async def publish_workspace_diagnostics(self) -> None:
347+
canceled = False
348+
349+
async for result_any in self.collect_workspace_documents(
350+
self,
351+
return_exceptions=True,
352+
):
353+
await check_canceled()
354+
if canceled:
355+
break
356+
357+
if isinstance(result_any, BaseException):
358+
if not isinstance(result_any, asyncio.CancelledError):
359+
self._logger.exception(result_any, exc_info=result_any)
360+
continue
361+
362+
result = cast(List[WorkspaceDocumentsResult], result_any)
363+
async with self.parent.window.progress(
364+
"Analyze workspace",
365+
cancellable=True,
366+
current=0,
367+
max=len(result),
368+
) as progress:
369+
for i, v in enumerate(result):
370+
if progress.is_canceled:
371+
canceled = True
372+
break
373+
374+
progress.report(f"Analyze {v.name}" if v.name else None, cancellable=False, current=i + 1)
375+
376+
await self.publish_document_diagnostics(str(v.document.uri))

0 commit comments

Comments
 (0)