lispython 0.3.3__tar.gz → 0.4.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.
Files changed (47) hide show
  1. lispython-0.4.0/.vscode/settings.json +3 -0
  2. {lispython-0.3.3 → lispython-0.4.0}/PKG-INFO +2 -1
  3. {lispython-0.3.3 → lispython-0.4.0}/pyproject.toml +3 -2
  4. lispython-0.4.0/src/lispy/lsp/__init__.py +6 -0
  5. lispython-0.4.0/src/lispy/lsp/__main__.py +3 -0
  6. lispython-0.4.0/src/lispy/lsp/server.lpy +721 -0
  7. {lispython-0.3.3 → lispython-0.4.0}/uv.lock +62 -0
  8. {lispython-0.3.3 → lispython-0.4.0}/.claude/settings.local.json +0 -0
  9. {lispython-0.3.3 → lispython-0.4.0}/.gitignore +0 -0
  10. {lispython-0.3.3 → lispython-0.4.0}/.pre-commit-config.yaml +0 -0
  11. {lispython-0.3.3 → lispython-0.4.0}/LICENSE.md +0 -0
  12. {lispython-0.3.3 → lispython-0.4.0}/README.md +0 -0
  13. {lispython-0.3.3 → lispython-0.4.0}/docs/index.md +0 -0
  14. {lispython-0.3.3 → lispython-0.4.0}/docs/macros.md +0 -0
  15. {lispython-0.3.3 → lispython-0.4.0}/docs/syntax/expressions.md +0 -0
  16. {lispython-0.3.3 → lispython-0.4.0}/docs/syntax/overview.md +0 -0
  17. {lispython-0.3.3 → lispython-0.4.0}/docs/syntax/statements.md +0 -0
  18. {lispython-0.3.3 → lispython-0.4.0}/docs/usage/cli.md +0 -0
  19. {lispython-0.3.3 → lispython-0.4.0}/docs/usage/getting-started.md +0 -0
  20. {lispython-0.3.3 → lispython-0.4.0}/docs/version_macro.py +0 -0
  21. {lispython-0.3.3 → lispython-0.4.0}/docs/why-lispy.md +0 -0
  22. {lispython-0.3.3 → lispython-0.4.0}/mkdocs.yml +0 -0
  23. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/__init__.py +0 -0
  24. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/core/compiler/__init__.py +0 -0
  25. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/core/compiler/expr.py +0 -0
  26. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/core/compiler/literal.py +0 -0
  27. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/core/compiler/stmt.py +0 -0
  28. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/core/compiler/utils.py +0 -0
  29. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/core/importer.py +0 -0
  30. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/core/macro.py +0 -0
  31. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/core/meta_functions.py +0 -0
  32. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/core/nodes.py +0 -0
  33. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/core/parser.py +0 -0
  34. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/core/utils.py +0 -0
  35. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/core_meta_functions.lpy +0 -0
  36. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/macros/__init__.py +0 -0
  37. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/macros/init.lpy +0 -0
  38. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/macros/sugar.lpy +0 -0
  39. {lispython-0.3.3 → lispython-0.4.0}/src/lispy/tools.lpy +0 -0
  40. {lispython-0.3.3 → lispython-0.4.0}/tests/__init__.py +0 -0
  41. {lispython-0.3.3 → lispython-0.4.0}/tests/test_expr.py +0 -0
  42. {lispython-0.3.3 → lispython-0.4.0}/tests/test_include_meta.py +0 -0
  43. {lispython-0.3.3 → lispython-0.4.0}/tests/test_literal.py +0 -0
  44. {lispython-0.3.3 → lispython-0.4.0}/tests/test_meta_functions.py +0 -0
  45. {lispython-0.3.3 → lispython-0.4.0}/tests/test_parser.py +0 -0
  46. {lispython-0.3.3 → lispython-0.4.0}/tests/test_stmt.py +0 -0
  47. {lispython-0.3.3 → lispython-0.4.0}/tests/utils.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ "lispython.lsp.pythonPath": "/home/user/workspace/lispython/.venv/bin/python"
3
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lispython
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Lisp-like Syntax for Python with Lisp-like Macros
5
5
  Project-URL: Homepage, https://jetack.github.io/lispython
6
6
  Project-URL: Repository, https://github.com/jetack/lispython
@@ -8,6 +8,7 @@ Author-email: Jetack <jetack23@gmail.com>
8
8
  License: MIT
9
9
  License-File: LICENSE.md
10
10
  Requires-Python: >=3.11
11
+ Requires-Dist: pygls>=1.0.0
11
12
  Provides-Extra: dev
12
13
  Requires-Dist: pre-commit>=3.6.0; extra == 'dev'
13
14
  Requires-Dist: pytest>=8.0.0; extra == 'dev'
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "lispython"
3
- version = "0.3.3"
3
+ version = "0.4.0"
4
4
  description = "Lisp-like Syntax for Python with Lisp-like Macros"
5
5
  authors = [{ name = "Jetack", email = "jetack23@gmail.com" }]
6
6
  license = { text = "MIT" }
7
7
  readme = "README.md"
8
8
  requires-python = ">=3.11"
9
- dependencies = []
9
+ dependencies = ["pygls>=1.0.0"]
10
10
 
11
11
  [project.urls]
12
12
  Homepage = "https://jetack.github.io/lispython"
@@ -15,6 +15,7 @@ Repository = "https://github.com/jetack/lispython"
15
15
  [project.scripts]
16
16
  lpy = "lispy:run"
17
17
  l2py = "lispy:l2py"
18
+ lpy-lsp = "lispy.lsp:main"
18
19
  test = "pytest:main"
19
20
 
20
21
  [project.optional-dependencies]
@@ -0,0 +1,6 @@
1
+ import lispy # register .lpy import hook
2
+ from lispy.lsp.server import server
3
+
4
+
5
+ def main():
6
+ server.start_io()
@@ -0,0 +1,3 @@
1
+ from lispy.lsp import main
2
+
3
+ main()
@@ -0,0 +1,721 @@
1
+ (require lispy.macros *)
2
+
3
+ (import builtins)
4
+ (import glob)
5
+ (import inspect)
6
+ (import logging)
7
+ (import os)
8
+ (import os.path as osp)
9
+ (import re)
10
+ (import pathlib)
11
+
12
+ (from lsprotocol [types as lsp])
13
+ (from pygls.lsp.server [LanguageServer])
14
+
15
+ (from lispy.core.nodes *)
16
+ (from lispy.core.parser [parse])
17
+
18
+ (= logger (logging.getLogger __name__))
19
+
20
+ ;; ---------------------------------------------------------------------------
21
+ ;; Server instance
22
+ ;; ---------------------------------------------------------------------------
23
+
24
+ (= server (LanguageServer "lpy-lsp" "v0.1.0"))
25
+
26
+ ;; ---------------------------------------------------------------------------
27
+ ;; Special-form documentation (for hover)
28
+ ;; ---------------------------------------------------------------------------
29
+
30
+ (= SPECIAL-FORM-DOCS
31
+ {;; Statement forms
32
+ "def" "```\n(def name [params] body...)\n```\n\nDefine a function. Parameters are given in a bracket list. An optional docstring can follow the parameter list."
33
+ "async-def" "```\n(async-def name [params] body...)\n```\n\nDefine an async function (coroutine)."
34
+ "class" "```\n(class Name [bases] body...)\n```\n\nDefine a class. Bases are given in a bracket list."
35
+ "defmacro" "```\n(defmacro name [params] body...)\n```\n\nDefine a compile-time macro. The macro receives unevaluated S-expressions and must return an S-expression."
36
+ "if" "```\n(if condition then-body)\n(if condition then-body else-body)\n```\n\nConditional statement. Compiles to Python `if/else`."
37
+ "while" "```\n(while condition body...)\n```\n\nWhile loop."
38
+ "for" "```\n(for target iterable body...)\n```\n\nFor loop. `target` is bound to each element of `iterable`."
39
+ "async-for" "```\n(async-for target iterable body...)\n```\n\nAsync for loop — use inside `async-def`."
40
+ "do" "```\n(do body...)\n```\n\nExecute a sequence of statements (block). Used where a single expression is expected but multiple statements are needed."
41
+ "match" "```\n(match subject case...)\n```\n\nStructural pattern matching (Python 3.10+)."
42
+ "try" "```\n(try body except-clauses... [finally-clause])\n```\n\nTry/except/finally statement."
43
+ "with" "```\n(with [ctx-expr as-name] body...)\n```\n\nContext manager statement."
44
+ "async-with" "```\n(async-with [ctx-expr as-name] body...)\n```\n\nAsync context manager — use inside `async-def`."
45
+ "import" "```\n(import module)\n(import module :as alias)\n```\n\nImport a module."
46
+ "from" "```\n(from module [names...])\n(from module [name :as alias ...])\n```\n\nImport specific names from a module."
47
+ "require" "```\n(require module *)\n(require module [names...])\n```\n\nImport macros from a LisPython macro module at compile time."
48
+ "=" "```\n(= target value)\n```\n\nAssignment statement."
49
+ ":=" "```\n(:= target value)\n```\n\nWalrus operator (assignment expression)."
50
+ "return" "```\n(return value)\n```\n\nReturn a value from a function."
51
+ "raise" "```\n(raise exception)\n```\n\nRaise an exception."
52
+ "assert" "```\n(assert condition)\n(assert condition message)\n```\n\nAssert that a condition is true."
53
+ "del" "```\n(del target)\n```\n\nDelete a name or item."
54
+ "pass" "```\n(pass)\n```\n\nNo-op placeholder statement."
55
+ "break" "```\n(break)\n```\n\nBreak out of a loop."
56
+ "continue" "```\n(continue)\n```\n\nContinue to the next loop iteration."
57
+ "global" "```\n(global name...)\n```\n\nDeclare names as global variables."
58
+ "nonlocal" "```\n(nonlocal name...)\n```\n\nDeclare names as nonlocal variables."
59
+ "yield" "```\n(yield value)\n```\n\nYield a value from a generator."
60
+ "yield-from" "```\n(yield-from iterable)\n```\n\nYield all values from a sub-generator."
61
+ "await" "```\n(await expr)\n```\n\nAwait a coroutine."
62
+ "deco" "```\n(deco decorator (def ...))\n```\n\nApply a decorator to the following function/class definition."
63
+ "lambda" "```\n(lambda [params] body)\n```\n\nAnonymous function (lambda expression)."
64
+ ;; Expression forms
65
+ "ife" "```\n(ife condition then-expr else-expr)\n```\n\nTernary conditional expression (`then-expr if condition else else-expr`)."
66
+ "." "```\n(. obj attr)\n```\n\nAttribute access (`obj.attr`)."
67
+ "sub" "```\n(sub obj index)\n```\n\nSubscript access (`obj[index]`)."
68
+ "," "```\n(, a b c)\n```\n\nTuple literal."
69
+ "fn" "```\n(fn [params] body)\n```\n\nLambda alias — same as `lambda`."
70
+ ;; Operators
71
+ "+" "Arithmetic addition / unary positive."
72
+ "-" "Arithmetic subtraction / unary negative."
73
+ "*" "Arithmetic multiplication / iterable unpacking."
74
+ "/" "True division."
75
+ "//" "Floor (integer) division."
76
+ "%" "Modulo (remainder)."
77
+ "**" "Exponentiation."
78
+ "@" "Matrix multiplication."
79
+ "<<" "Bitwise left shift."
80
+ ">>" "Bitwise right shift."
81
+ "|" "Bitwise OR."
82
+ "^" "Bitwise XOR."
83
+ "&" "Bitwise AND."
84
+ "~" "Bitwise NOT / unquote (inside quasiquote)."
85
+ "and" "Logical AND (short-circuit)."
86
+ "or" "Logical OR (short-circuit)."
87
+ "not" "Logical NOT."
88
+ "==" "Equality test."
89
+ "!=" "Inequality test."
90
+ "<" "Less than."
91
+ "<=" "Less than or equal."
92
+ ">" "Greater than."
93
+ ">=" "Greater than or equal."
94
+ "is" "Identity test (`x is y`)."
95
+ "is-not" "Negated identity test (`x is not y`)."
96
+ "in" "Membership test (`x in y`)."
97
+ "not-in" "Negated membership test (`x not in y`)."
98
+ ;; Macro-related
99
+ "'" "Quote — prevent evaluation; return the form as data."
100
+ "`" "Quasiquote — like quote but allows unquote (`~`) splicing."
101
+ "~@" "Unquote-splice — splice a list into a quasiquote."
102
+ "f-string" "```\nf\"text {expr} more\"\n```\n\nFormatted string literal (f-string)."})
103
+
104
+ ;; ---------------------------------------------------------------------------
105
+ ;; Python builtins introspection
106
+ ;; ---------------------------------------------------------------------------
107
+
108
+ (def build-builtin-info []
109
+ "Build a dict of Python builtin names -> {signature, doc, source_file, lineno}."
110
+ (= info {})
111
+ (for name in (dir builtins)
112
+ (if (.startswith name "_")
113
+ (continue))
114
+ (= obj (getattr builtins name))
115
+ (if (and (not (callable obj)) (not (isinstance obj type)))
116
+ (continue))
117
+ (= entry {"name" name})
118
+ ;; Signature
119
+ (try
120
+ (do (= sig (inspect.signature obj))
121
+ (= (sub entry "signature") f"{name}{sig}"))
122
+ (except [(, ValueError TypeError)]
123
+ (= (sub entry "signature") name)))
124
+ ;; Docstring
125
+ (= doc (inspect.getdoc obj))
126
+ (if doc
127
+ (= (sub entry "doc") (sub (.split doc "\n\n") 0))
128
+ (= (sub entry "doc") ""))
129
+ ;; Source file
130
+ (try
131
+ (do (= source-file (inspect.getfile obj))
132
+ (try
133
+ (do (= (, _ lineno) (inspect.getsourcelines obj))
134
+ (= (sub entry "lineno") lineno))
135
+ (except [(, OSError TypeError)]
136
+ (= (sub entry "lineno") 1)))
137
+ (= (sub entry "source_file") source-file))
138
+ (except [(, TypeError OSError)]
139
+ (= (sub entry "source_file") None)
140
+ (= (sub entry "lineno") None)))
141
+ (= (sub info name) entry))
142
+ (return info))
143
+
144
+ (= BUILTIN-INFO (build-builtin-info))
145
+
146
+ ;; ---------------------------------------------------------------------------
147
+ ;; Workspace index
148
+ ;; ---------------------------------------------------------------------------
149
+
150
+ ;; Global index: {symbol_name: [Location, ...]} across all .lpy files
151
+ (= WORKSPACE-INDEX {})
152
+ ;; Cache of parsed trees per URI: {uri: tree}
153
+ (= FILE-TREES {})
154
+
155
+ (def uri-to-path [uri]
156
+ "Convert a file:// URI to a filesystem path."
157
+ (if (.startswith uri "file://")
158
+ (return (sub uri [: 7 None]))
159
+ (return uri)))
160
+
161
+ (def path-to-uri [path]
162
+ "Convert a filesystem path to a file:// URI."
163
+ (return (.as-uri (pathlib.Path path))))
164
+
165
+ (def index-file [uri]
166
+ "Parse a .lpy file and add its definitions to the workspace index.
167
+ Removes old entries for this URI before re-indexing."
168
+ ;; Remove old entries for this URI
169
+ (= to-remove [])
170
+ (for [name locs] in (.items WORKSPACE-INDEX)
171
+ (= (sub WORKSPACE-INDEX name)
172
+ [loc for loc in locs if (!= loc.uri uri)])
173
+ (if (== (len (sub WORKSPACE-INDEX name)) 0)
174
+ (.append to-remove name)))
175
+ (for name in to-remove
176
+ (del (sub WORKSPACE-INDEX name)))
177
+
178
+ ;; Parse and collect definitions
179
+ (= fpath (uri-to-path uri))
180
+ (if (not (osp.isfile fpath))
181
+ (return))
182
+ (try
183
+ (with [(open fpath "rb") as f]
184
+ (= src (.decode (f.read) "utf-8")))
185
+ (= tree (parse src))
186
+ (= (sub FILE-TREES uri) tree)
187
+ (collect-definitions tree uri :defs WORKSPACE-INDEX)
188
+ (except [Exception as exc]
189
+ (logger.debug f"Failed to index {fpath}: {exc}"))))
190
+
191
+ (def index-workspace [workspace-folders]
192
+ "Scan all .lpy files in workspace folders and index them."
193
+ (for folder in workspace-folders
194
+ (= root (uri-to-path folder.uri))
195
+ (for fpath in (glob.glob (osp.join root "**" "*.lpy") :recursive True)
196
+ (index-file (path-to-uri fpath))))
197
+ (logger.info f"Indexed {(len WORKSPACE-INDEX)} symbols from workspace"))
198
+
199
+ (def resolve-module-to-file [module-name workspace-folders]
200
+ "Resolve a LisPython module name to a .lpy file path.
201
+ e.g., 'lispy.tools' -> '/path/to/src/lispy/tools.lpy'"
202
+ (= parts (.split (.replace module-name "-" "_") "."))
203
+ (= rel-path (+ (osp.join *parts) ".lpy"))
204
+ ;; Search in workspace folders and sys.path-like locations
205
+ (for folder in workspace-folders
206
+ (= root (uri-to-path folder.uri))
207
+ ;; Direct match
208
+ (= candidate (osp.join root rel-path))
209
+ (if (osp.isfile candidate)
210
+ (return candidate))
211
+ ;; Check in src/ subdirectory
212
+ (= candidate (osp.join root "src" rel-path))
213
+ (if (osp.isfile candidate)
214
+ (return candidate)))
215
+ (return None))
216
+
217
+ (def find-import-target [node workspace-folders]
218
+ "Given an import/from/require node, resolve where the imported name is defined."
219
+ (= form (. node op value))
220
+ (cond
221
+ (== form "from")
222
+ (do ;; (from module [names...])
223
+ (if (< (len node) 3)
224
+ (return None))
225
+ (= module-name (. (sub node 1) value))
226
+ (= fpath (resolve-module-to-file module-name workspace-folders))
227
+ (if fpath
228
+ (return fpath))
229
+ (return None))
230
+
231
+ (== form "import")
232
+ (do ;; (import module)
233
+ (if (< (len node) 2)
234
+ (return None))
235
+ (= module-name (. (sub node 1) value))
236
+ (= fpath (resolve-module-to-file module-name workspace-folders))
237
+ (if fpath
238
+ (return fpath))
239
+ (return None))
240
+
241
+ (== form "require")
242
+ (do ;; (require module *)
243
+ (if (< (len node) 2)
244
+ (return None))
245
+ (= module-name (. (sub node 1) value))
246
+ (= fpath (resolve-module-to-file module-name workspace-folders))
247
+ (if fpath
248
+ (return fpath))
249
+ (return None))
250
+
251
+ (return None)))
252
+
253
+ (def find-import-for-name [tree name]
254
+ "Find the import/from/require node that imports a given name."
255
+ (for node in tree
256
+ (if (not (isinstance node Paren))
257
+ (continue))
258
+ (if (== (len node) 0)
259
+ (continue))
260
+ (= form (ife (isinstance node.op Symbol) node.op.value ""))
261
+ (cond
262
+ (== form "from")
263
+ (do ;; (from module [names...])
264
+ (for child in (sub node.list [: 1 None])
265
+ (if (isinstance child Bracket)
266
+ (for item in child.list
267
+ (if (and (isinstance item Symbol) (== item.value name))
268
+ (return node))))))
269
+ (== form "import")
270
+ (do ;; (import module) or (import module as alias)
271
+ (for child in (sub node.list [: 1 None])
272
+ (if (and (isinstance child Symbol) (== child.value name))
273
+ (return node))))
274
+ (== form "require")
275
+ (do ;; (require module *) - imports all macros
276
+ (return node))))
277
+ (return None))
278
+
279
+ ;; ---------------------------------------------------------------------------
280
+ ;; Helpers
281
+ ;; ---------------------------------------------------------------------------
282
+
283
+ (def make-position [line char]
284
+ (return (lsp.Position :line line :character char)))
285
+
286
+ (def make-range [start-line start-char end-line end-char]
287
+ (return (lsp.Range
288
+ :start (make-position start-line start-char)
289
+ :end (make-position end-line end-char))))
290
+
291
+ (def node-range [node]
292
+ "Convert a LisPython AST node's position info to an LSP Range.
293
+ The parser uses 1-based line numbers; LSP uses 0-based."
294
+ (return (make-range
295
+ (- node.lineno 1)
296
+ node.col_offset
297
+ (- node.end_lineno 1)
298
+ node.end_col_offset)))
299
+
300
+ (def extract-error-position [exc src]
301
+ "Best-effort extraction of line/col from a parse or compile error."
302
+ ;; Some Python exceptions carry lineno/offset
303
+ (= lineno (getattr exc "lineno" None))
304
+ (if (is-not lineno None)
305
+ (do (= col (- (or (getattr exc "offset" None) 1) 1))
306
+ (return (, (- lineno 1) col (- lineno 1) col))))
307
+ ;; Try to extract from the message
308
+ (= msg (str exc))
309
+ (= m (re.search "line\\s+(\\d+)" msg re.IGNORECASE))
310
+ (if m
311
+ (do (= line (- (int (.group m 1)) 1))
312
+ (= col 0)
313
+ (= mc (re.search "col(?:umn)?\\s+(\\d+)" msg re.IGNORECASE))
314
+ (if mc
315
+ (= col (int (.group mc 1))))
316
+ (return (, line col line col))))
317
+ (return None))
318
+
319
+ ;; ---------------------------------------------------------------------------
320
+ ;; Diagnostics
321
+ ;; ---------------------------------------------------------------------------
322
+
323
+ (def compute-diagnostics [source uri]
324
+ (= diagnostics [])
325
+ ;; 1. Parse
326
+ (try
327
+ (= tree (parse source))
328
+ (except [Exception as exc]
329
+ (= pos (extract-error-position exc source))
330
+ (if (is pos None)
331
+ (= pos (, 0 0 0 0)))
332
+ (.append diagnostics
333
+ (lsp.Diagnostic
334
+ :range (make-range *pos)
335
+ :severity lsp.DiagnosticSeverity.Error
336
+ :source "lpy-lsp"
337
+ :message f"Parse error: {exc}"))
338
+ (return diagnostics)))
339
+ ;; 2. Compile (best-effort)
340
+ (try
341
+ (do (from lispy.core.macro [macroexpand-then-compile])
342
+ (macroexpand-then-compile tree))
343
+ (except [Exception as exc]
344
+ (= pos (extract-error-position exc source))
345
+ (if (is pos None)
346
+ (= pos (, 0 0 0 0)))
347
+ (.append diagnostics
348
+ (lsp.Diagnostic
349
+ :range (make-range *pos)
350
+ :severity lsp.DiagnosticSeverity.Error
351
+ :source "lpy-lsp"
352
+ :message f"Compile error: {exc}"))))
353
+ (return diagnostics))
354
+
355
+ (def publish-diagnostics [ls uri]
356
+ (= doc (.get-text-document ls.workspace uri))
357
+ (= diagnostics (compute-diagnostics doc.source uri))
358
+ (.text-document-publish-diagnostics ls
359
+ (lsp.PublishDiagnosticsParams :uri uri :diagnostics diagnostics)))
360
+
361
+ ;; ---------------------------------------------------------------------------
362
+ ;; Document symbols
363
+ ;; ---------------------------------------------------------------------------
364
+
365
+ (def symbol-name [node]
366
+ "Extract a string name from a node."
367
+ (cond (isinstance node Symbol)
368
+ (return node.value)
369
+ (isinstance node Annotation)
370
+ (return (symbol-name node.value))
371
+ (isinstance node String)
372
+ (return (.strip node.value "\"'"))
373
+ (return (str node))))
374
+
375
+ (def walk-symbols [nodes]
376
+ "Walk top-level parsed forms and produce DocumentSymbol entries."
377
+ (= symbols [])
378
+ (for node in nodes
379
+ (if (or (not (isinstance node Paren)) (== (len node) 0))
380
+ (continue))
381
+ (= op node.op)
382
+ (if (not (isinstance op Symbol))
383
+ (continue))
384
+ (= form op.value)
385
+
386
+ (cond
387
+ (in form (, "def" "async-def"))
388
+ (do (if (< (len node) 2) (continue))
389
+ (= name (symbol-name (sub node 1)))
390
+ (= rng (node-range node))
391
+ (= sel (node-range (sub node 1)))
392
+ (= children (ife (> (len node) 3)
393
+ (walk-symbols (sub node.list [: 3 None]))
394
+ []))
395
+ (.append symbols
396
+ (lsp.DocumentSymbol
397
+ :name name
398
+ :kind lsp.SymbolKind.Function
399
+ :range rng
400
+ :selection-range sel
401
+ :children (or children None))))
402
+
403
+ (== form "class")
404
+ (do (if (< (len node) 2) (continue))
405
+ (= name (symbol-name (sub node 1)))
406
+ (= rng (node-range node))
407
+ (= sel (node-range (sub node 1)))
408
+ (= body-nodes (ife (> (len node) 3)
409
+ (sub node.list [: 3 None])
410
+ (sub node.list [: 2 None])))
411
+ (= children (walk-symbols body-nodes))
412
+ (.append symbols
413
+ (lsp.DocumentSymbol
414
+ :name name
415
+ :kind lsp.SymbolKind.Class
416
+ :range rng
417
+ :selection-range sel
418
+ :children (or children None))))
419
+
420
+ (== form "defmacro")
421
+ (do (if (< (len node) 2) (continue))
422
+ (.append symbols
423
+ (lsp.DocumentSymbol
424
+ :name (symbol-name (sub node 1))
425
+ :kind lsp.SymbolKind.Function
426
+ :range (node-range node)
427
+ :selection-range (node-range (sub node 1))
428
+ :detail "macro")))
429
+
430
+ (== form "=")
431
+ (do (if (< (len node) 2) (continue))
432
+ (.append symbols
433
+ (lsp.DocumentSymbol
434
+ :name (symbol-name (sub node 1))
435
+ :kind lsp.SymbolKind.Variable
436
+ :range (node-range node)
437
+ :selection-range (node-range (sub node 1)))))
438
+
439
+ (in form (, "import" "from"))
440
+ (do (if (< (len node) 2) (continue))
441
+ (.append symbols
442
+ (lsp.DocumentSymbol
443
+ :name (symbol-name (sub node 1))
444
+ :kind lsp.SymbolKind.Module
445
+ :range (node-range node)
446
+ :selection-range (node-range (sub node 1)))))
447
+
448
+ (== form "require")
449
+ (do (if (< (len node) 2) (continue))
450
+ (.append symbols
451
+ (lsp.DocumentSymbol
452
+ :name (symbol-name (sub node 1))
453
+ :kind lsp.SymbolKind.Module
454
+ :range (node-range node)
455
+ :selection-range (node-range (sub node 1))
456
+ :detail "macro require")))))
457
+
458
+ (return symbols))
459
+
460
+ ;; ---------------------------------------------------------------------------
461
+ ;; Hover
462
+ ;; ---------------------------------------------------------------------------
463
+
464
+ (def node-at-position [nodes line-0 col]
465
+ "Find the most specific node at the given 0-based position."
466
+ (for node in nodes
467
+ (= n-start-line (- node.lineno 1))
468
+ (= n-start-col node.col_offset)
469
+ (= n-end-line (- node.end_lineno 1))
470
+ (= n-end-col node.end_col_offset)
471
+ ;; Check if position is inside this node's range
472
+ (if (< (, line-0 col) (, n-start-line n-start-col))
473
+ (continue))
474
+ (if (> (, line-0 col) (, n-end-line n-end-col))
475
+ (continue))
476
+ ;; Position is within this node — try to go deeper
477
+ (if (isinstance node Expression)
478
+ (do (= deeper (node-at-position node.list line-0 col))
479
+ (if (is-not deeper None)
480
+ (return deeper))))
481
+ (if (isinstance node (, Wrapper MetaIndicator))
482
+ (do (= deeper (node-at-position [node.value] line-0 col))
483
+ (if (is-not deeper None)
484
+ (return deeper))))
485
+ (return node))
486
+ (return None))
487
+
488
+ ;; ---------------------------------------------------------------------------
489
+ ;; Definition index
490
+ ;; ---------------------------------------------------------------------------
491
+
492
+ (def collect-definitions [nodes uri :defs None]
493
+ "Walk AST and collect all definition locations keyed by symbol name."
494
+ (if (is defs None)
495
+ (= defs {}))
496
+
497
+ (for node in nodes
498
+ (if (or (not (isinstance node Paren)) (== (len node) 0))
499
+ (continue))
500
+ (= op node.op)
501
+ (if (not (isinstance op Symbol))
502
+ (continue))
503
+ (= form op.value)
504
+
505
+ (cond
506
+ (in form (, "def" "async-def" "defmacro"))
507
+ (do (if (and (>= (len node) 2) (isinstance (sub node 1) Symbol))
508
+ (do (= name (. (sub node 1) value))
509
+ (= loc (lsp.Location :uri uri :range (node-range (sub node 1))))
510
+ (.append (.setdefault defs name []) loc)
511
+ (if (> (len node) 3)
512
+ (collect-definitions (sub node.list [: 3 None]) uri :defs defs)))))
513
+
514
+ (== form "class")
515
+ (do (if (and (>= (len node) 2) (isinstance (sub node 1) Symbol))
516
+ (do (= name (. (sub node 1) value))
517
+ (= loc (lsp.Location :uri uri :range (node-range (sub node 1))))
518
+ (.append (.setdefault defs name []) loc)
519
+ (= body (ife (> (len node) 3)
520
+ (sub node.list [: 3 None])
521
+ (sub node.list [: 2 None])))
522
+ (collect-definitions body uri :defs defs))))
523
+
524
+ (== form "=")
525
+ (do (if (and (>= (len node) 2) (isinstance (sub node 1) Symbol))
526
+ (do (= name (. (sub node 1) value))
527
+ (= loc (lsp.Location :uri uri :range (node-range (sub node 1))))
528
+ (.append (.setdefault defs name []) loc))))
529
+
530
+ (in form (, "for" "async-for"))
531
+ (do (if (and (>= (len node) 2) (isinstance (sub node 1) Symbol))
532
+ (do (= name (. (sub node 1) value))
533
+ (= loc (lsp.Location :uri uri :range (node-range (sub node 1))))
534
+ (.append (.setdefault defs name []) loc)))
535
+ (collect-definitions (sub node.list [: 1 None]) uri :defs defs))
536
+
537
+ (in form (, "import" "from"))
538
+ (do (for child in (sub node.list [: 1 None])
539
+ (cond
540
+ (and (isinstance child Symbol) (not-in child.value (, "as" "*")))
541
+ (do (= loc (lsp.Location :uri uri :range (node-range child)))
542
+ (.append (.setdefault defs child.value []) loc))
543
+ (isinstance child Bracket)
544
+ (for item in child.list
545
+ (if (and (isinstance item Symbol) (!= item.value "as"))
546
+ (do (= loc (lsp.Location :uri uri :range (node-range item)))
547
+ (.append (.setdefault defs item.value []) loc)))))))
548
+
549
+ ;; Default: recurse into nested paren forms
550
+ (do (collect-definitions
551
+ [c for c in node.list if (isinstance c Paren)]
552
+ uri :defs defs))))
553
+
554
+ (return defs))
555
+
556
+ ;; ---------------------------------------------------------------------------
557
+ ;; References
558
+ ;; ---------------------------------------------------------------------------
559
+
560
+ (def collect-references [nodes name uri]
561
+ "Find all occurrences of a symbol name in the AST."
562
+ (= refs [])
563
+ (for node in nodes
564
+ (if (and (isinstance node Symbol) (== node.value name))
565
+ (.append refs (lsp.Location :uri uri :range (node-range node))))
566
+ (if (isinstance node Expression)
567
+ (.extend refs (collect-references node.list name uri)))
568
+ (if (and (isinstance node (, Wrapper MetaIndicator))
569
+ (is-not node.value None))
570
+ (.extend refs (collect-references [node.value] name uri))))
571
+ (return refs))
572
+
573
+ ;; ---------------------------------------------------------------------------
574
+ ;; LSP event handlers
575
+ ;; ---------------------------------------------------------------------------
576
+
577
+ (deco (server.feature lsp.INITIALIZED)
578
+ (def on-initialized [ls params]
579
+ (= folders (ife ls.workspace.folders
580
+ (list (.values ls.workspace.folders))
581
+ []))
582
+ (if folders
583
+ (index-workspace folders))))
584
+
585
+ (deco (server.feature lsp.TEXT_DOCUMENT_DID_OPEN)
586
+ (def did-open [ls params]
587
+ (= uri params.text_document.uri)
588
+ (index-file uri)
589
+ (publish-diagnostics ls uri)))
590
+
591
+ (deco (server.feature lsp.TEXT_DOCUMENT_DID_CHANGE)
592
+ (def did-change [ls params]
593
+ (publish-diagnostics ls params.text_document.uri)))
594
+
595
+ (deco (server.feature lsp.TEXT_DOCUMENT_DID_SAVE)
596
+ (def did-save [ls params]
597
+ (= uri params.text_document.uri)
598
+ (index-file uri)
599
+ (publish-diagnostics ls uri)))
600
+
601
+ (deco (server.feature lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
602
+ (def document-symbol [ls params]
603
+ (= doc (.get-text-document ls.workspace params.text_document.uri))
604
+ (try
605
+ (= tree (parse doc.source))
606
+ (except [Exception]
607
+ (return [])))
608
+ (return (walk-symbols tree))))
609
+
610
+ (deco (server.feature lsp.TEXT_DOCUMENT_HOVER)
611
+ (def hover [ls params]
612
+ (= doc (.get-text-document ls.workspace params.text_document.uri))
613
+ (try
614
+ (= tree (parse doc.source))
615
+ (except [Exception]
616
+ (return None)))
617
+ (= pos params.position)
618
+ (= target (node-at-position tree pos.line pos.character))
619
+ (if (is target None)
620
+ (return None))
621
+ ;; Look up documentation for the symbol
622
+ (if (isinstance target Symbol)
623
+ (do
624
+ (= name target.value)
625
+ ;; 1. LisPython special forms
626
+ (= doc-text (.get SPECIAL-FORM-DOCS name))
627
+ (if doc-text
628
+ (return (lsp.Hover
629
+ :contents (lsp.MarkupContent
630
+ :kind lsp.MarkupKind.Markdown
631
+ :value doc-text)
632
+ :range (node-range target))))
633
+ ;; 2. Python builtins
634
+ (= builtin-name (.replace name "-" "_"))
635
+ (= bi (.get BUILTIN-INFO builtin-name))
636
+ (if bi
637
+ (do (= sig (sub bi "signature"))
638
+ (= doc-body (sub bi "doc"))
639
+ (= hover-md f"```python\n{sig}\n```\n\n*(Python builtin)*")
640
+ (if doc-body
641
+ (+= hover-md f"\n\n{doc-body}"))
642
+ (return (lsp.Hover
643
+ :contents (lsp.MarkupContent
644
+ :kind lsp.MarkupKind.Markdown
645
+ :value hover-md)
646
+ :range (node-range target)))))))
647
+ (return None)))
648
+
649
+ (deco (server.feature lsp.TEXT_DOCUMENT_DEFINITION)
650
+ (def definition [ls params]
651
+ (= doc (.get-text-document ls.workspace params.text_document.uri))
652
+ (try
653
+ (= tree (parse doc.source))
654
+ (except [Exception]
655
+ (return None)))
656
+ (= pos params.position)
657
+ (= target (node-at-position tree pos.line pos.character))
658
+ (if (or (is target None) (not (isinstance target Symbol)))
659
+ (return None))
660
+ (= name target.value)
661
+ (if (in name SPECIAL-FORM-DOCS)
662
+ (return None))
663
+
664
+ ;; 1. Local definitions in the file
665
+ (= defs (collect-definitions tree doc.uri))
666
+ (= local (.get defs name))
667
+ (if local
668
+ (return local))
669
+
670
+ ;; 2. Resolve via import statements — find which module imports this name
671
+ (= folders (ife ls.workspace.folders
672
+ (list (.values ls.workspace.folders))
673
+ []))
674
+ (= import-node (find-import-for-name tree name))
675
+ (if import-node
676
+ (do (= fpath (find-import-target import-node folders))
677
+ (if fpath
678
+ (do (= target-uri (path-to-uri fpath))
679
+ ;; Make sure the target file is indexed
680
+ (if (not-in target-uri FILE-TREES)
681
+ (index-file target-uri))
682
+ ;; Look up the name in the target file's definitions
683
+ (= target-tree (.get FILE-TREES target-uri))
684
+ (if target-tree
685
+ (do (= target-defs (collect-definitions target-tree target-uri))
686
+ (= result (.get target-defs name))
687
+ (if result
688
+ (return result))))))))
689
+
690
+ ;; 3. Workspace-wide index lookup
691
+ (= ws-result (.get WORKSPACE-INDEX name))
692
+ (if ws-result
693
+ ;; Filter out definitions from the current file (already checked)
694
+ (do (= external [loc for loc in ws-result if (!= loc.uri doc.uri)])
695
+ (if external
696
+ (return external))))
697
+
698
+ ;; 4. Python builtins with source files
699
+ (= builtin-name (.replace name "-" "_"))
700
+ (= bi (.get BUILTIN-INFO builtin-name))
701
+ (if (and bi (sub bi "source_file"))
702
+ (do (= source-path (pathlib.Path (sub bi "source_file")))
703
+ (if (.exists source-path)
704
+ (do (= lineno (- (or (sub bi "lineno") 1) 1))
705
+ (return [(lsp.Location
706
+ :uri (.as-uri source-path)
707
+ :range (make-range lineno 0 lineno 0))])))))
708
+ (return None)))
709
+
710
+ (deco (server.feature lsp.TEXT_DOCUMENT_REFERENCES)
711
+ (def references [ls params]
712
+ (= doc (.get-text-document ls.workspace params.text_document.uri))
713
+ (try
714
+ (= tree (parse doc.source))
715
+ (except [Exception]
716
+ (return None)))
717
+ (= pos params.position)
718
+ (= target (node-at-position tree pos.line pos.character))
719
+ (if (or (is target None) (not (isinstance target Symbol)))
720
+ (return None))
721
+ (return (collect-references tree target.value doc.uri))))
@@ -2,6 +2,15 @@ version = 1
2
2
  revision = 3
3
3
  requires-python = ">=3.11"
4
4
 
5
+ [[package]]
6
+ name = "attrs"
7
+ version = "26.1.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
12
+ ]
13
+
5
14
  [[package]]
6
15
  name = "babel"
7
16
  version = "2.17.0"
@@ -25,6 +34,19 @@ wheels = [
25
34
  { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" },
26
35
  ]
27
36
 
37
+ [[package]]
38
+ name = "cattrs"
39
+ version = "26.1.0"
40
+ source = { registry = "https://pypi.org/simple" }
41
+ dependencies = [
42
+ { name = "attrs" },
43
+ { name = "typing-extensions" },
44
+ ]
45
+ sdist = { url = "https://files.pythonhosted.org/packages/a0/ec/ba18945e7d6e55a58364d9fb2e46049c1c2998b3d805f19b703f14e81057/cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40", size = 495672, upload-time = "2026-02-18T22:15:19.406Z" }
46
+ wheels = [
47
+ { url = "https://files.pythonhosted.org/packages/80/56/60547f7801b97c67e97491dc3d9ade9fbccbd0325058fd3dfcb2f5d98d90/cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096", size = 73054, upload-time = "2026-02-18T22:15:17.958Z" },
48
+ ]
49
+
28
50
  [[package]]
29
51
  name = "certifi"
30
52
  version = "2025.11.12"
@@ -240,6 +262,9 @@ wheels = [
240
262
  name = "lispython"
241
263
  version = "0.3.3"
242
264
  source = { editable = "." }
265
+ dependencies = [
266
+ { name = "pygls" },
267
+ ]
243
268
 
244
269
  [package.optional-dependencies]
245
270
  dev = [
@@ -260,12 +285,26 @@ requires-dist = [
260
285
  { name = "mkdocs-macros-plugin", marker = "extra == 'docs'", specifier = ">=1.3.7" },
261
286
  { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.6.14" },
262
287
  { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.6.0" },
288
+ { name = "pygls", specifier = ">=1.0.0" },
263
289
  { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
264
290
  { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0" },
265
291
  { name = "toml", marker = "extra == 'dev'", specifier = ">=0.10.2" },
266
292
  ]
267
293
  provides-extras = ["dev", "docs"]
268
294
 
295
+ [[package]]
296
+ name = "lsprotocol"
297
+ version = "2025.0.0"
298
+ source = { registry = "https://pypi.org/simple" }
299
+ dependencies = [
300
+ { name = "attrs" },
301
+ { name = "cattrs" },
302
+ ]
303
+ sdist = { url = "https://files.pythonhosted.org/packages/e9/26/67b84e6ec1402f0e6764ef3d2a0aaf9a79522cc1d37738f4e5bb0b21521a/lsprotocol-2025.0.0.tar.gz", hash = "sha256:e879da2b9301e82cfc3e60d805630487ac2f7ab17492f4f5ba5aaba94fe56c29", size = 74896, upload-time = "2025-06-17T21:30:18.156Z" }
304
+ wheels = [
305
+ { url = "https://files.pythonhosted.org/packages/7b/f0/92f2d609d6642b5f30cb50a885d2bf1483301c69d5786286500d15651ef2/lsprotocol-2025.0.0-py3-none-any.whl", hash = "sha256:f9d78f25221f2a60eaa4a96d3b4ffae011b107537facee61d3da3313880995c7", size = 76250, upload-time = "2025-06-17T21:30:19.455Z" },
306
+ ]
307
+
269
308
  [[package]]
270
309
  name = "markdown"
271
310
  version = "3.10"
@@ -537,6 +576,20 @@ wheels = [
537
576
  { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
538
577
  ]
539
578
 
579
+ [[package]]
580
+ name = "pygls"
581
+ version = "2.1.1"
582
+ source = { registry = "https://pypi.org/simple" }
583
+ dependencies = [
584
+ { name = "attrs" },
585
+ { name = "cattrs" },
586
+ { name = "lsprotocol" },
587
+ ]
588
+ sdist = { url = "https://files.pythonhosted.org/packages/da/2e/7bbe061d175c0baddde8fc9edb908a4c31ba5d9165b8c68e3439c3a9f138/pygls-2.1.1.tar.gz", hash = "sha256:1da03ba9053201bb337dcdd8d121df70feb2a91e1a0dcc74de5da79755b1a201", size = 55091, upload-time = "2026-03-25T11:19:10.541Z" }
589
+ wheels = [
590
+ { url = "https://files.pythonhosted.org/packages/fd/1a/208293b6c350f5abea6941d5606080d4a492644052504f5312e5de30a902/pygls-2.1.1-py3-none-any.whl", hash = "sha256:510a6dea2476177230c7d851125e5948efdf3fdb9ebfd8543fc434972f8faed4", size = 68975, upload-time = "2026-03-25T11:19:11.374Z" },
591
+ ]
592
+
540
593
  [[package]]
541
594
  name = "pygments"
542
595
  version = "2.19.2"
@@ -743,6 +796,15 @@ wheels = [
743
796
  { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" },
744
797
  ]
745
798
 
799
+ [[package]]
800
+ name = "typing-extensions"
801
+ version = "4.15.0"
802
+ source = { registry = "https://pypi.org/simple" }
803
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
804
+ wheels = [
805
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
806
+ ]
807
+
746
808
  [[package]]
747
809
  name = "urllib3"
748
810
  version = "2.6.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes