lampson 0.1.0

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 (62) hide show
  1. package/.env.example +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +382 -0
  4. package/bin/lampson.js +81 -0
  5. package/chat.syn +799 -0
  6. package/lamps/example-hello/lamp.json +16 -0
  7. package/lamps/example-hello/lamp.syn +19 -0
  8. package/lampson.cmd +4 -0
  9. package/lampson.ps1 +88 -0
  10. package/lampson.sh +42 -0
  11. package/lib/agents.syn +471 -0
  12. package/lib/git.syn +58 -0
  13. package/lib/lamps.syn +386 -0
  14. package/lib/loop.syn +455 -0
  15. package/lib/lsp.syn +503 -0
  16. package/lib/mcp.syn +403 -0
  17. package/lib/permission.syn +154 -0
  18. package/lib/prompt.syn +75 -0
  19. package/lib/provider.syn +522 -0
  20. package/lib/session.syn +111 -0
  21. package/lib/settings.syn +70 -0
  22. package/lib/skills.syn +179 -0
  23. package/lib/tools/bash.syn +105 -0
  24. package/lib/tools/common.sh +49 -0
  25. package/lib/tools/common.syn +91 -0
  26. package/lib/tools/edit.syn +32 -0
  27. package/lib/tools/find.syn +35 -0
  28. package/lib/tools/grep.syn +31 -0
  29. package/lib/tools/img.ps1 +36 -0
  30. package/lib/tools/img.sh +22 -0
  31. package/lib/tools/ls.syn +18 -0
  32. package/lib/tools/memo.syn +148 -0
  33. package/lib/tools/proc.sh +42 -0
  34. package/lib/tools/proc.syn +314 -0
  35. package/lib/tools/process.syn +46 -0
  36. package/lib/tools/read.syn +25 -0
  37. package/lib/tools/skill.syn +14 -0
  38. package/lib/tools/todo.syn +97 -0
  39. package/lib/tools/write.syn +22 -0
  40. package/lib/tools.syn +198 -0
  41. package/lib/trace.syn +116 -0
  42. package/lib/tree.syn +59 -0
  43. package/lib/update.syn +57 -0
  44. package/package.json +40 -0
  45. package/public/fonts/plex-mono-400-latin-ext.woff2 +0 -0
  46. package/public/fonts/plex-mono-400-latin.woff2 +0 -0
  47. package/public/fonts/plex-mono-600-latin-ext.woff2 +0 -0
  48. package/public/fonts/plex-mono-600-latin.woff2 +0 -0
  49. package/public/fonts/plex-serif-400-latin-ext.woff2 +0 -0
  50. package/public/fonts/plex-serif-400-latin.woff2 +0 -0
  51. package/public/fonts/plex-serif-400i-latin-ext.woff2 +0 -0
  52. package/public/fonts/plex-serif-400i-latin.woff2 +0 -0
  53. package/public/fonts/plex-serif-600-latin-ext.woff2 +0 -0
  54. package/public/fonts/plex-serif-600-latin.woff2 +0 -0
  55. package/public/index.html +1268 -0
  56. package/public/vendor/xterm-addon-fit.js +2 -0
  57. package/public/vendor/xterm.css +218 -0
  58. package/public/vendor/xterm.js +2 -0
  59. package/skills/debugging/SKILL.md +33 -0
  60. package/skills/lampson/SKILL.md +117 -0
  61. package/skills/synsema/SKILL.md +75 -0
  62. package/web.syn +468 -0
package/lib/lsp.syn ADDED
@@ -0,0 +1,503 @@
1
+ -- lib/lsp.syn — cliente LSP (Language Server Protocol) por stdio: navegación semántica para el modelo
2
+ --
3
+ -- Por qué: grep encuentra TEXTO; un language server sabe cuál de los cinco `parse` es el que importa este
4
+ -- archivo, quién usa `colors.primary`, dónde se define un tipo, y el índice de símbolos de un archivo sin
5
+ -- leerlo entero. Es el mismo proceso que tu editor ya corre (tsserver, pyright, rust-analyzer…).
6
+ --
7
+ -- Diseño (deepseek-harness packages/lsp: lsp-stdio + tool-lsp): el harness NO instala servers — el usuario
8
+ -- declara uno por lenguaje (o elige un preset) y se lanza LAZY en la primera consulta. Cinco operaciones
9
+ -- cerradas (definition, references, implementation, hover, symbols), sin escape hatch JSON-RPC. Cada consulta
10
+ -- abre el documento de forma transitoria (didOpen → request → didClose): el server ve siempre el disco.
11
+ --
12
+ -- Config (misma idea que mcp.json):
13
+ -- .lampson/lsp.json GLOBAL {"servers": {"typescript": {"command": "npx", "args": ["--yes",
14
+ -- workspace/.lampson/lsp.json PROYECTO "typescript-language-server", "--stdio"], "languages": {".ts": "typescript", …}}}}
15
+ -- PRESETS abajo: /lsp add typescript · python · rust · go · css · html.
16
+ --
17
+ -- Arquitectura = la de lib/mcp.syn: un agente supervisor `LspSup` por server (proc_spawn, line_mode=false
18
+ -- porque LSP enmarca con "Content-Length: N\r\n\r\n" + JSON, no una línea por mensaje), estado en el
19
+ -- blackboard "lsp:<server>", requests por bus "lsp.req.<server>" / "lsp.res.<server>.<id>". Los requests
20
+ -- que el SERVER manda al cliente (workspace/configuration, client/registerCapability, window/*) se
21
+ -- responden con null para que no se trabe; las notificaciones (diagnostics) se ignoran.
22
+
23
+ use "./tools/common.syn" as c
24
+
25
+ export let GLOBAL_CONFIG be ".lampson/lsp.json"
26
+ export let PROJECT_CONFIG be "workspace/.lampson/lsp.json"
27
+ let CALL_TIMEOUT be 60
28
+ let START_WAIT be 25
29
+
30
+ -- presets: nombre → {command, args, languages}. `npx --yes` baja el server si no está (npm cache).
31
+ export let PRESETS be {
32
+ "typescript": {"command": "npx", "args": ["--yes", "-p", "typescript", "-p", "typescript-language-server", "typescript-language-server", "--stdio"], "languages": {".ts": "typescript", ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact", ".mjs": "javascript", ".cjs": "javascript"}, "install": "npm i -g typescript typescript-language-server (or let npx fetch it)"},
33
+ "python": {"command": "npx", "args": ["--yes", "pyright", "--stdio"], "languages": {".py": "python"}, "install": "pip install pyright · or npm i -g pyright"},
34
+ "rust": {"command": "rust-analyzer", "args": [], "languages": {".rs": "rust"}, "install": "rustup component add rust-analyzer"},
35
+ "go": {"command": "gopls", "args": [], "languages": {".go": "go"}, "install": "go install golang.org/x/tools/gopls@latest"},
36
+ "css": {"command": "npx", "args": ["--yes", "-p", "vscode-langservers-extracted", "vscode-css-language-server", "--stdio"], "languages": {".css": "css", ".scss": "scss", ".less": "less"}, "install": "npm i -g vscode-langservers-extracted"},
37
+ "html": {"command": "npx", "args": ["--yes", "-p", "vscode-langservers-extracted", "vscode-html-language-server", "--stdio"], "languages": {".html": "html", ".htm": "html"}, "install": "npm i -g vscode-langservers-extracted"}
38
+ }
39
+
40
+ let SYMBOL_KINDS be {"1": "file", "2": "module", "3": "namespace", "4": "package", "5": "class", "6": "method", "7": "property", "8": "field", "9": "constructor", "10": "enum", "11": "interface", "12": "function", "13": "variable", "14": "constant", "15": "string", "16": "number", "17": "boolean", "18": "array", "19": "object", "20": "key", "21": "null", "22": "enum member", "23": "struct", "24": "event", "25": "operator", "26": "type parameter"}
41
+
42
+ task valid_name(name)
43
+ when name == nothing or name == ""
44
+ give false
45
+ give matches(text(name), "[a-zA-Z0-9_-]{1,32}")
46
+
47
+ task read_config(path, scope)
48
+ let out be []
49
+ try
50
+ let doc be json_decode(read_file(path))
51
+ when contains(doc, "servers")
52
+ each name in keys(doc["servers"])
53
+ let s be doc["servers"][name]
54
+ let disabled be contains(s, "disabled") and s["disabled"] == true
55
+ when valid_name(name) and contains(s, "command") and contains(s, "languages") and not disabled
56
+ set out to append(out, {"name": name, "command": text(s["command"]), "args": when contains(s, "args") then s["args"] otherwise [], "env": when contains(s, "env") then s["env"] otherwise {}, "languages": s["languages"], "cwd": when contains(s, "cwd") then text(s["cwd"]) otherwise "", "scope": scope})
57
+ recover err
58
+ give out
59
+ give out
60
+
61
+ -- servers configurados: proyecto pisa a global por nombre. LAMPSON_LSP_CONFIG = un archivo extra (tests).
62
+ export task servers()
63
+ require file(".lampson")
64
+ require file(".lampson/*")
65
+ require file("workspace")
66
+ require file("workspace/*")
67
+ require env("LAMPSON_*")
68
+ let by_name be {}
69
+ each s in read_config(GLOBAL_CONFIG, "global")
70
+ set by_name[s["name"]] to s
71
+ each s in read_config(PROJECT_CONFIG, "project")
72
+ set by_name[s["name"]] to s
73
+ when env("LAMPSON_LSP_CONFIG", "") != ""
74
+ each s in read_config(env("LAMPSON_LSP_CONFIG", ""), "extra")
75
+ set by_name[s["name"]] to s
76
+ let out be []
77
+ each n in sort_by(keys(by_name), (x) => x)
78
+ set out to append(out, by_name[n])
79
+ give out
80
+
81
+ -- ---------- rutas y URIs ----------
82
+ -- raíz REAL del proyecto (lampson.ps1 exporta LAMPSON_WORKSPACE = destino de la junction); los servers
83
+ -- resuelven realpaths, así que el rootUri y los uri de los documentos tienen que ser los reales.
84
+ export task root_path()
85
+ require env("LAMPSON_*")
86
+ let r be replace_text(env("LAMPSON_WORKSPACE", "workspace"), "\\", "/")
87
+ when c.ends_with(r, "/")
88
+ set r to slice(r, 0, length(r) - 1)
89
+ give r
90
+
91
+ export task to_uri(path)
92
+ let p be replace_text(replace_text(path, "\\", "/"), " ", "%20")
93
+ when matches(p, "[A-Za-z]:/.*")
94
+ give "file:///" + p
95
+ give "file://" + p
96
+
97
+ export task from_uri(uri)
98
+ let u be text(uri)
99
+ when starts_with(u, "file:///")
100
+ set u to slice(u, 8, length(u))
101
+ otherwise when starts_with(u, "file://")
102
+ set u to slice(u, 7, length(u))
103
+ give replace_text(replace_text(replace_text(u, "%3A", ":"), "%3a", ":"), "%20", " ")
104
+
105
+ -- uri del server → ruta relativa al workspace (o absoluta si cae fuera)
106
+ export task relativize(uri, root)
107
+ let p be from_uri(uri)
108
+ let lp be lower(p)
109
+ let lr be lower(root)
110
+ when starts_with(lp, lr + "/")
111
+ give slice(p, length(root) + 1, length(p))
112
+ when lp == lr
113
+ give "."
114
+ give p
115
+
116
+ task ext_of(path)
117
+ let parts be split(replace_text(path, "\\", "/"), "/")
118
+ let base be parts[length(parts) - 1]
119
+ let segs be split(base, ".")
120
+ when length(segs) < 2
121
+ give ""
122
+ give "." + lower(segs[length(segs) - 1])
123
+
124
+ -- server para un archivo (por extensión) o nothing
125
+ export task server_for(path)
126
+ require file(".lampson")
127
+ require file(".lampson/*")
128
+ require file("workspace")
129
+ require file("workspace/*")
130
+ require env("LAMPSON_*")
131
+ let ext be ext_of(path)
132
+ each s in servers()
133
+ when contains(s["languages"], ext)
134
+ give {"server": s, "language": s["languages"][ext]}
135
+ give nothing
136
+
137
+ -- ---------- supervisor: un agente por server ----------
138
+ -- (un agente no ve el módulo: todo va por spawn y los literales viven acá adentro)
139
+ agent LspSup
140
+ require exec
141
+ require time
142
+ require env("LAMPSON_*")
143
+ require env("OS")
144
+ require file("workspace")
145
+ require file("workspace/*")
146
+ let key be "lsp:" + name
147
+ let args be json_decode(args_json)
148
+ let envs be json_decode(env_json)
149
+ share {"status": "starting", "error": nothing, "started": now()} as key
150
+ let p be nothing
151
+ try
152
+ set p to proc_spawn(command, args, {"cwd": cwd, "env": envs, "stderr": "separate", "line_mode": false, "on_full": "drop_oldest"})
153
+ recover err
154
+ -- Windows: `npx`/`npm`/`pyright` son .cmd — proc_spawn no pasa por el shell, así que se reintenta con la extensión
155
+ when env("OS", "") == "Windows_NT" and contains(text(err), "not found") and not contains(lower(command), ".")
156
+ try
157
+ set p to proc_spawn(command + ".cmd", args, {"cwd": cwd, "env": envs, "stderr": "separate", "line_mode": false, "on_full": "drop_oldest"})
158
+ recover err2
159
+ share {"status": "error", "error": "cannot start: " + text(err2), "started": now()} as key
160
+ otherwise
161
+ share {"status": "error", "error": "cannot start: " + text(err), "started": now()} as key
162
+ when p != nothing
163
+ let seq be 0
164
+ let buf be ""
165
+ let inbox be []
166
+ let sub be bus_subscribe(["lsp.req." + name, "lsp.stop." + name, "lsp.stop_all"])
167
+ task send(msg)
168
+ let body be json_encode(msg)
169
+ proc_send(p, "Content-Length: " + text(length(bytes(body))) + "\r\n\r\n" + body)
170
+ -- saca de `buf` todos los mensajes completos → inbox. Content-Length cuenta BYTES; los chunks son
171
+ -- texto (nunca partidos dentro de un carácter), así que recortamos por chars y ajustamos por bytes.
172
+ task drain()
173
+ let more be true
174
+ while more
175
+ let parts be split(buf, "\r\n\r\n")
176
+ when length(parts) < 2
177
+ set more to false
178
+ otherwise
179
+ let header be parts[0]
180
+ let rest be join(slice(parts, 1, length(parts)), "\r\n\r\n")
181
+ let n be -1
182
+ each hl in split(header, "\r\n")
183
+ when starts_with(lower(hl), "content-length:")
184
+ set n to floor(number(trim(slice(hl, 15, length(hl)))))
185
+ when n < 0
186
+ -- basura antes de un header: descartar hasta el separador
187
+ set buf to rest
188
+ otherwise when length(bytes(rest)) < n
189
+ set more to false
190
+ otherwise
191
+ let k be n
192
+ when k > length(rest)
193
+ set k to length(rest)
194
+ while length(bytes(slice(rest, 0, k))) > n
195
+ set k to k - 1
196
+ let body be slice(rest, 0, k)
197
+ set buf to slice(rest, k, length(rest))
198
+ try
199
+ set inbox to append(inbox, json_decode(body))
200
+ recover e2
201
+ set inbox to inbox
202
+ -- requests del SERVER hacia el cliente: contestar null para que siga; notificaciones: ignorar
203
+ task handle_server(m)
204
+ when contains(m, "method") and contains(m, "id")
205
+ let res be nothing
206
+ when m["method"] == "workspace/configuration"
207
+ set res to apply(m["params"]["items"], (it) => nothing)
208
+ send({"jsonrpc": "2.0", "id": m["id"], "result": res})
209
+ -- request síncrona: manda y espera la respuesta con ese id; atiende lo del server mientras tanto
210
+ task rpc(method, params, timeout)
211
+ set seq to seq + 1
212
+ let id be seq
213
+ send({"jsonrpc": "2.0", "id": id, "method": method, "params": params})
214
+ let deadline be now() + timeout
215
+ let answer be nothing
216
+ while answer == nothing and now() < deadline
217
+ -- primero lo que ya está en el inbox
218
+ let keep be []
219
+ each m in inbox
220
+ when answer == nothing and contains(m, "id") and not contains(m, "method") and text(m["id"]) == text(id)
221
+ set answer to when contains(m, "error") then {"error": text(m["error"]["message"])} otherwise {"result": when contains(m, "result") then m["result"] otherwise nothing}
222
+ otherwise
223
+ handle_server(m)
224
+ set inbox to keep
225
+ when answer == nothing
226
+ let ev be proc_recv(p, deadline - now())
227
+ when ev == nothing
228
+ set answer to {"error": "timeout waiting for " + method}
229
+ otherwise when ev["type"] == "exit"
230
+ set answer to {"error": "server exited with code " + text(ev["data"]["exit_code"])}
231
+ otherwise when ev["type"] == "stdout"
232
+ set buf to buf + ev["data"]
233
+ drain()
234
+ give answer
235
+ task notify(method, params)
236
+ send({"jsonrpc": "2.0", "method": method, "params": params})
237
+ let init be rpc("initialize", {"processId": nothing, "rootUri": root_uri, "rootPath": root_path, "workspaceFolders": [{"uri": root_uri, "name": "workspace"}], "clientInfo": {"name": "lampson", "version": "0.1"}, "capabilities": {"textDocument": {"hover": {"contentFormat": ["markdown", "plaintext"]}, "documentSymbol": {"hierarchicalDocumentSymbolSupport": true}, "definition": {"linkSupport": true}, "implementation": {"linkSupport": true}}, "workspace": {"configuration": true}}}, 60)
238
+ when init == nothing or contains(init, "error")
239
+ share {"status": "error", "error": "initialize failed: " + (when init == nothing then "no answer" otherwise init["error"]), "started": now()} as key
240
+ proc_close(p)
241
+ otherwise
242
+ notify("initialized", {})
243
+ share {"status": "ready", "error": nothing, "started": now()} as key
244
+ bus_publish("lsp." + name, {"name": name, "status": "ready"})
245
+ let open be true
246
+ while open
247
+ let ev be select({"p": p, "bus": sub}, 60)
248
+ when ev == nothing
249
+ set open to proc_status(p) == "running"
250
+ otherwise when ev["name"] == "bus"
251
+ when ev["topic"] == "lsp.req." + name
252
+ let r be ev["data"]
253
+ when r["kind"] == "notify"
254
+ notify(r["method"], r["params"])
255
+ otherwise
256
+ let ans be rpc(r["method"], r["params"], 60)
257
+ bus_publish("lsp.res." + name + "." + text(r["id"]), when ans == nothing then {"error": "no answer"} otherwise ans)
258
+ otherwise
259
+ set open to false
260
+ otherwise when ev["type"] == "exit"
261
+ share {"status": "exited", "error": "server exited with code " + text(ev["data"]["exit_code"]), "started": now()} as key
262
+ bus_publish("lsp." + name, {"name": name, "status": "exited"})
263
+ set open to false
264
+ otherwise when ev["type"] == "stdout"
265
+ set buf to buf + ev["data"]
266
+ drain()
267
+ each m in inbox
268
+ handle_server(m)
269
+ set inbox to []
270
+ otherwise when ev["type"] == "stderr"
271
+ bus_publish("lsp." + name, {"name": name, "status": "ready", "line": ev["data"]})
272
+ when open == false and proc_status(p) == "running"
273
+ rpc("shutdown", nothing, 5)
274
+ notify("exit", nothing)
275
+ proc_close(p)
276
+ observe key as st
277
+ when st["status"] == "ready"
278
+ share {"status": "stopped", "error": nothing, "started": now()} as key
279
+ bus_unsubscribe(sub)
280
+
281
+ export task state(name)
282
+ observe "lsp:" + name as st
283
+ give st
284
+
285
+ -- arranque LAZY: en la primera consulta a ese server; espera hasta START_WAIT a que esté listo
286
+ task ensure(s)
287
+ let st be state(s["name"])
288
+ when st == nothing or (st["status"] != "ready" and st["status"] != "starting")
289
+ let root be root_path()
290
+ spawn LspSup with name = s["name"], command = s["command"], args_json = json_encode(s["args"]), env_json = json_encode(s["env"]), cwd = (when s["cwd"] == "" then root otherwise s["cwd"]), root_path = root, root_uri = to_uri(root)
291
+ let waited be 0
292
+ while waited < START_WAIT * 10 and (state(s["name"]) == nothing or state(s["name"])["status"] == "starting")
293
+ sleep(0.1)
294
+ set waited to waited + 1
295
+ let fin be state(s["name"])
296
+ when fin == nothing or fin["status"] != "ready"
297
+ raise("LSP server '" + s["name"] + "' is not ready (" + (when fin == nothing then "no state" otherwise fin["status"] + (when fin["error"] != nothing then ": " + text(fin["error"]) otherwise "")) + "). Command: " + s["command"] + " " + join(s["args"], " "))
298
+ give fin
299
+
300
+ export task stop_all()
301
+ bus_publish("lsp.stop_all", {})
302
+
303
+ task request(server, method, params)
304
+ let id be text(floor(now() * 1000000))
305
+ let sub be bus_subscribe("lsp.res." + server + "." + id)
306
+ bus_publish("lsp.req." + server, {"id": id, "kind": "request", "method": method, "params": params})
307
+ let ev be bus_recv(sub, CALL_TIMEOUT + 5)
308
+ bus_unsubscribe(sub)
309
+ when ev == nothing
310
+ raise("LSP request timed out (" + method + ")")
311
+ let ans be ev["data"]
312
+ when contains(ans, "error")
313
+ raise("LSP error: " + text(ans["error"]))
314
+ give ans["result"]
315
+
316
+ task notify(server, method, params)
317
+ bus_publish("lsp.req." + server, {"id": "0", "kind": "notify", "method": method, "params": params})
318
+
319
+ -- ---------- la consulta ----------
320
+ -- op: definition | references | implementation | hover | symbols. line/character 1-based (como el editor).
321
+ export task query(op, path, line, character)
322
+ require exec
323
+ require time
324
+ require env("LAMPSON_*")
325
+ require env("OS")
326
+ require file(".lampson")
327
+ require file(".lampson/*")
328
+ require file("workspace")
329
+ require file("workspace/*")
330
+ let found be server_for(path)
331
+ when found == nothing
332
+ let have be apply(servers(), (s) => s["name"] + " (" + join(keys(s["languages"]), " ") + ")")
333
+ raise("no LSP server configured for '" + ext_of(path) + "' files. Configured: " + (when length(have) == 0 then "none" otherwise join(have, ", ")) + ". The user can add one with /lsp add <typescript|python|rust|go|css|html> or in " + GLOBAL_CONFIG)
334
+ let s be found["server"]
335
+ ensure(s)
336
+ let real be c.ws(path)
337
+ let content be read_file(real)
338
+ let root be root_path()
339
+ let uri be to_uri(root + "/" + c.unws(real))
340
+ notify(s["name"], "textDocument/didOpen", {"textDocument": {"uri": uri, "languageId": found["language"], "version": 1, "text": content}})
341
+ let out be ""
342
+ try
343
+ when op == "symbols"
344
+ set out to render_symbols(request(s["name"], "textDocument/documentSymbol", {"textDocument": {"uri": uri}}), root)
345
+ otherwise
346
+ when line == nothing or character == nothing
347
+ raise("line and character are required for " + op)
348
+ let pos be {"line": floor(line) - 1, "character": floor(character) - 1}
349
+ when op == "hover"
350
+ set out to render_hover(request(s["name"], "textDocument/hover", {"textDocument": {"uri": uri}, "position": pos}))
351
+ otherwise when op == "references"
352
+ set out to render_locations(request(s["name"], "textDocument/references", {"textDocument": {"uri": uri}, "position": pos, "context": {"includeDeclaration": true}}), root)
353
+ otherwise when op == "implementation"
354
+ set out to render_locations(request(s["name"], "textDocument/implementation", {"textDocument": {"uri": uri}, "position": pos}), root)
355
+ otherwise when op == "definition"
356
+ set out to render_locations(request(s["name"], "textDocument/definition", {"textDocument": {"uri": uri}, "position": pos}), root)
357
+ otherwise
358
+ raise("unknown op '" + text(op) + "' (definition | references | implementation | hover | symbols)")
359
+ recover err
360
+ notify(s["name"], "textDocument/didClose", {"textDocument": {"uri": uri}})
361
+ raise(err)
362
+ notify(s["name"], "textDocument/didClose", {"textDocument": {"uri": uri}})
363
+ give c.truncate(out, c.MAX_OUTPUT)
364
+
365
+ -- ---------- render ----------
366
+ task line_text(rel, line1)
367
+ try
368
+ give trim(read_file(c.ws(rel), line1, 1))
369
+ recover err
370
+ give ""
371
+
372
+ task render_locations(result, root)
373
+ when result == nothing
374
+ give "no results (is the cursor on a symbol? line/character are 1-based)"
375
+ let list be when type_of(result) == "list" then result otherwise [result]
376
+ when length(list) == 0
377
+ give "no results (is the cursor on a symbol? line/character are 1-based)"
378
+ let lines be []
379
+ let n be 0
380
+ each loc in list
381
+ when n < 100
382
+ let uri be when contains(loc, "targetUri") then loc["targetUri"] otherwise loc["uri"]
383
+ let range be when contains(loc, "targetSelectionRange") then loc["targetSelectionRange"] otherwise (when contains(loc, "targetRange") then loc["targetRange"] otherwise loc["range"])
384
+ let rel be relativize(uri, root)
385
+ let l1 be floor(range["start"]["line"]) + 1
386
+ let c1 be floor(range["start"]["character"]) + 1
387
+ set lines to append(lines, rel + ":" + text(l1) + ":" + text(c1) + " " + line_text(rel, l1))
388
+ set n to n + 1
389
+ when n > 100
390
+ set lines to append(lines, "[" + text(n - 100) + " more omitted]")
391
+ give join(lines, "\n")
392
+
393
+ task render_hover(result)
394
+ when result == nothing or not contains(result, "contents")
395
+ give "no hover info at that position"
396
+ let cts be result["contents"]
397
+ when type_of(cts) == "list"
398
+ give join(apply(cts, (x) => when type_of(x) == "text" then x otherwise text(x["value"])), "\n")
399
+ when type_of(cts) == "text"
400
+ give cts
401
+ give text(cts["value"])
402
+
403
+ task render_symbols(result, root)
404
+ when result == nothing or length(result) == 0
405
+ give "no symbols"
406
+ let lines be []
407
+ -- DocumentSymbol (jerárquico) o SymbolInformation (plano con location)
408
+ when contains(result[0], "location")
409
+ each si in result
410
+ let l1 be floor(si["location"]["range"]["start"]["line"]) + 1
411
+ set lines to append(lines, text(l1) + " " + kind_name(si["kind"]) + " " + si["name"] + (when contains(si, "containerName") and si["containerName"] != nothing then " (in " + text(si["containerName"]) + ")" otherwise ""))
412
+ give join(lines, "\n")
413
+ each ds in result
414
+ set lines to lines + symbol_lines(ds, "")
415
+ give join(lines, "\n")
416
+
417
+ task symbol_lines(ds, indent)
418
+ let l1 be floor(ds["range"]["start"]["line"]) + 1
419
+ let l2 be floor(ds["range"]["end"]["line"]) + 1
420
+ let out be [text(l1) + (when l2 != l1 then "-" + text(l2) otherwise "") + " " + indent + kind_name(ds["kind"]) + " " + ds["name"] + (when contains(ds, "detail") and ds["detail"] != nothing and ds["detail"] != "" then " " + text(ds["detail"]) otherwise "")]
421
+ when contains(ds, "children") and ds["children"] != nothing
422
+ each ch in ds["children"]
423
+ set out to out + symbol_lines(ch, indent + " ")
424
+ give out
425
+
426
+ task kind_name(k)
427
+ let ks be text(floor(k))
428
+ give when contains(SYMBOL_KINDS, ks) then SYMBOL_KINDS[ks] otherwise "symbol"
429
+
430
+ -- ---------- config: agregar / quitar (usuario: /lsp, web) ----------
431
+ task config_path(scope)
432
+ give when scope == "project" then PROJECT_CONFIG otherwise GLOBAL_CONFIG
433
+
434
+ -- add_server("typescript", nothing, nothing, "global") usa el preset; add_server("mylang", "cmd args…",
435
+ -- {".x": "xlang"}, scope) uno propio
436
+ export task add_server(name, command_line, languages, scope)
437
+ require file(".lampson")
438
+ require file(".lampson/*")
439
+ require file("workspace")
440
+ require file("workspace/*")
441
+ require env("LAMPSON_*")
442
+ when not valid_name(name)
443
+ raise("invalid server name (letters, digits, - or _)")
444
+ let entry be nothing
445
+ when (command_line == nothing or trim(text(command_line)) == "") and contains(PRESETS, name)
446
+ let pr be PRESETS[name]
447
+ set entry to {"command": pr["command"], "args": pr["args"], "languages": pr["languages"]}
448
+ otherwise
449
+ when command_line == nothing or trim(text(command_line)) == ""
450
+ raise("no preset named '" + name + "' (presets: " + join(keys(PRESETS), ", ") + "); give a command line")
451
+ when languages == nothing or length(keys(languages)) == 0
452
+ raise("languages required for a custom server: {\".ext\": \"languageId\"}")
453
+ let toks be where(split(trim(text(command_line)), " "), (x) => x != "")
454
+ set entry to {"command": toks[0], "args": slice(toks, 1, length(toks)), "languages": languages}
455
+ let path be config_path(scope)
456
+ let doc be {"servers": {}}
457
+ try
458
+ set doc to json_decode(read_file(path))
459
+ recover err
460
+ set doc to {"servers": {}}
461
+ when not contains(doc, "servers")
462
+ set doc["servers"] to {}
463
+ set doc["servers"][name] to entry
464
+ write_file(path, json_encode(doc))
465
+ give "LSP server '" + name + "' configured (" + (when scope == "project" then "project" otherwise "global") + "): " + entry["command"] + " " + join(entry["args"], " ") + " for " + join(keys(entry["languages"]), " ") + " — starts on the first query" + (when contains(PRESETS, name) then ". If it is not installed: " + PRESETS[name]["install"] otherwise "")
466
+
467
+ export task remove_server(name)
468
+ require file(".lampson")
469
+ require file(".lampson/*")
470
+ require file("workspace")
471
+ require file("workspace/*")
472
+ require env("LAMPSON_*")
473
+ let removed be false
474
+ each path in [GLOBAL_CONFIG, PROJECT_CONFIG]
475
+ try
476
+ let doc be json_decode(read_file(path))
477
+ when contains(doc, "servers") and contains(doc["servers"], name)
478
+ let srv be {}
479
+ each k in keys(doc["servers"])
480
+ when k != name
481
+ set srv[k] to doc["servers"][k]
482
+ set doc["servers"] to srv
483
+ write_file(path, json_encode(doc))
484
+ set removed to true
485
+ recover err
486
+ set removed to removed
487
+ when not removed
488
+ raise("no LSP server named '" + name + "' in the config")
489
+ bus_publish("lsp.stop." + name, {})
490
+ give "LSP server '" + name + "' removed"
491
+
492
+ -- resumen para UI/terminal
493
+ export task summary()
494
+ require file(".lampson")
495
+ require file(".lampson/*")
496
+ require file("workspace")
497
+ require file("workspace/*")
498
+ require env("LAMPSON_*")
499
+ let out be []
500
+ each s in servers()
501
+ let st be state(s["name"])
502
+ set out to append(out, {"name": s["name"], "scope": s["scope"], "command": s["command"] + " " + join(s["args"], " "), "extensions": keys(s["languages"]), "status": when st == nothing then "idle" otherwise st["status"], "error": when st == nothing then nothing otherwise st["error"]})
503
+ give out