lean-lsp-mcp 0.15.0__tar.gz → 0.16.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.
- {lean_lsp_mcp-0.15.0/src/lean_lsp_mcp.egg-info → lean_lsp_mcp-0.16.0}/PKG-INFO +2 -1
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/pyproject.toml +2 -2
- lean_lsp_mcp-0.16.0/src/lean_lsp_mcp/instructions.py +36 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/src/lean_lsp_mcp/loogle.py +61 -8
- lean_lsp_mcp-0.16.0/src/lean_lsp_mcp/models.py +120 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/src/lean_lsp_mcp/outline_utils.py +93 -0
- lean_lsp_mcp-0.16.0/src/lean_lsp_mcp/server.py +1175 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/src/lean_lsp_mcp/utils.py +31 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0/src/lean_lsp_mcp.egg-info}/PKG-INFO +2 -1
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/src/lean_lsp_mcp.egg-info/SOURCES.txt +1 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/src/lean_lsp_mcp.egg-info/requires.txt +1 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/tests/test_diagnostic_line_range.py +18 -20
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/tests/test_outline.py +43 -23
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/tests/test_search_tools.py +19 -4
- lean_lsp_mcp-0.15.0/src/lean_lsp_mcp/instructions.py +0 -17
- lean_lsp_mcp-0.15.0/src/lean_lsp_mcp/server.py +0 -1054
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/LICENSE +0 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/README.md +0 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/setup.cfg +0 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/src/lean_lsp_mcp/__init__.py +0 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/src/lean_lsp_mcp/__main__.py +0 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/src/lean_lsp_mcp/client_utils.py +0 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/src/lean_lsp_mcp/file_utils.py +0 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/src/lean_lsp_mcp/search_utils.py +0 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/src/lean_lsp_mcp.egg-info/dependency_links.txt +0 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/src/lean_lsp_mcp.egg-info/entry_points.txt +0 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/src/lean_lsp_mcp.egg-info/top_level.txt +0 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/tests/test_editor_tools.py +0 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/tests/test_file_caching.py +0 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/tests/test_logging.py +0 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/tests/test_misc_tools.py +0 -0
- {lean_lsp_mcp-0.15.0 → lean_lsp_mcp-0.16.0}/tests/test_project_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lean-lsp-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.0
|
|
4
4
|
Summary: Lean Theorem Prover MCP
|
|
5
5
|
Author-email: Oliver Dressler <hey@oli.show>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -10,6 +10,7 @@ Description-Content-Type: text/markdown
|
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Dist: leanclient==0.6.1
|
|
12
12
|
Requires-Dist: mcp[cli]==1.23.1
|
|
13
|
+
Requires-Dist: mcp[cli]>=1.22.0
|
|
13
14
|
Requires-Dist: orjson>=3.11.1
|
|
14
15
|
Provides-Extra: lint
|
|
15
16
|
Requires-Dist: ruff>=0.2.0; extra == "lint"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lean-lsp-mcp"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.16.0"
|
|
4
4
|
description = "Lean Theorem Prover MCP"
|
|
5
5
|
authors = [{name="Oliver Dressler", email="hey@oli.show"}]
|
|
6
6
|
readme = "README.md"
|
|
@@ -8,7 +8,7 @@ requires-python = ">=3.10"
|
|
|
8
8
|
license = "MIT"
|
|
9
9
|
dependencies = [
|
|
10
10
|
"leanclient==0.6.1",
|
|
11
|
-
"mcp[cli]==1.23.1",
|
|
11
|
+
"mcp[cli]==1.23.1", "mcp[cli]>=1.22.0",
|
|
12
12
|
"orjson>=3.11.1",
|
|
13
13
|
]
|
|
14
14
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
INSTRUCTIONS = """## General Rules
|
|
2
|
+
- All line and column numbers are 1-indexed.
|
|
3
|
+
- This MCP does NOT edit files. Use other tools for editing.
|
|
4
|
+
|
|
5
|
+
## Key Tools
|
|
6
|
+
- **lean_goal**: Proof state at position. Omit `column` for before/after. "no goals" = done!
|
|
7
|
+
- **lean_diagnostic_messages**: Compiler errors/warnings. "no goals to be solved" = remove tactics.
|
|
8
|
+
- **lean_hover_info**: Type signature + docs. Column at START of identifier.
|
|
9
|
+
- **lean_completions**: IDE autocomplete on incomplete code.
|
|
10
|
+
- **lean_local_search**: Fast local declaration search. Use BEFORE trying a lemma name.
|
|
11
|
+
- **lean_file_outline**: Token-efficient file skeleton (slow-ish).
|
|
12
|
+
- **lean_multi_attempt**: Test tactics without editing: `["simp", "ring", "omega"]`
|
|
13
|
+
- **lean_declaration_file**: Get declaration source. Use sparingly (large output).
|
|
14
|
+
- **lean_run_code**: Run standalone snippet. Use rarely.
|
|
15
|
+
- **lean_build**: Rebuild + restart LSP. Only if needed (new imports). SLOW!
|
|
16
|
+
|
|
17
|
+
## Search Tools (rate limited)
|
|
18
|
+
- **lean_leansearch** (3/30s): Natural language → mathlib
|
|
19
|
+
- **lean_loogle** (3/30s): Type pattern → mathlib
|
|
20
|
+
- **lean_leanfinder** (10/30s): Semantic/conceptual search
|
|
21
|
+
- **lean_state_search** (3/30s): Goal → closing lemmas
|
|
22
|
+
- **lean_hammer_premise** (3/30s): Goal → premises for simp/aesop
|
|
23
|
+
|
|
24
|
+
## Search Decision Tree
|
|
25
|
+
1. "Does X exist locally?" → lean_local_search
|
|
26
|
+
2. "I need a lemma that says X" → lean_leansearch
|
|
27
|
+
3. "Find lemma with type pattern" → lean_loogle
|
|
28
|
+
4. "What's the Lean name for concept X?" → lean_leanfinder
|
|
29
|
+
5. "What closes this goal?" → lean_state_search
|
|
30
|
+
6. "What to feed simp?" → lean_hammer_premise
|
|
31
|
+
|
|
32
|
+
After finding a name: lean_local_search to verify, lean_hover_info for signature.
|
|
33
|
+
|
|
34
|
+
## Return Formats
|
|
35
|
+
List tools return JSON arrays. Empty = `[]`.
|
|
36
|
+
"""
|
|
@@ -15,6 +15,8 @@ from typing import Any
|
|
|
15
15
|
|
|
16
16
|
import orjson
|
|
17
17
|
|
|
18
|
+
from lean_lsp_mcp.models import LoogleResult
|
|
19
|
+
|
|
18
20
|
logger = logging.getLogger(__name__)
|
|
19
21
|
|
|
20
22
|
|
|
@@ -25,7 +27,7 @@ def get_cache_dir() -> Path:
|
|
|
25
27
|
return Path(xdg) / "lean-lsp-mcp" / "loogle"
|
|
26
28
|
|
|
27
29
|
|
|
28
|
-
def loogle_remote(query: str, num_results: int) -> list[
|
|
30
|
+
def loogle_remote(query: str, num_results: int) -> list[LoogleResult] | str:
|
|
29
31
|
"""Query the remote loogle API."""
|
|
30
32
|
try:
|
|
31
33
|
req = urllib.request.Request(
|
|
@@ -36,10 +38,15 @@ def loogle_remote(query: str, num_results: int) -> list[dict] | str:
|
|
|
36
38
|
results = orjson.loads(response.read())
|
|
37
39
|
if "hits" not in results:
|
|
38
40
|
return "No results found."
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
hits = results["hits"][:num_results]
|
|
42
|
+
return [
|
|
43
|
+
LoogleResult(
|
|
44
|
+
name=r.get("name", ""),
|
|
45
|
+
type=r.get("type", ""),
|
|
46
|
+
module=r.get("module", ""),
|
|
47
|
+
)
|
|
48
|
+
for r in hits
|
|
49
|
+
]
|
|
43
50
|
except Exception as e:
|
|
44
51
|
return f"loogle error:\n{e}"
|
|
45
52
|
|
|
@@ -141,6 +148,39 @@ class LoogleManager:
|
|
|
141
148
|
pass
|
|
142
149
|
return "unknown"
|
|
143
150
|
|
|
151
|
+
def _get_toolchain_version(self) -> str | None:
|
|
152
|
+
"""Get the Lean toolchain version from lean-toolchain file."""
|
|
153
|
+
try:
|
|
154
|
+
return (self.repo_dir / "lean-toolchain").read_text().strip()
|
|
155
|
+
except Exception:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def _check_toolchain_installed(self) -> tuple[bool, str]:
|
|
159
|
+
"""Check if the required Lean toolchain is installed."""
|
|
160
|
+
tc = self._get_toolchain_version()
|
|
161
|
+
if not tc:
|
|
162
|
+
return True, "" # Can't check without lean-toolchain file
|
|
163
|
+
# Convert lean-toolchain format to elan directory name
|
|
164
|
+
# e.g., "leanprover/lean4:v4.25.0-rc1" -> "leanprover--lean4---v4.25.0-rc1"
|
|
165
|
+
tc_dir_name = tc.replace("/", "--").replace(":", "---")
|
|
166
|
+
elan_home = Path(os.environ.get("ELAN_HOME", Path.home() / ".elan"))
|
|
167
|
+
tc_path = elan_home / "toolchains" / tc_dir_name
|
|
168
|
+
if not tc_path.exists():
|
|
169
|
+
return False, (
|
|
170
|
+
f"Toolchain '{tc}' not installed. "
|
|
171
|
+
f"Run: cd {self.repo_dir} && lake update"
|
|
172
|
+
)
|
|
173
|
+
return True, ""
|
|
174
|
+
|
|
175
|
+
def check_environment(self) -> tuple[bool, str]:
|
|
176
|
+
"""Check if the loogle environment is valid. Returns (ok, error_msg)."""
|
|
177
|
+
if not self.is_installed:
|
|
178
|
+
return False, "Loogle binary not found"
|
|
179
|
+
ok, err = self._check_toolchain_installed()
|
|
180
|
+
if not ok:
|
|
181
|
+
return False, err
|
|
182
|
+
return True, ""
|
|
183
|
+
|
|
144
184
|
def _get_index_path(self) -> Path:
|
|
145
185
|
return self.index_dir / f"mathlib-{self._get_mathlib_version()}.idx"
|
|
146
186
|
|
|
@@ -190,7 +230,9 @@ class LoogleManager:
|
|
|
190
230
|
async def start(self) -> bool:
|
|
191
231
|
if self.process is not None and self.process.returncode is None:
|
|
192
232
|
return self._ready
|
|
193
|
-
|
|
233
|
+
ok, err = self.check_environment()
|
|
234
|
+
if not ok:
|
|
235
|
+
logger.error(f"Loogle environment check failed: {err}")
|
|
194
236
|
return False
|
|
195
237
|
cmd = [str(self.binary_path), "--json", "--interactive"]
|
|
196
238
|
if (idx := self._get_index_path()).exists():
|
|
@@ -201,14 +243,25 @@ class LoogleManager:
|
|
|
201
243
|
*cmd,
|
|
202
244
|
stdin=asyncio.subprocess.PIPE,
|
|
203
245
|
stdout=asyncio.subprocess.PIPE,
|
|
204
|
-
stderr=asyncio.subprocess.
|
|
246
|
+
stderr=asyncio.subprocess.PIPE,
|
|
205
247
|
cwd=self.repo_dir,
|
|
206
248
|
)
|
|
207
249
|
line = await asyncio.wait_for(self.process.stdout.readline(), timeout=120)
|
|
208
|
-
|
|
250
|
+
decoded = line.decode()
|
|
251
|
+
if self.READY_SIGNAL in decoded:
|
|
209
252
|
self._ready = True
|
|
210
253
|
logger.info("Loogle ready")
|
|
211
254
|
return True
|
|
255
|
+
# Check stderr for error messages
|
|
256
|
+
try:
|
|
257
|
+
stderr_data = await asyncio.wait_for(
|
|
258
|
+
self.process.stderr.read(), timeout=1
|
|
259
|
+
)
|
|
260
|
+
if stderr_data:
|
|
261
|
+
logger.error(f"Loogle stderr: {stderr_data.decode().strip()}")
|
|
262
|
+
except asyncio.TimeoutError:
|
|
263
|
+
pass
|
|
264
|
+
logger.error(f"Loogle failed to start. stdout: {decoded.strip()}")
|
|
212
265
|
return False
|
|
213
266
|
except asyncio.TimeoutError:
|
|
214
267
|
logger.error("Loogle startup timeout")
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Pydantic models for MCP tool structured outputs."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LocalSearchResult(BaseModel):
|
|
8
|
+
name: str = Field(description="Declaration name")
|
|
9
|
+
kind: str = Field(description="Declaration kind (theorem, def, class, etc.)")
|
|
10
|
+
file: str = Field(description="Relative file path")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LeanSearchResult(BaseModel):
|
|
14
|
+
name: str = Field(description="Full qualified name")
|
|
15
|
+
module_name: str = Field(description="Module where declared")
|
|
16
|
+
kind: Optional[str] = Field(None, description="Declaration kind")
|
|
17
|
+
type: Optional[str] = Field(None, description="Type signature")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LoogleResult(BaseModel):
|
|
21
|
+
name: str = Field(description="Declaration name")
|
|
22
|
+
type: str = Field(description="Type signature")
|
|
23
|
+
module: str = Field(description="Module where declared")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LeanFinderResult(BaseModel):
|
|
27
|
+
full_name: str = Field(description="Full qualified name")
|
|
28
|
+
formal_statement: str = Field(description="Lean type signature")
|
|
29
|
+
informal_statement: str = Field(description="Natural language description")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StateSearchResult(BaseModel):
|
|
33
|
+
name: str = Field(description="Theorem/lemma name")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PremiseResult(BaseModel):
|
|
37
|
+
name: str = Field(description="Premise name for simp/omega/aesop")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DiagnosticMessage(BaseModel):
|
|
41
|
+
severity: str = Field(description="error, warning, info, or hint")
|
|
42
|
+
message: str = Field(description="Diagnostic message text")
|
|
43
|
+
line: int = Field(description="Line (1-indexed)")
|
|
44
|
+
column: int = Field(description="Column (1-indexed)")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GoalState(BaseModel):
|
|
48
|
+
line_context: str = Field(description="Source line where goals were queried")
|
|
49
|
+
goals: str = Field(description="Goal state (before→after if column omitted)")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CompletionItem(BaseModel):
|
|
53
|
+
label: str = Field(description="Completion text to insert")
|
|
54
|
+
kind: Optional[str] = Field(
|
|
55
|
+
None, description="Completion kind (function, variable, etc.)"
|
|
56
|
+
)
|
|
57
|
+
detail: Optional[str] = Field(None, description="Additional detail")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class HoverInfo(BaseModel):
|
|
61
|
+
symbol: str = Field(description="The symbol being hovered")
|
|
62
|
+
info: str = Field(description="Type signature and documentation")
|
|
63
|
+
diagnostics: List[DiagnosticMessage] = Field(
|
|
64
|
+
default_factory=list, description="Diagnostics at this position"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TermGoalState(BaseModel):
|
|
69
|
+
line_context: str = Field(description="Source line where term goal was queried")
|
|
70
|
+
expected_type: Optional[str] = Field(
|
|
71
|
+
None, description="Expected type at this position"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class OutlineEntry(BaseModel):
|
|
76
|
+
name: str = Field(description="Declaration name")
|
|
77
|
+
kind: str = Field(description="Declaration kind (Thm, Def, Class, Struct, Ns, Ex)")
|
|
78
|
+
start_line: int = Field(description="Start line (1-indexed)")
|
|
79
|
+
end_line: int = Field(description="End line (1-indexed)")
|
|
80
|
+
type_signature: Optional[str] = Field(
|
|
81
|
+
None, description="Type signature if available"
|
|
82
|
+
)
|
|
83
|
+
children: List["OutlineEntry"] = Field(
|
|
84
|
+
default_factory=list, description="Nested declarations"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class FileOutline(BaseModel):
|
|
89
|
+
imports: List[str] = Field(default_factory=list, description="Import statements")
|
|
90
|
+
declarations: List[OutlineEntry] = Field(
|
|
91
|
+
default_factory=list, description="Top-level declarations"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AttemptResult(BaseModel):
|
|
96
|
+
snippet: str = Field(description="Code snippet that was tried")
|
|
97
|
+
goal_state: Optional[str] = Field(
|
|
98
|
+
None, description="Goal state after applying snippet"
|
|
99
|
+
)
|
|
100
|
+
diagnostics: List[DiagnosticMessage] = Field(
|
|
101
|
+
default_factory=list, description="Diagnostics for this attempt"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class BuildResult(BaseModel):
|
|
106
|
+
success: bool = Field(description="Whether build succeeded")
|
|
107
|
+
output: str = Field(description="Build output")
|
|
108
|
+
errors: List[str] = Field(default_factory=list, description="Build errors if any")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class RunResult(BaseModel):
|
|
112
|
+
success: bool = Field(description="Whether code compiled successfully")
|
|
113
|
+
diagnostics: List[DiagnosticMessage] = Field(
|
|
114
|
+
default_factory=list, description="Compiler diagnostics"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class DeclarationInfo(BaseModel):
|
|
119
|
+
file_path: str = Field(description="Path to declaration file")
|
|
120
|
+
content: str = Field(description="File content")
|
|
@@ -3,6 +3,8 @@ from typing import Dict, List, Optional, Tuple
|
|
|
3
3
|
from leanclient import LeanLSPClient
|
|
4
4
|
from leanclient.utils import DocumentContentChange
|
|
5
5
|
|
|
6
|
+
from lean_lsp_mcp.models import FileOutline, OutlineEntry
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
METHOD_KIND = {6, "method"}
|
|
8
10
|
KIND_TAGS = {"namespace": "Ns"}
|
|
@@ -181,6 +183,97 @@ def _format_symbol(sym: Dict, type_sigs: Dict, fields_map: Dict, indent: int) ->
|
|
|
181
183
|
return result + "\n"
|
|
182
184
|
|
|
183
185
|
|
|
186
|
+
def _build_outline_entry(
|
|
187
|
+
sym: Dict, type_sigs: Dict, fields_map: Dict, indent: int
|
|
188
|
+
) -> Optional[OutlineEntry]:
|
|
189
|
+
"""Build a structured outline entry for a symbol."""
|
|
190
|
+
name = sym["name"]
|
|
191
|
+
type_sig = sym.get("_type") or type_sigs.get(name, "")
|
|
192
|
+
fields = fields_map.get(name, [])
|
|
193
|
+
|
|
194
|
+
tag = _detect_tag(
|
|
195
|
+
name, sym.get("kind", ""), type_sig, bool(fields), sym.get("_keyword")
|
|
196
|
+
)
|
|
197
|
+
start = sym["range"]["start"]["line"] + 1
|
|
198
|
+
end = sym["range"]["end"]["line"] + 1
|
|
199
|
+
|
|
200
|
+
# Add fields as children for structs/classes
|
|
201
|
+
children = [
|
|
202
|
+
OutlineEntry(
|
|
203
|
+
name=fname,
|
|
204
|
+
kind="field",
|
|
205
|
+
start_line=start,
|
|
206
|
+
end_line=start,
|
|
207
|
+
type_signature=ftype,
|
|
208
|
+
children=[],
|
|
209
|
+
)
|
|
210
|
+
for fname, ftype in fields
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
return OutlineEntry(
|
|
214
|
+
name=name,
|
|
215
|
+
kind=tag,
|
|
216
|
+
start_line=start,
|
|
217
|
+
end_line=end,
|
|
218
|
+
type_signature=type_sig if type_sig else None,
|
|
219
|
+
children=children,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def generate_outline_data(client: LeanLSPClient, path: str) -> FileOutline:
|
|
224
|
+
"""Generate structured outline data for a Lean file."""
|
|
225
|
+
client.open_file(path)
|
|
226
|
+
content = client.get_file_content(path)
|
|
227
|
+
|
|
228
|
+
# Extract imports
|
|
229
|
+
imports = [
|
|
230
|
+
line.strip()[7:]
|
|
231
|
+
for line in content.splitlines()
|
|
232
|
+
if line.strip().startswith("import ")
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
symbols = client.get_document_symbols(path)
|
|
236
|
+
if not symbols and not imports:
|
|
237
|
+
return FileOutline(imports=[], declarations=[])
|
|
238
|
+
|
|
239
|
+
# Flatten symbol tree and extract namespace declarations
|
|
240
|
+
all_symbols = _flatten_symbols(symbols, content=content)
|
|
241
|
+
|
|
242
|
+
# Get info trees only for LSP symbols (not extracted declarations)
|
|
243
|
+
lsp_methods = [
|
|
244
|
+
s
|
|
245
|
+
for s, _ in all_symbols
|
|
246
|
+
if s.get("kind") in METHOD_KIND and "_keyword" not in s
|
|
247
|
+
]
|
|
248
|
+
info_trees = _get_info_trees(client, path, lsp_methods)
|
|
249
|
+
|
|
250
|
+
# Extract type signatures and fields from info trees
|
|
251
|
+
type_sigs = {
|
|
252
|
+
name: sig
|
|
253
|
+
for name, info in info_trees.items()
|
|
254
|
+
if (sig := _extract_type(info, name))
|
|
255
|
+
}
|
|
256
|
+
fields_map = {
|
|
257
|
+
name: fields
|
|
258
|
+
for name, info in info_trees.items()
|
|
259
|
+
if (fields := _extract_fields(info, name))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# Build declarations list
|
|
263
|
+
declarations = []
|
|
264
|
+
for sym, indent in all_symbols:
|
|
265
|
+
if (
|
|
266
|
+
sym.get("kind") in METHOD_KIND
|
|
267
|
+
or sym.get("_keyword")
|
|
268
|
+
or sym.get("kind") == "namespace"
|
|
269
|
+
):
|
|
270
|
+
entry = _build_outline_entry(sym, type_sigs, fields_map, indent)
|
|
271
|
+
if entry:
|
|
272
|
+
declarations.append(entry)
|
|
273
|
+
|
|
274
|
+
return FileOutline(imports=imports, declarations=declarations)
|
|
275
|
+
|
|
276
|
+
|
|
184
277
|
def generate_outline(client: LeanLSPClient, path: str) -> str:
|
|
185
278
|
"""Generate a concise outline of a Lean file showing structure and signatures."""
|
|
186
279
|
client.open_file(path)
|