Skip to content

Commit c94edb3

Browse files
committed
refactor: move diagnostics helper code to robotcode.robot package
1 parent a107b95 commit c94edb3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+1417
-1230
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "robotcode",
3-
"displayName": "Robot Code - Robot Framework Support",
3+
"displayName": "RobotCode - Robot Framework Support",
44
"description": "Robot Framework IntelliSense, linting, test execution and debugging, code formatting, refactoring, and many more",
55
"icon": "images/icon.png",
66
"publisher": "d-biehl",
@@ -541,7 +541,7 @@
541541
"items": {
542542
"type": "string"
543543
},
544-
"markdownDescription": "Specifies the library names that should not be cached. This is useful if you have a dynamic or hybrid library that has different keywords depending on the arguments. You can specify a glob pattern that matches the library name or the source file. \n\nExamples:\n- `**/mylibfolder/mylib.py`\n- `MyLib`\n- `mylib.subpackage.subpackage` \n\nFor robot framework internal libraries, you have to specify the full module name like `robot.libraries.Remote`.\n\nIf you change this setting, you may need to run the command `Robot Code: Clear Cache and Restart Language Servers`.",
544+
"markdownDescription": "Specifies the library names that should not be cached. This is useful if you have a dynamic or hybrid library that has different keywords depending on the arguments. You can specify a glob pattern that matches the library name or the source file. \n\nExamples:\n- `**/mylibfolder/mylib.py`\n- `MyLib`\n- `mylib.subpackage.subpackage` \n\nFor robot framework internal libraries, you have to specify the full module name like `robot.libraries.Remote`.\n\nIf you change this setting, you may need to run the command `RobotCode: Clear Cache and Restart Language Servers`.",
545545
"scope": "resource"
546546
},
547547
"robotcode.analysis.cache.ignoredVariables": {
@@ -550,7 +550,7 @@
550550
"items": {
551551
"type": "string"
552552
},
553-
"markdownDescription": "Specifies the variable files that should not be cached. This is useful if you have a dynamic or hybrid variable files that has different variables depending on the arguments. You can specify a glob pattern that matches the variable module name or the source file. \n\nExamples:\n- `**/variables/myvars.py`\n- `MyVariables`\n- `myvars.subpackage.subpackage` \n\nIf you change this setting, you may need to run the command `Robot Code: Clear Cache and Restart Language Servers`.",
553+
"markdownDescription": "Specifies the variable files that should not be cached. This is useful if you have a dynamic or hybrid variable files that has different variables depending on the arguments. You can specify a glob pattern that matches the variable module name or the source file. \n\nExamples:\n- `**/variables/myvars.py`\n- `MyVariables`\n- `myvars.subpackage.subpackage` \n\nIf you change this setting, you may need to run the command `RobotCode: Clear Cache and Restart Language Servers`.",
554554
"scope": "resource"
555555
},
556556
"robotcode.run.openOutputAfterRun": {

packages/analyze/src/robotcode/analyze/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def analyze(app: Application, paths: Tuple[str]) -> Union[str, int, None]:
3030
config_files, root_folder, _ = get_config_files(paths, app.config.config_files, verbose_callback=app.verbose)
3131

3232
try:
33-
analizer_config = load_config_from_path(
33+
analyzer_config = load_config_from_path(
3434
AnalyzerConfig,
3535
*config_files,
3636
tool_name="robotcode-analyze",
@@ -44,7 +44,7 @@ def analyze(app: Application, paths: Tuple[str]) -> Union[str, int, None]:
4444
except (TypeError, ValueError) as e:
4545
raise click.ClickException(str(e)) from e
4646

47-
app.print_data(analizer_config)
47+
app.print_data(analyzer_config)
4848
app.print_data(robot_config)
4949

5050
return 0
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import os
2+
import re
3+
import threading
4+
from pathlib import Path
5+
from typing import (
6+
Any,
7+
Callable,
8+
Dict,
9+
Final,
10+
Iterator,
11+
List,
12+
Optional,
13+
Union,
14+
)
15+
16+
from .event import event
17+
from .language import LanguageDefinition, language_id_filter
18+
from .lsp.types import DocumentUri
19+
from .text_document import TextDocument
20+
from .uri import Uri
21+
from .utils.logging import LoggingDescriptor
22+
23+
24+
class CantReadDocumentError(Exception):
25+
pass
26+
27+
28+
class DocumentsManager:
29+
_logger: Final = LoggingDescriptor()
30+
31+
def __init__(self, languages: List[LanguageDefinition]) -> None:
32+
self.languages = languages
33+
34+
self._documents: Dict[DocumentUri, TextDocument] = {}
35+
self._lock = threading.RLock()
36+
37+
@property
38+
def documents(self) -> List[TextDocument]:
39+
return list(self._documents.values())
40+
41+
__NORMALIZE_LINE_ENDINGS: Final = re.compile(r"(\r?\n)")
42+
43+
@classmethod
44+
def _normalize_line_endings(cls, text: str) -> str:
45+
return cls.__NORMALIZE_LINE_ENDINGS.sub("\n", text)
46+
47+
def read_document_text(self, uri: Uri, language_id: Union[str, Callable[[Any], bool], None]) -> str:
48+
for e in self.on_read_document_text(
49+
self,
50+
uri,
51+
callback_filter=language_id_filter(language_id) if isinstance(language_id, str) else language_id,
52+
):
53+
if isinstance(e, BaseException):
54+
raise RuntimeError(f"Can't read document text from {uri}: {e}") from e
55+
56+
if e is not None:
57+
return self._normalize_line_endings(e)
58+
59+
raise FileNotFoundError(str(uri))
60+
61+
def detect_language_id(self, path_or_uri: Union[str, "os.PathLike[Any]", Uri]) -> str:
62+
path = path_or_uri.to_path() if isinstance(path_or_uri, Uri) else Path(path_or_uri)
63+
64+
for lang in self.languages:
65+
suffix = path.suffix
66+
if lang.extensions_ignore_case:
67+
suffix = suffix.lower()
68+
if suffix in lang.extensions:
69+
return lang.id
70+
71+
return "unknown"
72+
73+
@_logger.call
74+
def get_or_open_document(
75+
self,
76+
path: Union[str, "os.PathLike[Any]"],
77+
language_id: Optional[str] = None,
78+
version: Optional[int] = None,
79+
) -> TextDocument:
80+
uri = Uri.from_path(path).normalized()
81+
82+
result = self.get(uri)
83+
if result is not None:
84+
return result
85+
86+
try:
87+
return self._append_document(
88+
document_uri=DocumentUri(uri),
89+
language_id=language_id or self.detect_language_id(path),
90+
text=self.read_document_text(uri, language_id),
91+
version=version,
92+
)
93+
except (SystemExit, KeyboardInterrupt):
94+
raise
95+
except BaseException as e:
96+
raise CantReadDocumentError(f"Error reading document '{path}': {e!s}") from e
97+
98+
@event
99+
def on_read_document_text(sender, uri: Uri) -> Optional[str]:
100+
...
101+
102+
@event
103+
def did_create_uri(sender, uri: DocumentUri) -> None:
104+
...
105+
106+
@event
107+
def did_create(sender, document: TextDocument) -> None:
108+
...
109+
110+
@event
111+
def did_open(sender, document: TextDocument) -> None:
112+
...
113+
114+
@event
115+
def did_close(sender, document: TextDocument, full_close: bool) -> None:
116+
...
117+
118+
@event
119+
def did_change(sender, document: TextDocument) -> None:
120+
...
121+
122+
@event
123+
def did_save(sender, document: TextDocument) -> None:
124+
...
125+
126+
def get(self, _uri: Union[DocumentUri, Uri]) -> Optional[TextDocument]:
127+
with self._lock:
128+
return self._documents.get(
129+
str(Uri(_uri).normalized() if not isinstance(_uri, Uri) else _uri),
130+
None,
131+
)
132+
133+
def __len__(self) -> int:
134+
return self._documents.__len__()
135+
136+
def __iter__(self) -> Iterator[DocumentUri]:
137+
return self._documents.__iter__()
138+
139+
@event
140+
def on_document_cache_invalidate(sender, document: TextDocument) -> None:
141+
...
142+
143+
def _on_document_cache_invalidate(self, sender: TextDocument) -> None:
144+
self.on_document_cache_invalidate(self, sender)
145+
146+
@event
147+
def on_document_cache_invalidated(sender, document: TextDocument) -> None:
148+
...
149+
150+
def _on_document_cache_invalidated(self, sender: TextDocument) -> None:
151+
self.on_document_cache_invalidated(self, sender)
152+
153+
def _create_document(
154+
self,
155+
document_uri: DocumentUri,
156+
text: str,
157+
language_id: Optional[str] = None,
158+
version: Optional[int] = None,
159+
) -> TextDocument:
160+
result = TextDocument(
161+
document_uri=document_uri,
162+
language_id=language_id,
163+
text=text,
164+
version=version,
165+
)
166+
167+
result.cache_invalidate.add(self._on_document_cache_invalidate)
168+
result.cache_invalidated.add(self._on_document_cache_invalidated)
169+
170+
return result
171+
172+
def _append_document(
173+
self,
174+
document_uri: DocumentUri,
175+
language_id: str,
176+
text: str,
177+
version: Optional[int] = None,
178+
) -> TextDocument:
179+
with self._lock:
180+
document = self._create_document(
181+
document_uri=document_uri,
182+
language_id=language_id,
183+
text=text,
184+
version=version,
185+
)
186+
187+
self._documents[document_uri] = document
188+
189+
return document
190+
191+
@_logger.call
192+
def close_document(self, document: TextDocument, real_close: bool = False) -> None:
193+
document._version = None
194+
195+
if real_close:
196+
with self._lock:
197+
self._documents.pop(str(document.uri), None)
198+
199+
document.clear()
200+
else:
201+
if document.revert(None):
202+
self.did_change(self, document, callback_filter=language_id_filter(document))
203+
204+
self.did_close(self, document, real_close, callback_filter=language_id_filter(document))
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Any, Callable, List, NamedTuple, Optional, Tuple, Union
3+
from uuid import uuid4
4+
5+
from robotcode.core.event import event
6+
from robotcode.core.lsp.types import FileEvent, WatchKind
7+
8+
9+
class FileWatcher(NamedTuple):
10+
glob_pattern: str
11+
kind: Optional[WatchKind] = None
12+
13+
14+
class FileWatcherEntry:
15+
def __init__(
16+
self,
17+
id: str,
18+
callback: Callable[[Any, List[FileEvent]], None],
19+
watchers: List[FileWatcher],
20+
) -> None:
21+
self.id = id
22+
self.callback = callback
23+
self.watchers = watchers
24+
self.parent: Optional[FileWatcherEntry] = None
25+
self.finalizer: Any = None
26+
27+
@event
28+
def child_callbacks(sender, changes: List[FileEvent]) -> None:
29+
...
30+
31+
def call_childrens(self, sender: Any, changes: List[FileEvent]) -> None:
32+
self.child_callbacks(sender, changes)
33+
34+
def __str__(self) -> str:
35+
return self.id
36+
37+
def __repr__(self) -> str:
38+
return f"{type(self).__qualname__}(id={self.id!r}, watchers={self.watchers!r})"
39+
40+
41+
class FileWatcherManagerBase(ABC):
42+
def add_file_watcher(
43+
self,
44+
callback: Callable[[Any, List[FileEvent]], None],
45+
glob_pattern: str,
46+
kind: Optional[WatchKind] = None,
47+
) -> FileWatcherEntry:
48+
return self.add_file_watchers(callback, [(glob_pattern, kind)])
49+
50+
@abstractmethod
51+
def add_file_watchers(
52+
self,
53+
callback: Callable[[Any, List[FileEvent]], None],
54+
watchers: List[Union[FileWatcher, str, Tuple[str, Optional[WatchKind]]]],
55+
) -> FileWatcherEntry:
56+
...
57+
58+
@abstractmethod
59+
def remove_file_watcher_entry(self, entry: FileWatcherEntry) -> None:
60+
...
61+
62+
63+
class FileWatcherManagerDummy(FileWatcherManagerBase):
64+
def add_file_watchers(
65+
self,
66+
callback: Callable[[Any, List[FileEvent]], None],
67+
watchers: List[Union[FileWatcher, str, Tuple[str, Optional[WatchKind]]]],
68+
) -> FileWatcherEntry:
69+
_watchers = [
70+
e if isinstance(e, FileWatcher) else FileWatcher(*e) if isinstance(e, tuple) else FileWatcher(e)
71+
for e in watchers
72+
]
73+
74+
return FileWatcherEntry(f"dummy-{uuid4()}", callback, _watchers)
75+
76+
def remove_file_watcher_entry(self, entry: FileWatcherEntry) -> None:
77+
pass
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from dataclasses import dataclass
2+
from typing import Any, Callable, List, Optional, TypeVar, Union
3+
4+
from robotcode.core.text_document import TextDocument
5+
6+
_F = TypeVar("_F", bound=Callable[..., Any])
7+
8+
LANGUAGE_ID_ATTR = "__language_id__"
9+
10+
11+
def language_id(id: str, *ids: str) -> Callable[[_F], _F]:
12+
def decorator(func: _F) -> _F:
13+
setattr(func, LANGUAGE_ID_ATTR, {id, *ids})
14+
return func
15+
16+
return decorator
17+
18+
19+
def language_id_filter(
20+
language_id_or_document: Union[str, TextDocument],
21+
) -> Callable[[Any], bool]:
22+
def filter(c: Any) -> bool:
23+
return not hasattr(c, LANGUAGE_ID_ATTR) or (
24+
(
25+
language_id_or_document.language_id
26+
if isinstance(language_id_or_document, TextDocument)
27+
else language_id_or_document
28+
)
29+
in getattr(c, LANGUAGE_ID_ATTR)
30+
)
31+
32+
return filter
33+
34+
35+
@dataclass
36+
class LanguageDefinition:
37+
id: str
38+
extensions: List[str]
39+
extensions_ignore_case: bool = False
40+
aliases: Optional[List[str]] = None

packages/core/src/robotcode/core/text_document.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ def get_lines(self) -> List[str]:
209209
return self._lines
210210

211211
@event
212-
async def cache_invalidate(sender) -> None:
212+
def cache_invalidate(sender) -> None:
213213
...
214214

215215
@event
@@ -238,7 +238,7 @@ def __remove_cache_entry(self, ref: Any) -> None:
238238
if ref in self._cache:
239239
self._cache.pop(ref)
240240

241-
def __get_cache_reference(self, entry: Callable[..., Any], /, *, add_remove: bool = True) -> weakref.ref[Any]:
241+
def __get_cache_reference(self, entry: Callable[..., Any], /, *, add_remove: bool = True) -> "weakref.ref[Any]":
242242
if inspect.ismethod(entry):
243243
return weakref.WeakMethod(entry, self.__remove_cache_entry if add_remove else None)
244244

0 commit comments

Comments
 (0)