sqlakit-lsp 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sqlakit_lsp-0.2.0 → sqlakit_lsp-0.3.0}/PKG-INFO +3 -1
- {sqlakit_lsp-0.2.0 → sqlakit_lsp-0.3.0}/README.md +2 -0
- {sqlakit_lsp-0.2.0 → sqlakit_lsp-0.3.0}/pyproject.toml +1 -1
- {sqlakit_lsp-0.2.0 → sqlakit_lsp-0.3.0}/pyproject.toml.orig +1 -1
- {sqlakit_lsp-0.2.0 → sqlakit_lsp-0.3.0}/sqlakit_lsp/_server.py +238 -55
- {sqlakit_lsp-0.2.0 → sqlakit_lsp-0.3.0}/LICENSE +0 -0
- {sqlakit_lsp-0.2.0 → sqlakit_lsp-0.3.0}/sqlakit_lsp/__init__.py +0 -0
- {sqlakit_lsp-0.2.0 → sqlakit_lsp-0.3.0}/sqlakit_lsp/__main__.py +0 -0
- {sqlakit_lsp-0.2.0 → sqlakit_lsp-0.3.0}/sqlakit_lsp/_cli.py +0 -0
- {sqlakit_lsp-0.2.0 → sqlakit_lsp-0.3.0}/sqlakit_lsp/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlakit-lsp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: The language server for SQLAKit templates.
|
|
5
5
|
Keywords: sqlalchemy,sqlakit,lsp,language-server,sql
|
|
6
6
|
Author: Anton Ruhlov
|
|
@@ -36,6 +36,8 @@ templates. In a `.sql` template:
|
|
|
36
36
|
- on hover, the SQL a call writes, above the macro's signature and docstring
|
|
37
37
|
- go to definition, implementation and references of a macro or a template
|
|
38
38
|
- rename of a macro or a template across the project
|
|
39
|
+
- go to definition of a `:parameter`, to each call of Python that passes it,
|
|
40
|
+
and on hover, the value each call passes
|
|
39
41
|
- the macros' calls and the parameters coloured
|
|
40
42
|
- a parameter no call of the project passes, marked, with a quick fix for a
|
|
41
43
|
name close to one the calls pass
|
|
@@ -9,6 +9,8 @@ templates. In a `.sql` template:
|
|
|
9
9
|
- on hover, the SQL a call writes, above the macro's signature and docstring
|
|
10
10
|
- go to definition, implementation and references of a macro or a template
|
|
11
11
|
- rename of a macro or a template across the project
|
|
12
|
+
- go to definition of a `:parameter`, to each call of Python that passes it,
|
|
13
|
+
and on hover, the value each call passes
|
|
12
14
|
- the macros' calls and the parameters coloured
|
|
13
15
|
- a parameter no call of the project passes, marked, with a quick fix for a
|
|
14
16
|
name close to one the calls pass
|
|
@@ -141,6 +141,8 @@ class Target:
|
|
|
141
141
|
path: Path
|
|
142
142
|
line: int
|
|
143
143
|
column: int = 0
|
|
144
|
+
length: int = 0
|
|
145
|
+
"""How many characters of the line the place spans: a name to select."""
|
|
144
146
|
|
|
145
147
|
|
|
146
148
|
def _innermost(parts: Sequence[Any], offset: int) -> Any: # noqa: ANN401
|
|
@@ -285,6 +287,22 @@ class Reference:
|
|
|
285
287
|
end: int
|
|
286
288
|
|
|
287
289
|
|
|
290
|
+
@dataclass(frozen=True, slots=True)
|
|
291
|
+
class Passing:
|
|
292
|
+
"""A call of Python that passes a template's parameter."""
|
|
293
|
+
|
|
294
|
+
path: Path
|
|
295
|
+
line: int
|
|
296
|
+
start: int
|
|
297
|
+
end: int
|
|
298
|
+
"""Where the value is: its keyword, or the code that passes values the call
|
|
299
|
+
does not name, `values` in `**values`."""
|
|
300
|
+
code: str
|
|
301
|
+
"""What the call writes, `page_size=limit` or `**values`."""
|
|
302
|
+
named: bool = True
|
|
303
|
+
"""Whether the call names the parameter, and does not only may pass it."""
|
|
304
|
+
|
|
305
|
+
|
|
288
306
|
def _read(path: Path) -> str:
|
|
289
307
|
return path.read_text(encoding="utf-8", errors="replace")
|
|
290
308
|
|
|
@@ -578,6 +596,21 @@ class _Assistant:
|
|
|
578
596
|
"""
|
|
579
597
|
if name in self._passed:
|
|
580
598
|
return self._passed[name]
|
|
599
|
+
calls = [call for _, call in self._calls_reading(name)]
|
|
600
|
+
passed = (
|
|
601
|
+
None
|
|
602
|
+
if not calls or any(call.open for call in calls)
|
|
603
|
+
else frozenset(keyword.name for call in calls for keyword in call.keywords)
|
|
604
|
+
)
|
|
605
|
+
self._passed[name] = passed
|
|
606
|
+
return passed
|
|
607
|
+
|
|
608
|
+
def _calls_reading(self, name: str) -> list[tuple[Path, _TemplateCall]]:
|
|
609
|
+
"""Return the calls of Python that read a template, and their files.
|
|
610
|
+
|
|
611
|
+
A call that reads a template that includes it, however deep, reads it
|
|
612
|
+
too.
|
|
613
|
+
"""
|
|
581
614
|
included_by = self._included_by()
|
|
582
615
|
readers, waiting = {name}, [name]
|
|
583
616
|
while waiting:
|
|
@@ -585,19 +618,12 @@ class _Assistant:
|
|
|
585
618
|
if including not in readers:
|
|
586
619
|
readers.add(including)
|
|
587
620
|
waiting.append(including)
|
|
588
|
-
|
|
589
|
-
call
|
|
621
|
+
return [
|
|
622
|
+
(path, call)
|
|
590
623
|
for path in self._project_files()[1]
|
|
591
|
-
for reader in readers
|
|
624
|
+
for reader in sorted(readers)
|
|
592
625
|
for call in self._scan(path, None).calls.get(reader, ())
|
|
593
626
|
]
|
|
594
|
-
passed = (
|
|
595
|
-
None
|
|
596
|
-
if not calls or any(call.open for call in calls)
|
|
597
|
-
else frozenset(keyword for call in calls for keyword, _, _ in call.keywords)
|
|
598
|
-
)
|
|
599
|
-
self._passed[name] = passed
|
|
600
|
-
return passed
|
|
601
627
|
|
|
602
628
|
def _included_by(self) -> dict[str, set[str]]:
|
|
603
629
|
"""Return, for each template, the templates that include it."""
|
|
@@ -900,23 +926,37 @@ class _Assistant:
|
|
|
900
926
|
|
|
901
927
|
def passes(
|
|
902
928
|
self, name: str, param: str
|
|
903
|
-
) -> tuple[list[
|
|
929
|
+
) -> tuple[list[Passing], list[tuple[Path, int]]]:
|
|
904
930
|
"""Return the calls of Python that pass a template's parameter, and not.
|
|
905
931
|
|
|
906
|
-
|
|
907
|
-
`**values` or a context, counts as one that
|
|
932
|
+
A call that does not pass it is a file and a line. A call that passes
|
|
933
|
+
values it does not name, `**values` or a context, counts as one that
|
|
934
|
+
may pass it. The calls of a template that includes this one count too.
|
|
908
935
|
"""
|
|
909
936
|
passing, silent = [], []
|
|
910
|
-
for path in self.
|
|
911
|
-
for
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
937
|
+
for path, call in self._calls_reading(name):
|
|
938
|
+
keyword = next((one for one in call.keywords if one.name == param), None)
|
|
939
|
+
if keyword is not None:
|
|
940
|
+
code = f"{param}={keyword.value}"
|
|
941
|
+
passing.append(
|
|
942
|
+
Passing(path, keyword.line, keyword.start, keyword.end, code)
|
|
943
|
+
)
|
|
944
|
+
elif call.open:
|
|
945
|
+
passing.extend(
|
|
946
|
+
Passing(path, one.line, one.start, one.end, one.value, named=False)
|
|
947
|
+
for one in call.unnamed
|
|
948
|
+
)
|
|
949
|
+
else:
|
|
950
|
+
silent.append((path, call.line))
|
|
915
951
|
return passing, silent
|
|
916
952
|
|
|
917
|
-
def
|
|
918
|
-
|
|
953
|
+
def _parameter_at(
|
|
954
|
+
self, path: Path, source: str, offset: int
|
|
955
|
+
) -> tuple[str, re.Match[str]] | None:
|
|
956
|
+
"""Return the template and the `:parameter` under the offset."""
|
|
919
957
|
name = self.project.name_of(path)
|
|
958
|
+
if name is None:
|
|
959
|
+
return None
|
|
920
960
|
found = next(
|
|
921
961
|
(
|
|
922
962
|
match
|
|
@@ -925,15 +965,23 @@ class _Assistant:
|
|
|
925
965
|
),
|
|
926
966
|
None,
|
|
927
967
|
)
|
|
928
|
-
if
|
|
968
|
+
return None if found is None else (name, found)
|
|
969
|
+
|
|
970
|
+
def parameter_hover(self, path: Path, source: str, offset: int) -> str | None:
|
|
971
|
+
"""Return which calls pass the parameter under the offset, what, and which not."""
|
|
972
|
+
at = self._parameter_at(path, source, offset)
|
|
973
|
+
if at is None:
|
|
929
974
|
return None
|
|
975
|
+
name, found = at
|
|
930
976
|
param = found.group(1)
|
|
931
977
|
passing, silent = self.passes(name, param)
|
|
932
978
|
root = self.project.root
|
|
933
979
|
|
|
934
|
-
def listed(places:
|
|
980
|
+
def listed(places: Sequence[tuple[Path, int, str | None]]) -> str:
|
|
935
981
|
shown = [
|
|
936
|
-
f"- `{_relative(path, root)}:{line}`"
|
|
982
|
+
f"- `{_relative(path, root)}:{line}`"
|
|
983
|
+
+ ("" if code is None else f" `{code}`")
|
|
984
|
+
for path, line, code in places[:_LISTED]
|
|
937
985
|
]
|
|
938
986
|
if len(places) > _LISTED:
|
|
939
987
|
shown.append(f"- and {len(places) - _LISTED} more")
|
|
@@ -941,13 +989,41 @@ class _Assistant:
|
|
|
941
989
|
|
|
942
990
|
parts = [f"`:{param}` of `{name}`"]
|
|
943
991
|
if passing:
|
|
944
|
-
|
|
992
|
+
given = [(one.path, one.line, one.code) for one in passing]
|
|
993
|
+
parts.append(f"Passed by {len(passing)}:\n\n{listed(given)}")
|
|
945
994
|
if silent:
|
|
946
|
-
|
|
995
|
+
missing = [(path, line, None) for path, line in silent]
|
|
996
|
+
parts.append(f"Not passed by {len(silent)}:\n\n{listed(missing)}")
|
|
947
997
|
if not passing and not silent:
|
|
948
998
|
parts.append("No call in the project's Python reads this template.")
|
|
949
999
|
return "\n\n".join(parts)
|
|
950
1000
|
|
|
1001
|
+
def parameter_definitions(
|
|
1002
|
+
self, path: Path, source: str, offset: int
|
|
1003
|
+
) -> list[Target]:
|
|
1004
|
+
"""Return where the Python passes the parameter under the offset.
|
|
1005
|
+
|
|
1006
|
+
That is the keyword of each call that names it. Only when none does, it
|
|
1007
|
+
is the code of the calls that pass values they do not name: `values` in
|
|
1008
|
+
`**values`, or a context that is not written out.
|
|
1009
|
+
"""
|
|
1010
|
+
at = self._parameter_at(path, source, offset)
|
|
1011
|
+
if at is None:
|
|
1012
|
+
return []
|
|
1013
|
+
name, found = at
|
|
1014
|
+
passing = self.passes(name, found.group(1))[0]
|
|
1015
|
+
named = [one for one in passing if one.named]
|
|
1016
|
+
texts: dict[Path, str] = {}
|
|
1017
|
+
targets = []
|
|
1018
|
+
for one in named or passing:
|
|
1019
|
+
text = texts.get(one.path)
|
|
1020
|
+
if text is None:
|
|
1021
|
+
text = texts[one.path] = _read(one.path)
|
|
1022
|
+
line = text.count("\n", 0, one.start)
|
|
1023
|
+
column = one.start - (text.rfind("\n", 0, one.start) + 1)
|
|
1024
|
+
targets.append(Target(one.path, line, column, one.end - one.start))
|
|
1025
|
+
return targets
|
|
1026
|
+
|
|
951
1027
|
def signature(self, source: str, offset: int) -> Signature | None:
|
|
952
1028
|
"""Return the macro whose arguments the offset is in, and which one it is."""
|
|
953
1029
|
start = max(0, source.rfind("\n\n", 0, offset))
|
|
@@ -1064,6 +1140,9 @@ class _Assistant:
|
|
|
1064
1140
|
for match in self._name_at.finditer(source, start, end):
|
|
1065
1141
|
if match.start() <= offset <= match.end():
|
|
1066
1142
|
return match.span()
|
|
1143
|
+
for match in _PARAMETERS.finditer(source, start, end):
|
|
1144
|
+
if match.start() <= offset <= match.end():
|
|
1145
|
+
return match.span()
|
|
1067
1146
|
return defined
|
|
1068
1147
|
|
|
1069
1148
|
def reads_python(self, path: Path) -> bool:
|
|
@@ -1098,12 +1177,13 @@ class _Assistant:
|
|
|
1098
1177
|
listed = ", ".join(f"`{name}`" for name in sorted(reads)) or "nothing"
|
|
1099
1178
|
found.extend(
|
|
1100
1179
|
Diagnostic(
|
|
1101
|
-
start,
|
|
1102
|
-
end,
|
|
1103
|
-
f"`{name}` is not a parameter of `{call.name}`,
|
|
1180
|
+
keyword.start,
|
|
1181
|
+
keyword.end,
|
|
1182
|
+
f"`{keyword.name}` is not a parameter of `{call.name}`, "
|
|
1183
|
+
f"which reads {listed}.",
|
|
1104
1184
|
)
|
|
1105
|
-
for
|
|
1106
|
-
if name not in reads
|
|
1185
|
+
for keyword in call.keywords
|
|
1186
|
+
if keyword.name not in reads
|
|
1107
1187
|
)
|
|
1108
1188
|
return found
|
|
1109
1189
|
|
|
@@ -1543,23 +1623,69 @@ _ARGUMENT_TYPED = re.compile(
|
|
|
1543
1623
|
_PASSED = re.compile(r"(\w+)\s*=")
|
|
1544
1624
|
|
|
1545
1625
|
|
|
1626
|
+
_SHOWN = 60
|
|
1627
|
+
"""The most characters of a value's code a hover shows."""
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
@dataclass(frozen=True, slots=True)
|
|
1631
|
+
class _Keyword:
|
|
1632
|
+
"""A value a call passes by name, where the name is written, and its code."""
|
|
1633
|
+
|
|
1634
|
+
name: str
|
|
1635
|
+
start: int
|
|
1636
|
+
end: int
|
|
1637
|
+
value: str
|
|
1638
|
+
line: int
|
|
1639
|
+
|
|
1640
|
+
|
|
1546
1641
|
@dataclass(frozen=True, slots=True)
|
|
1547
1642
|
class _TemplateCall:
|
|
1548
1643
|
"""A call that reads a template, and the values it passes by name."""
|
|
1549
1644
|
|
|
1550
1645
|
name: str
|
|
1551
|
-
keywords: tuple[
|
|
1552
|
-
"""Each keyword
|
|
1646
|
+
keywords: tuple[_Keyword, ...]
|
|
1647
|
+
"""Each keyword, and where it is written."""
|
|
1553
1648
|
open: bool
|
|
1554
1649
|
"""Whether it passes values the code does not name: `**values` or a context."""
|
|
1555
1650
|
line: int = 1
|
|
1556
1651
|
span: tuple[int, int] | None = None
|
|
1557
1652
|
"""Where the template's name is written, without its quotes."""
|
|
1653
|
+
unnamed: tuple[_Keyword, ...] = ()
|
|
1654
|
+
"""The code that passes values it does not name: where `values` of
|
|
1655
|
+
`**values` is, and all of it written, or a context's."""
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
def _unnamed(node: ast.expr, written: str, source: str, starts: list[int]) -> _Keyword:
|
|
1659
|
+
"""Return code that passes values it does not name, as a keyword with no name."""
|
|
1660
|
+
start = _char_offset(source, starts, node.lineno, node.col_offset)
|
|
1661
|
+
end = _char_offset(
|
|
1662
|
+
source,
|
|
1663
|
+
starts,
|
|
1664
|
+
node.end_lineno or node.lineno,
|
|
1665
|
+
node.end_col_offset or node.col_offset,
|
|
1666
|
+
)
|
|
1667
|
+
code = _code(node, source, starts)
|
|
1668
|
+
# The name the editor selects: the value, or the start of a long one.
|
|
1669
|
+
end = end if source.count("\n", start, end) == 0 else source.find("\n", start)
|
|
1670
|
+
return _Keyword("", start, end, written.format(code), node.lineno)
|
|
1671
|
+
|
|
1672
|
+
|
|
1673
|
+
def _code(node: ast.expr, source: str, starts: list[int]) -> str:
|
|
1674
|
+
"""Return the code of an expression on one line, cut short when it is long."""
|
|
1675
|
+
start = _char_offset(source, starts, node.lineno, node.col_offset)
|
|
1676
|
+
end = _char_offset(
|
|
1677
|
+
source,
|
|
1678
|
+
starts,
|
|
1679
|
+
node.end_lineno or node.lineno,
|
|
1680
|
+
node.end_col_offset or node.col_offset,
|
|
1681
|
+
)
|
|
1682
|
+
text = " ".join(source[start:end].split())
|
|
1683
|
+
return text if len(text) <= _SHOWN else f"{text[: _SHOWN - 1]}…"
|
|
1558
1684
|
|
|
1559
1685
|
|
|
1560
1686
|
def _named_keys(
|
|
1561
1687
|
node: ast.expr, source: str, starts: list[int]
|
|
1562
|
-
) -> list[
|
|
1688
|
+
) -> list[_Keyword] | None:
|
|
1563
1689
|
"""Return the keys of a dict written out, and where each is, or None.
|
|
1564
1690
|
|
|
1565
1691
|
None is a context the code does not write out: a name, a call, or a dict
|
|
@@ -1568,11 +1694,19 @@ def _named_keys(
|
|
|
1568
1694
|
if not isinstance(node, ast.Dict) or any(key is None for key in node.keys):
|
|
1569
1695
|
return None
|
|
1570
1696
|
found = []
|
|
1571
|
-
for key in node.keys:
|
|
1697
|
+
for key, value in zip(node.keys, node.values, strict=True):
|
|
1572
1698
|
if not (isinstance(key, ast.Constant) and isinstance(key.value, str)):
|
|
1573
1699
|
return None
|
|
1574
1700
|
start = _char_offset(source, starts, key.lineno, key.col_offset) + 1
|
|
1575
|
-
found.append(
|
|
1701
|
+
found.append(
|
|
1702
|
+
_Keyword(
|
|
1703
|
+
key.value,
|
|
1704
|
+
start,
|
|
1705
|
+
start + len(key.value),
|
|
1706
|
+
_code(value, source, starts),
|
|
1707
|
+
key.lineno,
|
|
1708
|
+
)
|
|
1709
|
+
)
|
|
1576
1710
|
return found
|
|
1577
1711
|
|
|
1578
1712
|
|
|
@@ -1611,7 +1745,15 @@ def _template_calls(source: str) -> list[_TemplateCall]:
|
|
|
1611
1745
|
if keyword.arg is None or keyword.arg == "context":
|
|
1612
1746
|
continue
|
|
1613
1747
|
start = _char_offset(source, starts, keyword.lineno, keyword.col_offset)
|
|
1614
|
-
keywords.append(
|
|
1748
|
+
keywords.append(
|
|
1749
|
+
_Keyword(
|
|
1750
|
+
keyword.arg,
|
|
1751
|
+
start,
|
|
1752
|
+
start + len(keyword.arg),
|
|
1753
|
+
_code(keyword.value, source, starts),
|
|
1754
|
+
keyword.lineno,
|
|
1755
|
+
)
|
|
1756
|
+
)
|
|
1615
1757
|
# A context written out as a dict names its values as keywords do.
|
|
1616
1758
|
contexts = [
|
|
1617
1759
|
*node.args[1:2],
|
|
@@ -1620,6 +1762,28 @@ def _template_calls(source: str) -> list[_TemplateCall]:
|
|
|
1620
1762
|
written = [_named_keys(context, source, starts) for context in contexts]
|
|
1621
1763
|
for keys in written:
|
|
1622
1764
|
keywords.extend(keys or ())
|
|
1765
|
+
unnamed = [
|
|
1766
|
+
*(
|
|
1767
|
+
_unnamed(context, "{}", source, starts)
|
|
1768
|
+
for context, keys in zip(node.args[1:2], written, strict=False)
|
|
1769
|
+
if keys is None
|
|
1770
|
+
),
|
|
1771
|
+
*(
|
|
1772
|
+
_unnamed(keyword.value, "context={}", source, starts)
|
|
1773
|
+
for keyword, keys in zip(
|
|
1774
|
+
[one for one in node.keywords if one.arg == "context"],
|
|
1775
|
+
written[len(node.args[1:2]) :],
|
|
1776
|
+
strict=True,
|
|
1777
|
+
)
|
|
1778
|
+
if keys is None
|
|
1779
|
+
),
|
|
1780
|
+
*(_unnamed(one, "{}", source, starts) for one in node.args[2:]),
|
|
1781
|
+
*(
|
|
1782
|
+
_unnamed(keyword.value, "**{}", source, starts)
|
|
1783
|
+
for keyword in node.keywords
|
|
1784
|
+
if keyword.arg is None
|
|
1785
|
+
),
|
|
1786
|
+
]
|
|
1623
1787
|
named = node.args[0]
|
|
1624
1788
|
value = str(named.value) if isinstance(named, ast.Constant) else ""
|
|
1625
1789
|
span = None
|
|
@@ -1630,13 +1794,7 @@ def _template_calls(source: str) -> list[_TemplateCall]:
|
|
|
1630
1794
|
span = None if quote < 0 else (quote, quote + len(value))
|
|
1631
1795
|
found.append(
|
|
1632
1796
|
_TemplateCall(
|
|
1633
|
-
value,
|
|
1634
|
-
tuple(keywords),
|
|
1635
|
-
any(keys is None for keys in written)
|
|
1636
|
-
or len(node.args) > 2 # noqa: PLR2004 - the name and a context
|
|
1637
|
-
or any(keyword.arg is None for keyword in node.keywords),
|
|
1638
|
-
node.lineno,
|
|
1639
|
-
span,
|
|
1797
|
+
value, tuple(keywords), bool(unnamed), node.lineno, span, tuple(unnamed)
|
|
1640
1798
|
)
|
|
1641
1799
|
)
|
|
1642
1800
|
return found
|
|
@@ -2113,26 +2271,40 @@ def _server() -> Any: # noqa: ANN401, C901, PLR0915 - a handler for each reques
|
|
|
2113
2271
|
if found is None:
|
|
2114
2272
|
return None
|
|
2115
2273
|
path = _path(params.text_document.uri)
|
|
2116
|
-
|
|
2117
|
-
found.helper.python_definition(found.source, found.offset, path)
|
|
2274
|
+
if found.python:
|
|
2275
|
+
target = found.helper.python_definition(found.source, found.offset, path)
|
|
2276
|
+
else:
|
|
2277
|
+
target = found.helper.definition(found.source, found.offset, path)
|
|
2278
|
+
# A parameter is defined where the Python passes it, by each call.
|
|
2279
|
+
targets = (
|
|
2280
|
+
[target]
|
|
2281
|
+
if target is not None
|
|
2282
|
+
else []
|
|
2118
2283
|
if found.python
|
|
2119
|
-
else found.helper.
|
|
2284
|
+
else found.helper.parameter_definitions(path, found.source, found.offset)
|
|
2120
2285
|
)
|
|
2121
|
-
if
|
|
2286
|
+
if not targets:
|
|
2122
2287
|
return None
|
|
2123
|
-
|
|
2124
|
-
|
|
2288
|
+
places = []
|
|
2289
|
+
for one in targets:
|
|
2290
|
+
start, end = _utf16_columns(one)
|
|
2291
|
+
place = types.Range(
|
|
2292
|
+
types.Position(one.line, start), types.Position(one.line, end)
|
|
2293
|
+
)
|
|
2294
|
+
places.append((one.path.resolve().as_uri(), place))
|
|
2125
2295
|
span = found.helper.origin(
|
|
2126
2296
|
found.source, found.offset, python=found.python, path=path
|
|
2127
2297
|
)
|
|
2128
2298
|
if span is None or not links_supported(ls):
|
|
2129
|
-
|
|
2299
|
+
if len(places) == 1:
|
|
2300
|
+
return types.Location(*places[0])
|
|
2301
|
+
return [types.Location(uri, place) for uri, place in places]
|
|
2130
2302
|
# A link carries the span it starts from, which the editor underlines.
|
|
2131
2303
|
origin = types.Range(
|
|
2132
2304
|
types.Position(*position_of(found.source, span[0])),
|
|
2133
2305
|
types.Position(*position_of(found.source, span[1])),
|
|
2134
2306
|
)
|
|
2135
|
-
return [types.LocationLink(uri, place, place, origin)]
|
|
2307
|
+
return [types.LocationLink(uri, place, place, origin) for uri, place in places]
|
|
2136
2308
|
|
|
2137
2309
|
def links_supported(ls: LanguageServer) -> bool:
|
|
2138
2310
|
"""Whether the editor takes a definition as a link with the span it starts from."""
|
|
@@ -2499,13 +2671,24 @@ def _server() -> Any: # noqa: ANN401, C901, PLR0915 - a handler for each reques
|
|
|
2499
2671
|
|
|
2500
2672
|
def _utf16_column(target: Target) -> int:
|
|
2501
2673
|
"""Return a target's column in the UTF-16 units the protocol counts in."""
|
|
2502
|
-
|
|
2503
|
-
|
|
2674
|
+
return _utf16_columns(target)[0]
|
|
2675
|
+
|
|
2676
|
+
|
|
2677
|
+
def _utf16_columns(target: Target) -> tuple[int, int]:
|
|
2678
|
+
"""Return where a target starts and ends on its line, in UTF-16 units."""
|
|
2679
|
+
end = target.column + target.length
|
|
2680
|
+
if not end:
|
|
2681
|
+
return 0, 0
|
|
2504
2682
|
try:
|
|
2505
2683
|
line = _lines(target.path, target.path.stat().st_mtime_ns)[target.line]
|
|
2506
2684
|
except (OSError, IndexError):
|
|
2507
|
-
return target.column
|
|
2508
|
-
|
|
2685
|
+
return target.column, end
|
|
2686
|
+
if line.isascii():
|
|
2687
|
+
return target.column, end
|
|
2688
|
+
units = [
|
|
2689
|
+
len(line[:column].encode("utf-16-le")) // 2 for column in (target.column, end)
|
|
2690
|
+
]
|
|
2691
|
+
return units[0], units[1]
|
|
2509
2692
|
|
|
2510
2693
|
|
|
2511
2694
|
@lru_cache(maxsize=64)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|