skyforge-lsp 0.1.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.
- skyforge_lsp-0.1.0/PKG-INFO +60 -0
- skyforge_lsp-0.1.0/README.md +39 -0
- skyforge_lsp-0.1.0/pyproject.toml +42 -0
- skyforge_lsp-0.1.0/setup.cfg +4 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp/__init__.py +3 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp/__main__.py +50 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp/documents.py +68 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp/features/__init__.py +1 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp/features/completion.py +76 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp/features/definition.py +98 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp/features/diagnostics.py +58 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp/features/document_symbols.py +28 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp/features/hover.py +54 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp/reference.py +424 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp/server.py +119 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp/symbols.py +54 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp.egg-info/PKG-INFO +60 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp.egg-info/SOURCES.txt +23 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp.egg-info/dependency_links.txt +1 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp.egg-info/entry_points.txt +2 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp.egg-info/requires.txt +5 -0
- skyforge_lsp-0.1.0/src/skyforge_lsp.egg-info/top_level.txt +1 -0
- skyforge_lsp-0.1.0/tests/test_diagnostics.py +11 -0
- skyforge_lsp-0.1.0/tests/test_reference.py +16 -0
- skyforge_lsp-0.1.0/tests/test_symbols.py +21 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: skyforge-lsp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Language Server Protocol для языка SkyForge
|
|
5
|
+
Author: SkyForge
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/skytech-alt/SkyForge-Docs
|
|
8
|
+
Project-URL: Repository, https://github.com/skytech-alt/SkyForge-Docs
|
|
9
|
+
Keywords: skyforge,lsp,language-server,ide,editor
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Natural Language :: Russian
|
|
12
|
+
Classifier: Topic :: Software Development :: Interpreters
|
|
13
|
+
Classifier: Topic :: Text Editors :: Integrated Development Environments (IDE)
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: pygls>=2.0.0
|
|
18
|
+
Requires-Dist: skyforge-lang>=1.0.6
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
21
|
+
|
|
22
|
+
# skyforge-lsp
|
|
23
|
+
|
|
24
|
+
Language Server Protocol для языка [SkyForge](https://github.com/skytech-alt/SkyForge-Docs).
|
|
25
|
+
|
|
26
|
+
## Возможности
|
|
27
|
+
|
|
28
|
+
- Диагностика — синтаксические ошибки подсвечиваются прямо при наборе
|
|
29
|
+
- Автодополнение — ключевые слова, встроенные функции, символы текущего файла
|
|
30
|
+
- Hover — сигнатура и описание при наведении
|
|
31
|
+
- Переход к определению — F12 на функции, классы, переменные
|
|
32
|
+
- Outline — структура файла в боковой панели (Ctrl+Shift+O)
|
|
33
|
+
|
|
34
|
+
## Установка
|
|
35
|
+
|
|
36
|
+
pip install skyforge-lsp
|
|
37
|
+
|
|
38
|
+
## Использование
|
|
39
|
+
|
|
40
|
+
LSP-клиент в редакторе запускает skyforge-lsp --stdio как фоновый процесс.
|
|
41
|
+
|
|
42
|
+
Ручной запуск для отладки:
|
|
43
|
+
|
|
44
|
+
skyforge-lsp --stdio
|
|
45
|
+
skyforge-lsp --tcp 2087
|
|
46
|
+
|
|
47
|
+
## VS Code
|
|
48
|
+
|
|
49
|
+
Расширение с этим сервером — в репозитории SkyForge-Docs.
|
|
50
|
+
|
|
51
|
+
## Разработка
|
|
52
|
+
|
|
53
|
+
git clone https://github.com/skytech-alt/SkyForge-Docs
|
|
54
|
+
cd SkyForge-Docs/skyforge-lsp
|
|
55
|
+
pip install -e ".[dev]"
|
|
56
|
+
pytest
|
|
57
|
+
|
|
58
|
+
## Лицензия
|
|
59
|
+
|
|
60
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# skyforge-lsp
|
|
2
|
+
|
|
3
|
+
Language Server Protocol для языка [SkyForge](https://github.com/skytech-alt/SkyForge-Docs).
|
|
4
|
+
|
|
5
|
+
## Возможности
|
|
6
|
+
|
|
7
|
+
- Диагностика — синтаксические ошибки подсвечиваются прямо при наборе
|
|
8
|
+
- Автодополнение — ключевые слова, встроенные функции, символы текущего файла
|
|
9
|
+
- Hover — сигнатура и описание при наведении
|
|
10
|
+
- Переход к определению — F12 на функции, классы, переменные
|
|
11
|
+
- Outline — структура файла в боковой панели (Ctrl+Shift+O)
|
|
12
|
+
|
|
13
|
+
## Установка
|
|
14
|
+
|
|
15
|
+
pip install skyforge-lsp
|
|
16
|
+
|
|
17
|
+
## Использование
|
|
18
|
+
|
|
19
|
+
LSP-клиент в редакторе запускает skyforge-lsp --stdio как фоновый процесс.
|
|
20
|
+
|
|
21
|
+
Ручной запуск для отладки:
|
|
22
|
+
|
|
23
|
+
skyforge-lsp --stdio
|
|
24
|
+
skyforge-lsp --tcp 2087
|
|
25
|
+
|
|
26
|
+
## VS Code
|
|
27
|
+
|
|
28
|
+
Расширение с этим сервером — в репозитории SkyForge-Docs.
|
|
29
|
+
|
|
30
|
+
## Разработка
|
|
31
|
+
|
|
32
|
+
git clone https://github.com/skytech-alt/SkyForge-Docs
|
|
33
|
+
cd SkyForge-Docs/skyforge-lsp
|
|
34
|
+
pip install -e ".[dev]"
|
|
35
|
+
pytest
|
|
36
|
+
|
|
37
|
+
## Лицензия
|
|
38
|
+
|
|
39
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "skyforge-lsp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Language Server Protocol для языка SkyForge"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "SkyForge" }]
|
|
13
|
+
keywords = ["skyforge", "lsp", "language-server", "ide", "editor"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Natural Language :: Russian",
|
|
17
|
+
"Topic :: Software Development :: Interpreters",
|
|
18
|
+
"Topic :: Text Editors :: Integrated Development Environments (IDE)",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
dependencies = [
|
|
23
|
+
"pygls>=2.0.0",
|
|
24
|
+
"skyforge-lang>=1.0.6",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/skytech-alt/SkyForge-Docs"
|
|
29
|
+
Repository = "https://github.com/skytech-alt/SkyForge-Docs"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
skyforge-lsp = "skyforge_lsp.__main__:main"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = ["pytest>=7.0"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
include = ["skyforge_lsp*"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Точка входа: skyforge-lsp [--stdio | --tcp PORT] [--version] [--help]"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from . import __version__
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
USAGE = """\
|
|
8
|
+
skyforge-lsp — Language Server Protocol для SkyForge.
|
|
9
|
+
|
|
10
|
+
Использование:
|
|
11
|
+
skyforge-lsp --stdio Запуск через stdio (как ждёт большинство редакторов)
|
|
12
|
+
skyforge-lsp --tcp PORT Запуск через TCP на 127.0.0.1:PORT (для отладки)
|
|
13
|
+
skyforge-lsp --version Показать версию
|
|
14
|
+
skyforge-lsp --help Показать эту справку
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main(argv=None) -> int:
|
|
19
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
20
|
+
|
|
21
|
+
if "--help" in argv or "-h" in argv:
|
|
22
|
+
print(USAGE)
|
|
23
|
+
return 0
|
|
24
|
+
|
|
25
|
+
if "--version" in argv:
|
|
26
|
+
print(f"skyforge-lsp {__version__}")
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
from .server import create_server
|
|
30
|
+
server = create_server()
|
|
31
|
+
|
|
32
|
+
if "--tcp" in argv:
|
|
33
|
+
idx = argv.index("--tcp")
|
|
34
|
+
if idx + 1 >= len(argv):
|
|
35
|
+
print("Ошибка: --tcp требует номер порта", file=sys.stderr)
|
|
36
|
+
return 2
|
|
37
|
+
try:
|
|
38
|
+
port = int(argv[idx + 1])
|
|
39
|
+
except ValueError:
|
|
40
|
+
print(f"Ошибка: '{argv[idx + 1]}' не является номером порта", file=sys.stderr)
|
|
41
|
+
return 2
|
|
42
|
+
print(f"skyforge-lsp {__version__} — TCP 127.0.0.1:{port}", flush=True)
|
|
43
|
+
server.start_tcp("127.0.0.1", port)
|
|
44
|
+
else:
|
|
45
|
+
server.start_io()
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
sys.exit(main())
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Кэш открытых .skf с ленивым парсингом."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Document:
|
|
7
|
+
def __init__(self, uri: str, text: str, version: int = 0):
|
|
8
|
+
self.uri = uri
|
|
9
|
+
self.text = text
|
|
10
|
+
self.version = version
|
|
11
|
+
self._ast = self._tokens = self._parse_error = None
|
|
12
|
+
self._parsed_version = -1
|
|
13
|
+
|
|
14
|
+
def invalidate(self) -> None:
|
|
15
|
+
self._ast = self._tokens = self._parse_error = None
|
|
16
|
+
self._parsed_version = -1
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def ast(self):
|
|
20
|
+
self._ensure()
|
|
21
|
+
return self._ast
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def parse_error(self) -> Optional[Exception]:
|
|
25
|
+
self._ensure()
|
|
26
|
+
return self._parse_error
|
|
27
|
+
|
|
28
|
+
def _ensure(self) -> None:
|
|
29
|
+
if self._parsed_version == self.version:
|
|
30
|
+
return
|
|
31
|
+
try:
|
|
32
|
+
from skyforge.core import Lexer, Parser
|
|
33
|
+
self._tokens = Lexer(self.text).tokenize()
|
|
34
|
+
self._ast = Parser(self._tokens).parse()
|
|
35
|
+
self._parse_error = None
|
|
36
|
+
except Exception as e:
|
|
37
|
+
self._ast = self._tokens = None
|
|
38
|
+
self._parse_error = e
|
|
39
|
+
self._parsed_version = self.version
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DocumentStore:
|
|
43
|
+
def __init__(self):
|
|
44
|
+
self._docs: Dict[str, Document] = {}
|
|
45
|
+
|
|
46
|
+
def open(self, uri: str, text: str, version: int = 0) -> Document:
|
|
47
|
+
d = Document(uri, text, version)
|
|
48
|
+
self._docs[uri] = d
|
|
49
|
+
return d
|
|
50
|
+
|
|
51
|
+
def apply_change(self, uri: str, text: str, version: int) -> Optional[Document]:
|
|
52
|
+
"""Применить изменение. В режиме Full text — это полный текст файла."""
|
|
53
|
+
d = self._docs.get(uri)
|
|
54
|
+
if d is None:
|
|
55
|
+
return self.open(uri, text, version)
|
|
56
|
+
d.text = text
|
|
57
|
+
d.version = version
|
|
58
|
+
d.invalidate()
|
|
59
|
+
return d
|
|
60
|
+
|
|
61
|
+
def close(self, uri: str) -> None:
|
|
62
|
+
self._docs.pop(uri, None)
|
|
63
|
+
|
|
64
|
+
def get(self, uri: str) -> Optional[Document]:
|
|
65
|
+
return self._docs.get(uri)
|
|
66
|
+
|
|
67
|
+
def all(self) -> List[Document]:
|
|
68
|
+
return list(self._docs.values())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""LSP-фичи."""
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Автодополнение."""
|
|
2
|
+
|
|
3
|
+
from lsprotocol.types import (
|
|
4
|
+
CompletionItem, CompletionItemKind, CompletionList, Position, Range,
|
|
5
|
+
)
|
|
6
|
+
from .. import reference
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _word(text: str, line: int, char: int):
|
|
10
|
+
lines = text.splitlines()
|
|
11
|
+
if line >= len(lines):
|
|
12
|
+
return "", char
|
|
13
|
+
src = lines[line]
|
|
14
|
+
char = min(char, len(src))
|
|
15
|
+
i = char
|
|
16
|
+
while i > 0 and (src[i - 1].isalnum() or src[i - 1] == "_"):
|
|
17
|
+
i -= 1
|
|
18
|
+
return src[i:char], i
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _after_dot(text: str, line: int, char: int) -> bool:
|
|
22
|
+
lines = text.splitlines()
|
|
23
|
+
if line >= len(lines):
|
|
24
|
+
return False
|
|
25
|
+
src = lines[line]
|
|
26
|
+
char = min(char, len(src))
|
|
27
|
+
i = char - 1
|
|
28
|
+
while i >= 0 and src[i] in " \t":
|
|
29
|
+
i -= 1
|
|
30
|
+
return i >= 0 and src[i] == "."
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def complete(store, params) -> CompletionList:
|
|
34
|
+
doc = store.get(params.text_document.uri)
|
|
35
|
+
if doc is None:
|
|
36
|
+
return CompletionList(is_incomplete=False, items=[])
|
|
37
|
+
line, char = params.position.line, params.position.character
|
|
38
|
+
_, start = _word(doc.text, line, char)
|
|
39
|
+
|
|
40
|
+
items = []
|
|
41
|
+
if _after_dot(doc.text, line, char):
|
|
42
|
+
for r in reference.methods():
|
|
43
|
+
items.append(CompletionItem(
|
|
44
|
+
label=r.name,
|
|
45
|
+
kind=CompletionItemKind.Method,
|
|
46
|
+
detail=r.signature,
|
|
47
|
+
documentation=r.description or None,
|
|
48
|
+
sort_text="1_" + r.name,
|
|
49
|
+
))
|
|
50
|
+
else:
|
|
51
|
+
for r in reference.keywords():
|
|
52
|
+
items.append(CompletionItem(
|
|
53
|
+
label=r.name,
|
|
54
|
+
kind=CompletionItemKind.Keyword,
|
|
55
|
+
detail=r.description or r.name,
|
|
56
|
+
sort_text="0_" + r.name,
|
|
57
|
+
))
|
|
58
|
+
for r in reference.builtins():
|
|
59
|
+
items.append(CompletionItem(
|
|
60
|
+
label=r.name,
|
|
61
|
+
kind=CompletionItemKind.Function,
|
|
62
|
+
detail=r.description or r.name,
|
|
63
|
+
sort_text="2_" + r.name,
|
|
64
|
+
))
|
|
65
|
+
from ..symbols import extract_symbols
|
|
66
|
+
for s in extract_symbols(doc.ast):
|
|
67
|
+
kind = {
|
|
68
|
+
"function": CompletionItemKind.Function,
|
|
69
|
+
"class": CompletionItemKind.Class,
|
|
70
|
+
"variable": CompletionItemKind.Variable,
|
|
71
|
+
}.get(s.kind, CompletionItemKind.Text)
|
|
72
|
+
items.append(CompletionItem(
|
|
73
|
+
label=s.name, kind=kind, detail=s.signature(),
|
|
74
|
+
sort_text="1_" + s.name,
|
|
75
|
+
))
|
|
76
|
+
return CompletionList(is_incomplete=False, items=items)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Переход к определению (F12)."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
from lsprotocol.types import Location, Position, Range
|
|
6
|
+
from ..symbols import extract_symbols
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _word_at(text: str, line: int, char: int) -> Optional[str]:
|
|
10
|
+
lines = text.splitlines()
|
|
11
|
+
if line < 0 or line >= len(lines):
|
|
12
|
+
return None
|
|
13
|
+
src = lines[line]
|
|
14
|
+
char = min(char, len(src))
|
|
15
|
+
i = char
|
|
16
|
+
while i > 0 and (src[i - 1].isalnum() or src[i - 1] == "_"):
|
|
17
|
+
i -= 1
|
|
18
|
+
j = char
|
|
19
|
+
while j < len(src) and (src[j].isalnum() or src[j] == "_"):
|
|
20
|
+
j += 1
|
|
21
|
+
if i == j:
|
|
22
|
+
return None
|
|
23
|
+
return src[i:j]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _pos_from_offset(text: str, offset: int):
|
|
27
|
+
line = text.count("\n", 0, offset)
|
|
28
|
+
last_nl = text.rfind("\n", 0, offset)
|
|
29
|
+
col = offset - (last_nl + 1)
|
|
30
|
+
return line, col
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _find_in_text(text: str, word: str) -> List[Location]:
|
|
34
|
+
"""Regex-fallback: ищет объявления функции/класса/переменной в тексте.
|
|
35
|
+
|
|
36
|
+
Нужен, если AST не проставил _line/_col (в SkyForge 1.0.6 это часто так).
|
|
37
|
+
"""
|
|
38
|
+
out = []
|
|
39
|
+
patterns = [
|
|
40
|
+
# функция имя(...)
|
|
41
|
+
rf"\bфункция\s+({re.escape(word)})\b",
|
|
42
|
+
# функ имя(...)
|
|
43
|
+
rf"\bфунк\s+({re.escape(word)})\b",
|
|
44
|
+
# класс Имя
|
|
45
|
+
rf"\bкласс\s+({re.escape(word)})\b",
|
|
46
|
+
# пусть имя = ...
|
|
47
|
+
rf"\bпусть\s+({re.escape(word)})\b",
|
|
48
|
+
# имя = ... (переприсваивание)
|
|
49
|
+
rf"(?<![\w]){re.escape(word)}(?=\s*=)",
|
|
50
|
+
]
|
|
51
|
+
seen = set()
|
|
52
|
+
for pat in patterns:
|
|
53
|
+
for m in re.finditer(pat, text):
|
|
54
|
+
start = m.start(1)
|
|
55
|
+
if start in seen:
|
|
56
|
+
continue
|
|
57
|
+
seen.add(start)
|
|
58
|
+
line, col = _pos_from_offset(text, start)
|
|
59
|
+
out.append(Location(
|
|
60
|
+
uri=None, # подставим ниже
|
|
61
|
+
range=Range(
|
|
62
|
+
Position(line, col),
|
|
63
|
+
Position(line, col + len(word)),
|
|
64
|
+
),
|
|
65
|
+
))
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def goto_definition(store, params) -> List[Location]:
|
|
70
|
+
uri = params.text_document.uri
|
|
71
|
+
doc = store.get(uri)
|
|
72
|
+
if doc is None:
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
word = _word_at(doc.text, params.position.line, params.position.character)
|
|
76
|
+
if not word:
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
out: List[Location] = []
|
|
80
|
+
|
|
81
|
+
# Проход 1: через AST (если он есть и _line/_col корректные)
|
|
82
|
+
for s in extract_symbols(doc.ast):
|
|
83
|
+
if s.name == word:
|
|
84
|
+
out.append(Location(
|
|
85
|
+
uri=uri,
|
|
86
|
+
range=Range(
|
|
87
|
+
Position(s.line, s.col),
|
|
88
|
+
Position(s.end_line, s.end_col),
|
|
89
|
+
),
|
|
90
|
+
))
|
|
91
|
+
|
|
92
|
+
# Проход 2: regex по тексту — работает всегда, независимо от AST
|
|
93
|
+
if not out:
|
|
94
|
+
for loc in _find_in_text(doc.text, word):
|
|
95
|
+
loc.uri = uri # type: ignore
|
|
96
|
+
out.append(loc)
|
|
97
|
+
|
|
98
|
+
return out
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Синтаксические ошибки → LSP diagnostics."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List
|
|
5
|
+
from lsprotocol.types import (
|
|
6
|
+
Diagnostic, DiagnosticSeverity, Position, Range,
|
|
7
|
+
PublishDiagnosticsParams,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
_POS = re.compile(r"на (\d+):(\d+)\s*$")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_diagnostics(doc) -> List[Diagnostic]:
|
|
14
|
+
if doc.parse_error is None:
|
|
15
|
+
return []
|
|
16
|
+
msg = str(doc.parse_error)
|
|
17
|
+
m = _POS.search(msg)
|
|
18
|
+
line, col = (int(m.group(1)), int(m.group(2))) if m else (1, 1)
|
|
19
|
+
clean = _POS.sub("", msg).strip() or "Синтаксическая ошибка"
|
|
20
|
+
l, c = max(0, line - 1), max(0, col - 1)
|
|
21
|
+
return [Diagnostic(
|
|
22
|
+
range=Range(Position(l, c), Position(l, c + 1)),
|
|
23
|
+
message=clean,
|
|
24
|
+
severity=DiagnosticSeverity.Error,
|
|
25
|
+
source="skyforge",
|
|
26
|
+
)]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _send(server, params: PublishDiagnosticsParams) -> None:
|
|
30
|
+
"""Отправить диагностику. Поддерживает pygls 1.x и 2.x."""
|
|
31
|
+
# pygls 2.x: у сервера есть .protocol с низкоуровневым notify
|
|
32
|
+
if hasattr(server, "protocol") and hasattr(server.protocol, "notify"):
|
|
33
|
+
server.protocol.notify("textDocument/publishDiagnostics", params)
|
|
34
|
+
return
|
|
35
|
+
# pygls 1.x: удобный метод publish_diagnostics
|
|
36
|
+
if hasattr(server, "publish_diagnostics"):
|
|
37
|
+
server.publish_diagnostics(
|
|
38
|
+
params.uri, params.diagnostics, version=params.version,
|
|
39
|
+
)
|
|
40
|
+
return
|
|
41
|
+
raise RuntimeError("Не могу отправить диагностику: сервер не поддерживает notify")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def publish(server, store, uri: str) -> None:
|
|
45
|
+
doc = store.get(uri)
|
|
46
|
+
if doc is None:
|
|
47
|
+
return
|
|
48
|
+
params = PublishDiagnosticsParams(
|
|
49
|
+
uri=uri,
|
|
50
|
+
diagnostics=build_diagnostics(doc),
|
|
51
|
+
version=doc.version,
|
|
52
|
+
)
|
|
53
|
+
_send(server, params)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def clear(server, uri: str) -> None:
|
|
57
|
+
params = PublishDiagnosticsParams(uri=uri, diagnostics=[], version=None)
|
|
58
|
+
_send(server, params)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Outline документа."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
from lsprotocol.types import DocumentSymbol, SymbolKind, Range, Position
|
|
5
|
+
from ..symbols import extract_symbols
|
|
6
|
+
|
|
7
|
+
_KIND = {
|
|
8
|
+
"function": SymbolKind.Function,
|
|
9
|
+
"class": SymbolKind.Class,
|
|
10
|
+
"variable": SymbolKind.Variable,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def symbols(store, params) -> List[DocumentSymbol]:
|
|
15
|
+
doc = store.get(params.text_document.uri)
|
|
16
|
+
if doc is None:
|
|
17
|
+
return []
|
|
18
|
+
out = []
|
|
19
|
+
for s in extract_symbols(doc.ast):
|
|
20
|
+
rng = Range(Position(s.line, s.col), Position(s.end_line, s.end_col))
|
|
21
|
+
out.append(DocumentSymbol(
|
|
22
|
+
name=s.name,
|
|
23
|
+
kind=_KIND.get(s.kind, SymbolKind.Variable),
|
|
24
|
+
range=rng,
|
|
25
|
+
selection_range=rng,
|
|
26
|
+
detail=s.signature(),
|
|
27
|
+
))
|
|
28
|
+
return out
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Hover-документация."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from lsprotocol.types import Hover, MarkupContent, MarkupKind, Position, Range
|
|
5
|
+
from .. import reference
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _word(text: str, line: int, char: int):
|
|
9
|
+
lines = text.splitlines()
|
|
10
|
+
if line >= len(lines):
|
|
11
|
+
return None, 0, 0
|
|
12
|
+
src = lines[line]
|
|
13
|
+
char = min(char, len(src))
|
|
14
|
+
i = char
|
|
15
|
+
while i > 0 and (src[i - 1].isalnum() or src[i - 1] == "_"):
|
|
16
|
+
i -= 1
|
|
17
|
+
j = char
|
|
18
|
+
while j < len(src) and (src[j].isalnum() or src[j] == "_"):
|
|
19
|
+
j += 1
|
|
20
|
+
return (src[i:j] if i != j else None), i, j
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def hover(store, params) -> Optional[Hover]:
|
|
24
|
+
doc = store.get(params.text_document.uri)
|
|
25
|
+
if doc is None:
|
|
26
|
+
return None
|
|
27
|
+
word, start, end = _word(doc.text, params.position.line, params.position.character)
|
|
28
|
+
if not word:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
rng = Range(
|
|
32
|
+
Position(params.position.line, start),
|
|
33
|
+
Position(params.position.line, end),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
ref = reference.by_name().get(word)
|
|
37
|
+
if ref:
|
|
38
|
+
parts = [f"**{ref.name}**", "", ref.description or "_нет описания_"]
|
|
39
|
+
return Hover(
|
|
40
|
+
contents=MarkupContent(kind=MarkupKind.Markdown, value="\n".join(parts)),
|
|
41
|
+
range=rng,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
from ..symbols import extract_symbols
|
|
45
|
+
for s in extract_symbols(doc.ast):
|
|
46
|
+
if s.name == word:
|
|
47
|
+
return Hover(
|
|
48
|
+
contents=MarkupContent(
|
|
49
|
+
kind=MarkupKind.Markdown,
|
|
50
|
+
value=f"**{s.signature()}**\n\nОпределено в этом файле",
|
|
51
|
+
),
|
|
52
|
+
range=rng,
|
|
53
|
+
)
|
|
54
|
+
return None
|