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.
- package/.env.example +29 -0
- package/LICENSE +21 -0
- package/README.md +382 -0
- package/bin/lampson.js +81 -0
- package/chat.syn +799 -0
- package/lamps/example-hello/lamp.json +16 -0
- package/lamps/example-hello/lamp.syn +19 -0
- package/lampson.cmd +4 -0
- package/lampson.ps1 +88 -0
- package/lampson.sh +42 -0
- package/lib/agents.syn +471 -0
- package/lib/git.syn +58 -0
- package/lib/lamps.syn +386 -0
- package/lib/loop.syn +455 -0
- package/lib/lsp.syn +503 -0
- package/lib/mcp.syn +403 -0
- package/lib/permission.syn +154 -0
- package/lib/prompt.syn +75 -0
- package/lib/provider.syn +522 -0
- package/lib/session.syn +111 -0
- package/lib/settings.syn +70 -0
- package/lib/skills.syn +179 -0
- package/lib/tools/bash.syn +105 -0
- package/lib/tools/common.sh +49 -0
- package/lib/tools/common.syn +91 -0
- package/lib/tools/edit.syn +32 -0
- package/lib/tools/find.syn +35 -0
- package/lib/tools/grep.syn +31 -0
- package/lib/tools/img.ps1 +36 -0
- package/lib/tools/img.sh +22 -0
- package/lib/tools/ls.syn +18 -0
- package/lib/tools/memo.syn +148 -0
- package/lib/tools/proc.sh +42 -0
- package/lib/tools/proc.syn +314 -0
- package/lib/tools/process.syn +46 -0
- package/lib/tools/read.syn +25 -0
- package/lib/tools/skill.syn +14 -0
- package/lib/tools/todo.syn +97 -0
- package/lib/tools/write.syn +22 -0
- package/lib/tools.syn +198 -0
- package/lib/trace.syn +116 -0
- package/lib/tree.syn +59 -0
- package/lib/update.syn +57 -0
- package/package.json +40 -0
- package/public/fonts/plex-mono-400-latin-ext.woff2 +0 -0
- package/public/fonts/plex-mono-400-latin.woff2 +0 -0
- package/public/fonts/plex-mono-600-latin-ext.woff2 +0 -0
- package/public/fonts/plex-mono-600-latin.woff2 +0 -0
- package/public/fonts/plex-serif-400-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-400-latin.woff2 +0 -0
- package/public/fonts/plex-serif-400i-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-400i-latin.woff2 +0 -0
- package/public/fonts/plex-serif-600-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-600-latin.woff2 +0 -0
- package/public/index.html +1268 -0
- package/public/vendor/xterm-addon-fit.js +2 -0
- package/public/vendor/xterm.css +218 -0
- package/public/vendor/xterm.js +2 -0
- package/skills/debugging/SKILL.md +33 -0
- package/skills/lampson/SKILL.md +117 -0
- package/skills/synsema/SKILL.md +75 -0
- 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
|