agent-codinglanguage-mapper 1.0.0__py3-none-any.whl
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.
- agent_codinglanguage_mapper/__init__.py +20 -0
- agent_codinglanguage_mapper/__main__.py +4 -0
- agent_codinglanguage_mapper/_version.py +2 -0
- agent_codinglanguage_mapper/adapters/__init__.py +4 -0
- agent_codinglanguage_mapper/adapters/delphi.py +822 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/__init__.py +6 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/comment_builder.py +29 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/consts.py +345 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/grammar.py +460 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lark_builder.py +2674 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lark_tokens.py +237 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lsp_server.py +1832 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/nodes.py +371 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/parser.py +193 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/preprocessor.py +997 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/project_discovery.py +518 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/project_indexer.py +319 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/semantic.py +375 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/semantic_builder.py +1384 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/source_reader.py +17 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/workspace.py +67 -0
- agent_codinglanguage_mapper/adapters/tree_sitter.py +1722 -0
- agent_codinglanguage_mapper/cli.py +138 -0
- agent_codinglanguage_mapper/discovery.py +1328 -0
- agent_codinglanguage_mapper/indexer.py +1102 -0
- agent_codinglanguage_mapper/integration.py +186 -0
- agent_codinglanguage_mapper/lsp_server.py +413 -0
- agent_codinglanguage_mapper/lsp_service.py +447 -0
- agent_codinglanguage_mapper/mapper.py +596 -0
- agent_codinglanguage_mapper/models.py +101 -0
- agent_codinglanguage_mapper/protocol.py +152 -0
- agent_codinglanguage_mapper/rendering.py +153 -0
- agent_codinglanguage_mapper/templates/opencode/plugins/codebase_map.ts +191 -0
- agent_codinglanguage_mapper/templates/skill/SKILL.md +52 -0
- agent_codinglanguage_mapper/worker.py +29 -0
- agent_codinglanguage_mapper/workspace.py +114 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/METADATA +383 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/RECORD +42 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/WHEEL +5 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/entry_points.txt +2 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/licenses/LICENSE +373 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from tempfile import NamedTemporaryFile
|
|
7
|
+
from typing import Any, Final
|
|
8
|
+
|
|
9
|
+
AGENT_NAME: Final[str] = "mixed-codebase-navigator"
|
|
10
|
+
WORKTREE_PERMISSIONS: Final[dict[str, str]] = {
|
|
11
|
+
"*": "deny",
|
|
12
|
+
"skill": "allow",
|
|
13
|
+
"codebase_map": "allow",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
SKILL_RELATIVE_PATH: Final[Path] = Path(".agents/skills/mixed-codebase-navigator/SKILL.md")
|
|
17
|
+
OPENCODE_PLUGIN_RELATIVE_PATH: Final[Path] = Path(".opencode/plugins/codebase_map.ts")
|
|
18
|
+
OPENCODE_CONFIG_RELATIVE_PATH: Final[Path] = Path("opencode.json")
|
|
19
|
+
|
|
20
|
+
SKILL_TEMPLATE_PATH: Final[Path] = Path("skill", "SKILL.md")
|
|
21
|
+
OPENCODE_PLUGIN_TEMPLATE_PATH: Final[Path] = Path("opencode", "plugins", "codebase_map.ts")
|
|
22
|
+
_TEMPLATES_ROOT: Final[Path] = Path(__file__).resolve().parent / "templates"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def install_skill(target: Path, force: bool = False) -> Path:
|
|
26
|
+
return _install_file(_normalize_target(target), SKILL_RELATIVE_PATH, SKILL_TEMPLATE_PATH, force=force)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def install_opencode(
|
|
30
|
+
target: Path,
|
|
31
|
+
python_executable: str | os.PathLike[str],
|
|
32
|
+
force: bool = False,
|
|
33
|
+
write_config: bool = False,
|
|
34
|
+
) -> tuple[Path, Path] | tuple[Path, Path, Path]:
|
|
35
|
+
target_root = _normalize_target(target)
|
|
36
|
+
skill_path = _safe_join(target_root, SKILL_RELATIVE_PATH)
|
|
37
|
+
plugin_path = _safe_join(target_root, OPENCODE_PLUGIN_RELATIVE_PATH)
|
|
38
|
+
_preflight_overwrite((skill_path, plugin_path), force=force)
|
|
39
|
+
rendered_skill = _load_template(SKILL_TEMPLATE_PATH)
|
|
40
|
+
rendered_plugin = _render_plugin(str(python_executable))
|
|
41
|
+
config_path: Path | None = None
|
|
42
|
+
config: dict[str, Any] | None = None
|
|
43
|
+
changed = False
|
|
44
|
+
if write_config:
|
|
45
|
+
config_path = _safe_join(target_root, OPENCODE_CONFIG_RELATIVE_PATH)
|
|
46
|
+
config = _load_json_object(config_path)
|
|
47
|
+
changed = _merge_opencode_agent_config(config, force=force)
|
|
48
|
+
|
|
49
|
+
_write_atomic_text(skill_path, rendered_skill, force=True)
|
|
50
|
+
_write_atomic_text(plugin_path, rendered_plugin, force=True)
|
|
51
|
+
if config_path is not None and config is not None and changed:
|
|
52
|
+
_write_atomic_json(config_path, config)
|
|
53
|
+
if not write_config:
|
|
54
|
+
return skill_path, plugin_path
|
|
55
|
+
assert config_path is not None
|
|
56
|
+
return skill_path, plugin_path, config_path
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _normalize_target(target: Path) -> Path:
|
|
60
|
+
normalized = target.expanduser().resolve()
|
|
61
|
+
if normalized.exists() and not normalized.is_dir():
|
|
62
|
+
raise NotADirectoryError(f"Target must be a directory: {normalized}")
|
|
63
|
+
normalized.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
return normalized
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _install_file(
|
|
68
|
+
target: Path,
|
|
69
|
+
destination: Path,
|
|
70
|
+
template_path: Path | None = None,
|
|
71
|
+
rendered_template: str | None = None,
|
|
72
|
+
force: bool = False,
|
|
73
|
+
) -> Path:
|
|
74
|
+
if template_path is None and rendered_template is None:
|
|
75
|
+
raise ValueError("Either template_path or rendered_template must be provided.")
|
|
76
|
+
if template_path is not None and rendered_template is not None:
|
|
77
|
+
raise ValueError("Provide only one source template.")
|
|
78
|
+
|
|
79
|
+
if template_path is not None:
|
|
80
|
+
rendered_template = _load_template(template_path)
|
|
81
|
+
assert rendered_template is not None
|
|
82
|
+
|
|
83
|
+
destination_path = _safe_join(target, destination)
|
|
84
|
+
_write_atomic_text(destination_path, rendered_template, force=force)
|
|
85
|
+
return destination_path
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _safe_join(target: Path, relative: Path) -> Path:
|
|
89
|
+
destination = (target / relative).resolve()
|
|
90
|
+
target_resolved = target.resolve()
|
|
91
|
+
destination.relative_to(target_resolved)
|
|
92
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
return destination
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _preflight_overwrite(paths: tuple[Path, ...], *, force: bool) -> None:
|
|
97
|
+
if force:
|
|
98
|
+
return
|
|
99
|
+
existing = [path for path in paths if path.exists()]
|
|
100
|
+
if existing:
|
|
101
|
+
raise FileExistsError(f"Refusing to overwrite existing file: {existing[0]}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _load_template(relative_path: Path) -> str:
|
|
105
|
+
template = _TEMPLATES_ROOT / relative_path
|
|
106
|
+
return template.read_text(encoding="utf-8")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _render_plugin(python_executable: str) -> str:
|
|
110
|
+
template = _load_template(OPENCODE_PLUGIN_TEMPLATE_PATH)
|
|
111
|
+
escaped_python_executable = json.dumps(str(python_executable))
|
|
112
|
+
return template.replace("{{PYTHON_EXECUTABLE}}", escaped_python_executable)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _write_atomic_text(path: Path, content: str, force: bool = False) -> None:
|
|
116
|
+
if path.exists() and not force:
|
|
117
|
+
raise FileExistsError(f"Refusing to overwrite existing file: {path}")
|
|
118
|
+
|
|
119
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
with NamedTemporaryFile("w", encoding="utf-8", newline="\n", dir=path.parent, delete=False) as handle:
|
|
121
|
+
handle.write(content)
|
|
122
|
+
tmp_path = Path(handle.name)
|
|
123
|
+
os.chmod(tmp_path, 0o644)
|
|
124
|
+
os.replace(tmp_path, path)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _load_json_object(path: Path) -> dict[str, Any]:
|
|
128
|
+
if not path.exists():
|
|
129
|
+
return {}
|
|
130
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
131
|
+
data: Any = json.load(handle)
|
|
132
|
+
if not isinstance(data, dict):
|
|
133
|
+
raise ValueError(f"Invalid opencode config object in {path}")
|
|
134
|
+
return data
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _merge_opencode_agent_config(config: dict[str, Any], *, force: bool) -> bool:
|
|
138
|
+
changed = False
|
|
139
|
+
if "$schema" not in config:
|
|
140
|
+
config["$schema"] = "https://opencode.ai/config.json"
|
|
141
|
+
changed = True
|
|
142
|
+
agents = config.get("agent")
|
|
143
|
+
if agents is None:
|
|
144
|
+
config["agent"] = {}
|
|
145
|
+
agents = config["agent"]
|
|
146
|
+
if not isinstance(agents, dict):
|
|
147
|
+
raise ValueError("Expected 'agent' to be an object.")
|
|
148
|
+
|
|
149
|
+
agent_config = agents.get(AGENT_NAME)
|
|
150
|
+
if agent_config is None:
|
|
151
|
+
agents[AGENT_NAME] = {
|
|
152
|
+
"description": "Navigate mixed-language codebases through the bounded semantic mapper.",
|
|
153
|
+
"permission": WORKTREE_PERMISSIONS.copy(),
|
|
154
|
+
}
|
|
155
|
+
return True
|
|
156
|
+
if not isinstance(agent_config, dict):
|
|
157
|
+
raise ValueError(f"Invalid agent block for {AGENT_NAME}")
|
|
158
|
+
|
|
159
|
+
permissions = agent_config.get("permission")
|
|
160
|
+
if permissions is None:
|
|
161
|
+
agent_config["permission"] = WORKTREE_PERMISSIONS.copy()
|
|
162
|
+
return True
|
|
163
|
+
if not isinstance(permissions, dict):
|
|
164
|
+
raise ValueError("permission must be an object")
|
|
165
|
+
|
|
166
|
+
for key, required_value in WORKTREE_PERMISSIONS.items():
|
|
167
|
+
current_value = permissions.get(key)
|
|
168
|
+
if current_value is not None and current_value != required_value:
|
|
169
|
+
if not force:
|
|
170
|
+
raise FileExistsError(
|
|
171
|
+
"Permission conflict in .opencode/opencode.json: "
|
|
172
|
+
f"key={key} expected={required_value!r} got={current_value!r}"
|
|
173
|
+
)
|
|
174
|
+
changed = True
|
|
175
|
+
|
|
176
|
+
normalized_permissions = WORKTREE_PERMISSIONS.copy()
|
|
177
|
+
if permissions != normalized_permissions:
|
|
178
|
+
permissions.clear()
|
|
179
|
+
permissions.update(normalized_permissions)
|
|
180
|
+
changed = True
|
|
181
|
+
return changed
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _write_atomic_json(path: Path, payload: dict[str, Any]) -> None:
|
|
185
|
+
serialized = json.dumps(payload, sort_keys=True, indent=2) + "\n"
|
|
186
|
+
_write_atomic_text(path, serialized, force=True)
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
6
|
+
import re
|
|
7
|
+
from urllib.parse import quote, unquote, urlparse
|
|
8
|
+
|
|
9
|
+
from ._version import __version__
|
|
10
|
+
from .lsp_service import LanguageService, SafeRenameError
|
|
11
|
+
from .models import Problem, SourceRange
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pygls.server import LanguageServer
|
|
15
|
+
from lsprotocol.types import (
|
|
16
|
+
CompletionItem,
|
|
17
|
+
CompletionItemKind,
|
|
18
|
+
CompletionList,
|
|
19
|
+
CompletionParams,
|
|
20
|
+
DefinitionParams,
|
|
21
|
+
Diagnostic,
|
|
22
|
+
DocumentSymbol,
|
|
23
|
+
DocumentSymbolParams,
|
|
24
|
+
Hover,
|
|
25
|
+
HoverParams,
|
|
26
|
+
InitializeParams,
|
|
27
|
+
InitializeResult,
|
|
28
|
+
Location,
|
|
29
|
+
Range,
|
|
30
|
+
ReferenceParams,
|
|
31
|
+
RenameParams,
|
|
32
|
+
SymbolInformation,
|
|
33
|
+
SymbolKind,
|
|
34
|
+
WorkspaceSymbolParams,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class _ServerState:
|
|
40
|
+
service: LanguageService | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _feature(constants: object, name: str, fallback: str) -> str:
|
|
44
|
+
return str(getattr(constants, name, fallback))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _uri_to_path(value: str) -> str:
|
|
48
|
+
parsed = urlparse(value)
|
|
49
|
+
if parsed.scheme == "file":
|
|
50
|
+
path = unquote(parsed.path)
|
|
51
|
+
if re.match(r"^/[A-Za-z]:/", path):
|
|
52
|
+
return path[1:]
|
|
53
|
+
return path
|
|
54
|
+
|
|
55
|
+
if parsed.scheme:
|
|
56
|
+
return unquote(value)
|
|
57
|
+
|
|
58
|
+
return unquote(value)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _to_file_uri(path: str | Path) -> str:
|
|
62
|
+
text = str(path)
|
|
63
|
+
if re.match(r"^[A-Za-z]:[\\/]", text):
|
|
64
|
+
drive = text[0].upper()
|
|
65
|
+
rest = quote(text[2:].replace("\\", "/"), safe="/")
|
|
66
|
+
return f"file:///{drive}:{rest}"
|
|
67
|
+
|
|
68
|
+
parsed = urlparse(text)
|
|
69
|
+
if parsed.scheme:
|
|
70
|
+
return text
|
|
71
|
+
|
|
72
|
+
candidate = Path(text).expanduser()
|
|
73
|
+
if not candidate.is_absolute():
|
|
74
|
+
candidate = Path.cwd() / candidate
|
|
75
|
+
return candidate.resolve().as_uri()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _source_range_to_lsp_range(source_range: SourceRange) -> "Range":
|
|
79
|
+
from lsprotocol.types import Position, Range
|
|
80
|
+
|
|
81
|
+
return Range(
|
|
82
|
+
start=Position(line=source_range.start_line, character=source_range.start_character),
|
|
83
|
+
end=Position(line=source_range.end_line, character=source_range.end_character),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _problem_to_diagnostic(problem: Problem) -> "Diagnostic":
|
|
88
|
+
from lsprotocol.types import Diagnostic, DiagnosticSeverity
|
|
89
|
+
|
|
90
|
+
source_range = problem.range or SourceRange("", 0, 0, 0, 1)
|
|
91
|
+
|
|
92
|
+
severity_map = {
|
|
93
|
+
"error": DiagnosticSeverity.Error,
|
|
94
|
+
"warning": DiagnosticSeverity.Warning,
|
|
95
|
+
"hint": DiagnosticSeverity.Hint,
|
|
96
|
+
"info": DiagnosticSeverity.Information,
|
|
97
|
+
"information": DiagnosticSeverity.Information,
|
|
98
|
+
}
|
|
99
|
+
severity = severity_map.get(problem.severity.casefold(), DiagnosticSeverity.Warning)
|
|
100
|
+
|
|
101
|
+
return Diagnostic(
|
|
102
|
+
range=_source_range_to_lsp_range(source_range),
|
|
103
|
+
message=problem.message,
|
|
104
|
+
severity=severity,
|
|
105
|
+
code=problem.code,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _symbol_kind(kind: str) -> "SymbolKind":
|
|
110
|
+
from lsprotocol.types import SymbolKind
|
|
111
|
+
|
|
112
|
+
mapping = {
|
|
113
|
+
"module": SymbolKind.Module,
|
|
114
|
+
"class": SymbolKind.Class,
|
|
115
|
+
"interface": SymbolKind.Interface,
|
|
116
|
+
"enum": SymbolKind.Enum,
|
|
117
|
+
"enum_value": SymbolKind.EnumMember,
|
|
118
|
+
"record": SymbolKind.Struct,
|
|
119
|
+
"method": SymbolKind.Method,
|
|
120
|
+
"function": SymbolKind.Function,
|
|
121
|
+
"procedure": SymbolKind.Function,
|
|
122
|
+
"constructor": SymbolKind.Constructor,
|
|
123
|
+
"destructor": SymbolKind.Method,
|
|
124
|
+
"variable": SymbolKind.Variable,
|
|
125
|
+
"parameter": SymbolKind.Variable,
|
|
126
|
+
"property": SymbolKind.Property,
|
|
127
|
+
"field": SymbolKind.Field,
|
|
128
|
+
"constant": SymbolKind.Constant,
|
|
129
|
+
}
|
|
130
|
+
return mapping.get(kind.lower(), SymbolKind.String)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _completion_kind(kind: str) -> "CompletionItemKind":
|
|
134
|
+
from lsprotocol.types import CompletionItemKind
|
|
135
|
+
|
|
136
|
+
mapping = {
|
|
137
|
+
"class": CompletionItemKind.Class,
|
|
138
|
+
"interface": CompletionItemKind.Interface,
|
|
139
|
+
"enum": CompletionItemKind.Enum,
|
|
140
|
+
"enum_value": CompletionItemKind.EnumMember,
|
|
141
|
+
"record": CompletionItemKind.Struct,
|
|
142
|
+
"method": CompletionItemKind.Method,
|
|
143
|
+
"function": CompletionItemKind.Function,
|
|
144
|
+
"procedure": CompletionItemKind.Function,
|
|
145
|
+
"constructor": CompletionItemKind.Constructor,
|
|
146
|
+
"destructor": CompletionItemKind.Method,
|
|
147
|
+
"variable": CompletionItemKind.Variable,
|
|
148
|
+
"parameter": CompletionItemKind.Variable,
|
|
149
|
+
"property": CompletionItemKind.Property,
|
|
150
|
+
"field": CompletionItemKind.Field,
|
|
151
|
+
"constant": CompletionItemKind.Constant,
|
|
152
|
+
}
|
|
153
|
+
return mapping.get(kind.lower(), CompletionItemKind.Text)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _state(server: "LanguageServer") -> _ServerState:
|
|
157
|
+
if not hasattr(server, "_acm_lsp_state"):
|
|
158
|
+
setattr(server, "_acm_lsp_state", _ServerState())
|
|
159
|
+
return cast(_ServerState, getattr(server, "_acm_lsp_state"))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _resolve_workspace_root(params: object) -> Path:
|
|
163
|
+
workspace_folders = getattr(params, "workspace_folders", None)
|
|
164
|
+
if workspace_folders:
|
|
165
|
+
first = workspace_folders[0]
|
|
166
|
+
candidate = getattr(first, "uri", None)
|
|
167
|
+
if isinstance(candidate, str) and candidate:
|
|
168
|
+
return Path(_uri_to_path(candidate)).expanduser().resolve()
|
|
169
|
+
|
|
170
|
+
root_uri = getattr(params, "root_uri", None)
|
|
171
|
+
if isinstance(root_uri, str) and root_uri:
|
|
172
|
+
return Path(_uri_to_path(root_uri)).expanduser().resolve()
|
|
173
|
+
|
|
174
|
+
root_path = getattr(params, "root_path", None)
|
|
175
|
+
if isinstance(root_path, str) and root_path:
|
|
176
|
+
return Path(root_path).expanduser().resolve()
|
|
177
|
+
|
|
178
|
+
return Path.cwd()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _workspace_service(server: "LanguageServer") -> LanguageService:
|
|
182
|
+
state = _state(server)
|
|
183
|
+
if state.service is None:
|
|
184
|
+
state.service = LanguageService(Path.cwd())
|
|
185
|
+
return state.service
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _publish_diagnostics_for_uri(server: "LanguageServer", service: LanguageService, uri: str) -> None:
|
|
189
|
+
path = _uri_to_path(uri)
|
|
190
|
+
diagnostics = [_problem_to_diagnostic(problem) for problem in service.diagnostics(path)]
|
|
191
|
+
server.publish_diagnostics(uri, diagnostics)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def create_server() -> "LanguageServer":
|
|
195
|
+
try:
|
|
196
|
+
from lsprotocol import types as lsp_types
|
|
197
|
+
from pygls.server import LanguageServer
|
|
198
|
+
except ImportError as exc: # pragma: no cover - optional dependency
|
|
199
|
+
raise RuntimeError("pygls and lsprotocol are required for the LSP server") from exc
|
|
200
|
+
|
|
201
|
+
server = LanguageServer("agent-codinglanguage-mapper", __version__)
|
|
202
|
+
state = _state(server)
|
|
203
|
+
# ensure deterministic test access to internal state
|
|
204
|
+
state.service = None
|
|
205
|
+
|
|
206
|
+
initialize = _feature(lsp_types, "INITIALIZE", "initialize")
|
|
207
|
+
did_open = _feature(lsp_types, "TEXT_DOCUMENT_DID_OPEN", "textDocument/didOpen")
|
|
208
|
+
did_change = _feature(lsp_types, "TEXT_DOCUMENT_DID_CHANGE", "textDocument/didChange")
|
|
209
|
+
did_close = _feature(lsp_types, "TEXT_DOCUMENT_DID_CLOSE", "textDocument/didClose")
|
|
210
|
+
definition = _feature(lsp_types, "TEXT_DOCUMENT_DEFINITION", "textDocument/definition")
|
|
211
|
+
hover = _feature(lsp_types, "TEXT_DOCUMENT_HOVER", "textDocument/hover")
|
|
212
|
+
references = _feature(lsp_types, "TEXT_DOCUMENT_REFERENCES", "textDocument/references")
|
|
213
|
+
rename = _feature(lsp_types, "TEXT_DOCUMENT_RENAME", "textDocument/rename")
|
|
214
|
+
document_symbol = _feature(lsp_types, "TEXT_DOCUMENT_DOCUMENT_SYMBOL", "textDocument/documentSymbol")
|
|
215
|
+
workspace_symbol = _feature(lsp_types, "WORKSPACE_SYMBOL", "workspace/symbol")
|
|
216
|
+
completion = _feature(lsp_types, "TEXT_DOCUMENT_COMPLETION", "textDocument/completion")
|
|
217
|
+
|
|
218
|
+
@server.feature(initialize)
|
|
219
|
+
def on_initialize(ls: "LanguageServer", params: "InitializeParams") -> "InitializeResult":
|
|
220
|
+
root = _resolve_workspace_root(params)
|
|
221
|
+
_state(ls).service = LanguageService(root)
|
|
222
|
+
return lsp_types.InitializeResult(
|
|
223
|
+
capabilities=lsp_types.ServerCapabilities(
|
|
224
|
+
text_document_sync=lsp_types.TextDocumentSyncOptions(
|
|
225
|
+
open_close=True,
|
|
226
|
+
change=lsp_types.TextDocumentSyncKind.Full,
|
|
227
|
+
),
|
|
228
|
+
definition_provider=True,
|
|
229
|
+
hover_provider=True,
|
|
230
|
+
references_provider=True,
|
|
231
|
+
rename_provider=True,
|
|
232
|
+
document_symbol_provider=True,
|
|
233
|
+
workspace_symbol_provider=True,
|
|
234
|
+
completion_provider=lsp_types.CompletionOptions(),
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
@server.feature(did_open)
|
|
239
|
+
def on_did_open(ls: "LanguageServer", params: Any) -> None:
|
|
240
|
+
service = _workspace_service(ls)
|
|
241
|
+
uri = getattr(params.text_document, "uri")
|
|
242
|
+
text = getattr(params.text_document, "text")
|
|
243
|
+
service.update_document(_uri_to_path(uri), text)
|
|
244
|
+
_publish_diagnostics_for_uri(ls, service, uri)
|
|
245
|
+
|
|
246
|
+
@server.feature(did_change)
|
|
247
|
+
def on_did_change(ls: "LanguageServer", params: Any) -> None:
|
|
248
|
+
changes = getattr(params, "content_changes", ())
|
|
249
|
+
if not changes:
|
|
250
|
+
return
|
|
251
|
+
service = _workspace_service(ls)
|
|
252
|
+
text = changes[-1].text
|
|
253
|
+
uri = params.text_document.uri
|
|
254
|
+
service.update_document(_uri_to_path(uri), text)
|
|
255
|
+
_publish_diagnostics_for_uri(ls, service, uri)
|
|
256
|
+
|
|
257
|
+
@server.feature(did_close)
|
|
258
|
+
def on_did_close(ls: "LanguageServer", params: Any) -> None:
|
|
259
|
+
service = _workspace_service(ls)
|
|
260
|
+
uri = params.text_document.uri
|
|
261
|
+
service.close_document(_uri_to_path(uri))
|
|
262
|
+
ls.publish_diagnostics(uri, [])
|
|
263
|
+
|
|
264
|
+
@server.feature(definition)
|
|
265
|
+
def on_definition(ls: "LanguageServer", params: "DefinitionParams") -> "Location | None":
|
|
266
|
+
service = _workspace_service(ls)
|
|
267
|
+
path = _uri_to_path(params.text_document.uri)
|
|
268
|
+
symbol_range = service.definition(path, params.position.line, params.position.character)
|
|
269
|
+
if symbol_range is None:
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
return lsp_types.Location(
|
|
273
|
+
uri=service.path_uri(symbol_range.path),
|
|
274
|
+
range=_source_range_to_lsp_range(symbol_range),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
@server.feature(hover)
|
|
278
|
+
def on_hover(ls: "LanguageServer", params: "HoverParams") -> "Hover | None":
|
|
279
|
+
service = _workspace_service(ls)
|
|
280
|
+
path = _uri_to_path(params.text_document.uri)
|
|
281
|
+
contents = service.hover(path, params.position.line, params.position.character)
|
|
282
|
+
if contents is None:
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
return lsp_types.Hover(
|
|
286
|
+
contents=lsp_types.MarkupContent(kind=lsp_types.MarkupKind.Markdown, value=contents)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
@server.feature(references)
|
|
290
|
+
def on_references(ls: "LanguageServer", params: "ReferenceParams") -> list["Location"]:
|
|
291
|
+
service = _workspace_service(ls)
|
|
292
|
+
path = _uri_to_path(params.text_document.uri)
|
|
293
|
+
|
|
294
|
+
context = getattr(params, "context", None)
|
|
295
|
+
include_declaration = True
|
|
296
|
+
if context is not None and getattr(context, "include_declaration", None) is not None:
|
|
297
|
+
include_declaration = bool(context.include_declaration)
|
|
298
|
+
|
|
299
|
+
return [
|
|
300
|
+
lsp_types.Location(
|
|
301
|
+
uri=service.path_uri(item.path),
|
|
302
|
+
range=_source_range_to_lsp_range(item),
|
|
303
|
+
)
|
|
304
|
+
for item in service.references(path, params.position.line, params.position.character, include_declaration=include_declaration)
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
@server.feature(rename)
|
|
308
|
+
def on_rename(ls: "LanguageServer", params: "RenameParams") -> Any:
|
|
309
|
+
service = _workspace_service(ls)
|
|
310
|
+
path = _uri_to_path(params.text_document.uri)
|
|
311
|
+
try:
|
|
312
|
+
edits = service.rename(path, params.position.line, params.position.character, params.new_name)
|
|
313
|
+
except SafeRenameError:
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
if not edits:
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
return lsp_types.WorkspaceEdit(
|
|
320
|
+
changes={
|
|
321
|
+
service.path_uri(file_name): [
|
|
322
|
+
lsp_types.TextEdit(
|
|
323
|
+
range=_source_range_to_lsp_range(edit.range),
|
|
324
|
+
new_text=edit.new_text,
|
|
325
|
+
)
|
|
326
|
+
for edit in file_edits
|
|
327
|
+
]
|
|
328
|
+
for file_name, file_edits in edits.items()
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
@server.feature(document_symbol)
|
|
333
|
+
def on_document_symbols(
|
|
334
|
+
ls: "LanguageServer",
|
|
335
|
+
params: "DocumentSymbolParams",
|
|
336
|
+
) -> list["DocumentSymbol"]:
|
|
337
|
+
service = _workspace_service(ls)
|
|
338
|
+
path = _uri_to_path(params.text_document.uri)
|
|
339
|
+
symbols = service.document_symbols(path)
|
|
340
|
+
|
|
341
|
+
return [
|
|
342
|
+
lsp_types.DocumentSymbol(
|
|
343
|
+
name=item.name,
|
|
344
|
+
detail=item.signature or None,
|
|
345
|
+
kind=_symbol_kind(item.kind),
|
|
346
|
+
range=_source_range_to_lsp_range(item.range),
|
|
347
|
+
selection_range=_source_range_to_lsp_range(item.name_range or item.range),
|
|
348
|
+
)
|
|
349
|
+
for item in symbols
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
@server.feature(workspace_symbol)
|
|
353
|
+
def on_workspace_symbols(
|
|
354
|
+
ls: "LanguageServer",
|
|
355
|
+
params: "WorkspaceSymbolParams",
|
|
356
|
+
) -> list["SymbolInformation"]:
|
|
357
|
+
service = _workspace_service(ls)
|
|
358
|
+
symbols = service.workspace_symbols(getattr(params, "query", "") or "")
|
|
359
|
+
return [
|
|
360
|
+
lsp_types.SymbolInformation(
|
|
361
|
+
name=item.name,
|
|
362
|
+
kind=_symbol_kind(item.kind),
|
|
363
|
+
location=lsp_types.Location(
|
|
364
|
+
uri=service.path_uri(item.range.path),
|
|
365
|
+
range=_source_range_to_lsp_range(item.range),
|
|
366
|
+
),
|
|
367
|
+
container_name=item.qualified_name.rpartition(".")[0] or None,
|
|
368
|
+
)
|
|
369
|
+
for item in symbols
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
@server.feature(completion)
|
|
373
|
+
def on_completion(
|
|
374
|
+
ls: "LanguageServer",
|
|
375
|
+
params: "CompletionParams",
|
|
376
|
+
) -> "CompletionList | list[CompletionItem]":
|
|
377
|
+
service = _workspace_service(ls)
|
|
378
|
+
path = _uri_to_path(params.text_document.uri)
|
|
379
|
+
matches = service.completions(path)
|
|
380
|
+
items = [
|
|
381
|
+
lsp_types.CompletionItem(
|
|
382
|
+
label=item.name,
|
|
383
|
+
kind=_completion_kind(item.kind),
|
|
384
|
+
detail=item.signature,
|
|
385
|
+
documentation=item.qualified_name,
|
|
386
|
+
)
|
|
387
|
+
for item in matches
|
|
388
|
+
]
|
|
389
|
+
return lsp_types.CompletionList(is_incomplete=False, items=items)
|
|
390
|
+
|
|
391
|
+
# Expose handlers for focused non-stdio unit tests.
|
|
392
|
+
setattr(server, "_handle_initialize", on_initialize)
|
|
393
|
+
setattr(server, "_handle_did_open", on_did_open)
|
|
394
|
+
setattr(server, "_handle_did_change", on_did_change)
|
|
395
|
+
setattr(server, "_handle_did_close", on_did_close)
|
|
396
|
+
setattr(server, "_handle_definition", on_definition)
|
|
397
|
+
setattr(server, "_handle_hover", on_hover)
|
|
398
|
+
setattr(server, "_handle_references", on_references)
|
|
399
|
+
setattr(server, "_handle_rename", on_rename)
|
|
400
|
+
setattr(server, "_handle_document_symbols", on_document_symbols)
|
|
401
|
+
setattr(server, "_handle_workspace_symbols", on_workspace_symbols)
|
|
402
|
+
setattr(server, "_handle_completion", on_completion)
|
|
403
|
+
setattr(server, "_acm_state", state)
|
|
404
|
+
setattr(server, "_acm_lsp_state", state)
|
|
405
|
+
|
|
406
|
+
return server
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def main() -> None:
|
|
410
|
+
create_server().start_io()
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
__all__ = ["create_server", "main", "_to_file_uri", "_uri_to_path", "_source_range_to_lsp_range"]
|