Skip to content

Commit 567ac72

Browse files
committed
perf(langserver): speedup semantic highlightning a lot
1 parent 0d5616c commit 567ac72

File tree

153 files changed

+685671
-40712
lines changed

Some content is hidden

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

153 files changed

+685671
-40712
lines changed

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,9 @@
241241
"rules": {
242242
"*.documentation:robotframework": {
243243
"fontStyle": "italic"
244+
},
245+
"*.embedded:robotframework": {
246+
"fontStyle": "italic"
244247
}
245248
}
246249
}
@@ -249,6 +252,10 @@
249252
{
250253
"id": "builtin",
251254
"description": "built in library, keyword or variable"
255+
},
256+
{
257+
"id": "embedded",
258+
"description": "embedded argument"
252259
}
253260
],
254261
"semanticTokenScopes": [
@@ -1650,4 +1657,4 @@
16501657
"workspaces": [
16511658
"docs"
16521659
]
1653-
}
1660+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import collections
2+
import functools
23
import inspect
34
import io
45
import weakref
@@ -47,10 +48,18 @@ def position_from_utf16(lines: List[str], position: Position) -> Position:
4748
return Position(line=position.line, character=utf32_offset)
4849

4950

51+
@functools.lru_cache(maxsize=2048)
52+
def has_multibyte_char(line: str) -> bool:
53+
return any(is_multibyte_char(c) for c in line)
54+
55+
5056
def position_to_utf16(lines: List[str], position: Position) -> Position:
5157
if position.line >= len(lines):
5258
return position
5359

60+
if not has_multibyte_char(lines[position.line]):
61+
return position
62+
5463
utf16_counter = 0
5564

5665
for i, c in enumerate(lines[position.line]):

packages/core/src/robotcode/core/utils/logging.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,10 @@ def measure_time(
258258
self._measure_contexts[context_name] = depth
259259

260260
self._log_measure_time(
261-
level, f"{' '*depth}End {msg() if callable(msg) else msg} took {duration} seconds", *args, **kwargs
261+
level,
262+
f"{' '*depth}End {msg() if callable(msg) else msg} took {duration} seconds",
263+
*args,
264+
**kwargs,
262265
)
263266
else:
264267
yield

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

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from robot.parsing.lexer.tokens import Token
2424
from robot.parsing.model.statements import (
2525
Arguments,
26+
Documentation,
2627
Fixture,
2728
KeywordCall,
2829
LibraryImport,
@@ -62,6 +63,7 @@
6263
from robotcode.robot.diagnostics.namespace import DEFAULT_BDD_PREFIXES, Namespace
6364
from robotcode.robot.utils import get_robot_version
6465
from robotcode.robot.utils.ast import (
66+
cached_isinstance,
6567
iter_nodes,
6668
iter_over_keyword_names_and_owners,
6769
token_in_range,
@@ -120,6 +122,7 @@ class RobotSemTokenTypes(Enum):
120122

121123
class RobotSemTokenModifiers(Enum):
122124
BUILTIN = "builtin"
125+
EMBEDDED = "embedded"
123126

124127

125128
@dataclass
@@ -340,6 +343,7 @@ def generate_sem_sub_tokens(
340343
node: ast.AST,
341344
col_offset: Optional[int] = None,
342345
length: Optional[int] = None,
346+
yield_arguments: bool = False,
343347
) -> Iterator[SemTokenInfo]:
344348
sem_info = cls.mapping().get(token.type, None) if token.type is not None else None
345349
if sem_info is not None:
@@ -391,7 +395,7 @@ def generate_sem_sub_tokens(
391395
yield SemTokenInfo.from_token(token, sem_type, sem_mod)
392396

393397
elif token.type in [Token.KEYWORD, ROBOT_KEYWORD_INNER] or (
394-
token.type == Token.NAME and isinstance(node, (Fixture, Template, TestTemplate))
398+
token.type == Token.NAME and cached_isinstance(node, Fixture, Template, TestTemplate)
395399
):
396400
if (
397401
namespace.find_keyword(
@@ -461,6 +465,9 @@ def generate_sem_sub_tokens(
461465

462466
kw_index = len(kw_namespace) + 1 if kw_namespace else 0
463467

468+
if token.type == Token.NAME and kw_doc is not None:
469+
sem_type = RobotSemTokenTypes.KEYWORD
470+
464471
if kw_namespace:
465472
kw = token.value[kw_index:]
466473

@@ -501,13 +508,25 @@ def generate_sem_sub_tokens(
501508
col_offset + kw_index + start,
502509
arg_start - start,
503510
)
504-
yield SemTokenInfo.from_token(
505-
token,
506-
RobotSemTokenTypes.EMBEDDED_ARGUMENT,
507-
sem_mod,
508-
col_offset + kw_index + arg_start,
509-
arg_end - arg_start,
511+
512+
embedded_token = Token(
513+
Token.ARGUMENT,
514+
token.value[arg_start:arg_end],
515+
token.lineno,
516+
token.col_offset + arg_start,
510517
)
518+
519+
for sub_token in ModelHelper.tokenize_variables(
520+
embedded_token,
521+
ignore_errors=True,
522+
identifiers="$@&%",
523+
):
524+
for e in cls.generate_sem_sub_tokens(
525+
namespace, builtin_library_doc, sub_token, node, yield_arguments=True
526+
):
527+
e.sem_modifiers = {RobotSemTokenModifiers.EMBEDDED}
528+
yield e
529+
511530
start = arg_end + 1
512531

513532
if start < end:
@@ -521,7 +540,7 @@ def generate_sem_sub_tokens(
521540

522541
else:
523542
yield SemTokenInfo.from_token(token, sem_type, sem_mod, col_offset + kw_index, len(kw))
524-
elif token.type == Token.NAME and isinstance(node, (LibraryImport, ResourceImport, VariablesImport)):
543+
elif token.type == Token.NAME and cached_isinstance(node, LibraryImport, ResourceImport, VariablesImport):
525544
if "\\" in token.value:
526545
if col_offset is None:
527546
col_offset = token.col_offset
@@ -543,7 +562,9 @@ def generate_sem_sub_tokens(
543562
length,
544563
)
545564
elif get_robot_version() >= (5, 0) and token.type == Token.OPTION:
546-
if (isinstance(node, ExceptHeader) or isinstance(node, WhileHeader)) and "=" in token.value:
565+
if (
566+
cached_isinstance(node, ExceptHeader) or cached_isinstance(node, WhileHeader)
567+
) and "=" in token.value:
547568
if col_offset is None:
548569
col_offset = token.col_offset
549570

@@ -589,7 +610,12 @@ def generate_sem_sub_tokens(
589610
1,
590611
)
591612
else:
592-
if token.type != Token.ARGUMENT or token.type != Token.NAME and isinstance(node, Metadata):
613+
if (
614+
yield_arguments
615+
or token.type != Token.ARGUMENT
616+
or token.type != Token.NAME
617+
and cached_isinstance(node, Metadata)
618+
):
593619
yield SemTokenInfo.from_token(token, sem_type, sem_mod, col_offset, length)
594620

595621
def generate_sem_tokens(
@@ -602,25 +628,25 @@ def generate_sem_tokens(
602628
if (
603629
token.type in {Token.ARGUMENT, Token.TESTCASE_NAME, Token.KEYWORD_NAME}
604630
or token.type == Token.NAME
605-
and isinstance(node, (VariablesImport, LibraryImport, ResourceImport))
631+
and cached_isinstance(node, VariablesImport, LibraryImport, ResourceImport)
606632
):
607-
if (isinstance(node, Variable) and token.type == Token.ARGUMENT and node.name and node.name[0] == "&") or (
608-
isinstance(node, Arguments)
609-
):
633+
if (
634+
cached_isinstance(node, Variable) and token.type == Token.ARGUMENT and node.name and node.name[0] == "&"
635+
) or (cached_isinstance(node, Arguments)):
610636
name, value = split_from_equals(token.value)
611637
if value is not None:
612638
length = len(name)
613639

614640
yield SemTokenInfo.from_token(
615641
Token(
616-
ROBOT_NAMED_ARGUMENT if isinstance(node, Variable) else SemanticTokenTypes.PARAMETER,
642+
ROBOT_NAMED_ARGUMENT if cached_isinstance(node, Variable) else SemanticTokenTypes.PARAMETER,
617643
name,
618644
token.lineno,
619645
token.col_offset,
620646
),
621647
(
622648
RobotSemTokenTypes.NAMED_ARGUMENT
623-
if isinstance(node, Variable)
649+
if cached_isinstance(node, Variable)
624650
else SemanticTokenTypes.PARAMETER
625651
),
626652
)
@@ -640,7 +666,7 @@ def generate_sem_tokens(
640666
token.col_offset + length + 1,
641667
token.error,
642668
)
643-
elif isinstance(node, Arguments) and name:
669+
elif cached_isinstance(node, Arguments) and name:
644670
yield SemTokenInfo.from_token(
645671
Token(
646672
ROBOT_NAMED_ARGUMENT,
@@ -663,11 +689,13 @@ def generate_sem_tokens(
663689
ignore_errors=True,
664690
identifiers="$" if token.type == Token.KEYWORD_NAME else "$@&%",
665691
):
666-
for e in self.generate_sem_sub_tokens(namespace, builtin_library_doc, sub_token, node):
692+
for e in self.generate_sem_sub_tokens(
693+
namespace, builtin_library_doc, sub_token, node, yield_arguments=True
694+
):
667695
yield e
668696

669697
else:
670-
for e in self.generate_sem_sub_tokens(namespace, builtin_library_doc, token, node):
698+
for e in self.generate_sem_sub_tokens(namespace, builtin_library_doc, token, node, yield_arguments=True):
671699
yield e
672700

673701
def generate_run_kw_tokens(
@@ -956,8 +984,8 @@ def get_tokens() -> Iterator[Tuple[Token, ast.AST]]:
956984
for node in iter_nodes(model):
957985
check_current_task_canceled()
958986

959-
if isinstance(node, Statement):
960-
if isinstance(node, LibraryImport) and node.name:
987+
if cached_isinstance(node, Statement):
988+
if cached_isinstance(node, LibraryImport) and node.name:
961989
lib_doc = namespace.get_imported_library_libdoc(node.name, node.args, node.alias)
962990
kw_doc = lib_doc.inits.keywords[0] if lib_doc and lib_doc.inits else None
963991
if lib_doc is not None:
@@ -1009,7 +1037,7 @@ def get_tokens() -> Iterator[Tuple[Token, ast.AST]]:
10091037

10101038
yield token, node
10111039
continue
1012-
if isinstance(node, VariablesImport) and node.name:
1040+
if cached_isinstance(node, VariablesImport) and node.name:
10131041
lib_doc = namespace.get_imported_variables_libdoc(node.name, node.args)
10141042
kw_doc = lib_doc.inits.keywords[0] if lib_doc and lib_doc.inits else None
10151043
if lib_doc is not None:
@@ -1061,12 +1089,12 @@ def get_tokens() -> Iterator[Tuple[Token, ast.AST]]:
10611089

10621090
yield token, node
10631091
continue
1064-
if isinstance(node, (KeywordCall, Fixture)):
1092+
if cached_isinstance(node, KeywordCall, Fixture):
10651093
kw_token = cast(
10661094
Token,
10671095
(
10681096
node.get_token(Token.KEYWORD)
1069-
if isinstance(node, KeywordCall)
1097+
if cached_isinstance(node, KeywordCall)
10701098
else node.get_token(Token.NAME)
10711099
),
10721100
)
@@ -1109,8 +1137,16 @@ def get_tokens() -> Iterator[Tuple[Token, ast.AST]]:
11091137
yield kw_res
11101138

11111139
continue
1140+
if cached_isinstance(node, Documentation):
1141+
for token in node.tokens:
1142+
if token.type == Token.ARGUMENT:
1143+
continue
1144+
yield token, node
1145+
continue
11121146

11131147
for token in node.tokens:
1148+
if token.type == Token.COMMENT:
1149+
continue
11141150
yield token, node
11151151

11161152
lines = document.get_lines()
@@ -1136,6 +1172,7 @@ def get_tokens() -> Iterator[Tuple[Token, ast.AST]]:
11361172
),
11371173
),
11381174
)
1175+
11391176
token_col_offset = token_range.start.character
11401177
token_length = token_range.end.character - token_range.start.character
11411178

packages/robot/src/robotcode/robot/utils/ast.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import ast
44
import itertools
5-
from typing import Any, Iterator, List, Optional, Sequence, Set, Tuple
5+
from typing import Any, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, TypeVar, Union
6+
7+
from typing_extensions import TypeGuard
68

79
from robot.errors import VariableError
810
from robot.parsing.lexer.tokens import Token
@@ -17,17 +19,42 @@
1719
else:
1820
from robot.variables.search import VariableMatches as VariableIterator
1921

22+
_cached_isinstance_cache: Dict[Tuple[type, Tuple[type, ...]], bool] = {}
23+
24+
_T = TypeVar("_T")
25+
26+
27+
def cached_isinstance(obj: Any, *expected_types: Type[_T]) -> TypeGuard[Union[_T]]:
28+
try:
29+
t = type(obj)
30+
if (t, expected_types) in _cached_isinstance_cache:
31+
return _cached_isinstance_cache[(t, expected_types)]
32+
33+
_cached_isinstance_cache[(t, expected_types)] = result = isinstance(obj, expected_types)
34+
35+
return result
36+
37+
except TypeError:
38+
return False
39+
40+
41+
# def cached_isinstance(obj: Any, *expected_types: type) -> bool:
42+
# try:
43+
# return isinstance(obj, expected_types)
44+
# except TypeError:
45+
# return False
46+
2047

2148
def iter_nodes(node: ast.AST, descendants: bool = True) -> Iterator[ast.AST]:
2249
for _field, value in ast.iter_fields(node):
23-
if isinstance(value, list):
50+
if cached_isinstance(value, list):
2451
for item in value:
25-
if isinstance(item, ast.AST):
52+
if cached_isinstance(item, ast.AST):
2653
yield item
2754
if descendants:
2855
yield from iter_nodes(item)
2956

30-
elif isinstance(value, ast.AST):
57+
elif cached_isinstance(value, ast.AST):
3158
yield value
3259
if descendants:
3360
yield from iter_nodes(value)
@@ -53,7 +80,7 @@ def find_from(cls, model: ast.AST) -> Tuple[Optional[ast.AST], Optional[ast.AST]
5380
return finder.first_statement, finder.last_statement
5481

5582
def visit_Statement(self, statement: ast.AST) -> None: # noqa: N802
56-
if not isinstance(statement, EmptyLine):
83+
if not cached_isinstance(statement, EmptyLine):
5784
if self.first_statement is None:
5885
self.first_statement = statement
5986

@@ -63,7 +90,7 @@ def visit_Statement(self, statement: ast.AST) -> None: # noqa: N802
6390
def _get_non_data_range_from_node(
6491
node: ast.AST, only_start: bool = False, allow_comments: bool = False
6592
) -> Optional[Range]:
66-
if isinstance(node, Statement) and node.tokens:
93+
if cached_isinstance(node, Statement) and node.tokens:
6794
start_token = next(
6895
(
6996
v
@@ -115,7 +142,7 @@ def range_from_node(
115142
allow_comments: bool = False,
116143
) -> Range:
117144
if skip_non_data:
118-
if isinstance(node, Statement) and node.tokens:
145+
if cached_isinstance(node, Statement) and node.tokens:
119146
result = _get_non_data_range_from_node(node, only_start, allow_comments)
120147
if result is not None:
121148
return result

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ filterwarnings = "ignore:.*Using or importing the ABCs from 'collections' instea
142142
testpaths = ["tests"]
143143
junit_suite_name = "robotcode"
144144
# console_output_style = "classic"
145-
# log_cli = true
145+
log_cli = true
146146
# log_cli_level = 4
147147
# log_cli_format = "%(levelname)s %(name)s: %(message)s"
148148
asyncio_mode = "auto"

0 commit comments

Comments
 (0)