pi-x-ide 1.4.0 → 1.4.2
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/README.md +183 -114
- package/README.zh.md +183 -114
- package/dist/package.json +10 -4
- package/dist/src/nvim/sidecar-schema.d.ts +25 -0
- package/dist/src/nvim/sidecar-schema.js +64 -0
- package/dist/src/nvim/sidecar-schema.js.map +1 -0
- package/dist/src/nvim/sidecar.d.ts +16 -0
- package/dist/src/nvim/sidecar.js +173 -0
- package/dist/src/nvim/sidecar.js.map +1 -0
- package/dist/src/pi/index.js +10 -2
- package/dist/src/pi/index.js.map +1 -1
- package/dist/src/pi/install.js +22 -1
- package/dist/src/pi/install.js.map +1 -1
- package/dist/src/shared/ide-server.d.ts +20 -0
- package/dist/src/shared/ide-server.js +144 -0
- package/dist/src/shared/ide-server.js.map +1 -0
- package/dist/src/shared/lock-file.d.ts +16 -0
- package/dist/src/shared/lock-file.js +58 -0
- package/dist/src/shared/lock-file.js.map +1 -0
- package/dist/src/shared/paths.js +8 -1
- package/dist/src/shared/paths.js.map +1 -1
- package/dist/src/shared/protocol.d.ts +1 -1
- package/dist/src/shared/schema.js +1 -1
- package/dist/src/shared/schema.js.map +1 -1
- package/dist/test/nvim-sidecar.test.d.ts +1 -0
- package/dist/test/nvim-sidecar.test.js +148 -0
- package/dist/test/nvim-sidecar.test.js.map +1 -0
- package/dist/test/shared.test.js +10 -0
- package/dist/test/shared.test.js.map +1 -1
- package/nvim/bin/pi-x-ide-nvim-sidecar.cjs +21 -0
- package/nvim/doc/pi-x-ide.txt +112 -0
- package/nvim/lua/pi_x_ide/init.lua +442 -0
- package/nvim/plugin/pi-x-ide.lua +15 -0
- package/package.json +10 -4
- package/src/nvim/sidecar-schema.ts +71 -0
- package/src/nvim/sidecar.ts +219 -0
- package/src/pi/index.ts +12 -2
- package/src/pi/install.ts +24 -1
- package/src/shared/ide-server.ts +120 -0
- package/src/shared/lock-file.ts +65 -0
- package/src/shared/paths.ts +8 -1
- package/src/shared/protocol.ts +1 -1
- package/src/shared/schema.ts +1 -1
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
local M = {}
|
|
2
|
+
|
|
3
|
+
local state = {
|
|
4
|
+
config = nil,
|
|
5
|
+
job_id = nil,
|
|
6
|
+
timer = nil,
|
|
7
|
+
augroup = nil,
|
|
8
|
+
latest_snapshot = nil,
|
|
9
|
+
visual_mark_valid = false,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
local defaults = {
|
|
13
|
+
enabled = true,
|
|
14
|
+
sidecar_cmd = nil,
|
|
15
|
+
debounce_ms = 150,
|
|
16
|
+
range_format = "comma",
|
|
17
|
+
keymap = nil,
|
|
18
|
+
workspace_folders = nil,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
local function notify(message, level)
|
|
22
|
+
vim.notify("Pi x IDE: " .. message, level or vim.log.levels.INFO)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
local function plugin_root()
|
|
26
|
+
local source = debug.getinfo(1, "S").source
|
|
27
|
+
if source:sub(1, 1) == "@" then
|
|
28
|
+
source = source:sub(2)
|
|
29
|
+
end
|
|
30
|
+
return vim.fn.fnamemodify(source, ":p:h:h:h")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
local function default_sidecar_cmd()
|
|
34
|
+
return { "node", plugin_root() .. "/bin/pi-x-ide-nvim-sidecar.cjs" }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
local function encode(value)
|
|
38
|
+
return vim.json and vim.json.encode(value) or vim.fn.json_encode(value)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
local function send(value)
|
|
42
|
+
if not state.job_id then
|
|
43
|
+
return false
|
|
44
|
+
end
|
|
45
|
+
local ok = vim.fn.chansend(state.job_id, encode(value) .. "\n")
|
|
46
|
+
return ok == 1
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
local function normalize_config(opts)
|
|
50
|
+
local config = vim.tbl_deep_extend("force", defaults, opts or {})
|
|
51
|
+
if config.range_format ~= "dash" then
|
|
52
|
+
config.range_format = "comma"
|
|
53
|
+
end
|
|
54
|
+
return config
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
local function workspace_folders()
|
|
58
|
+
local configured = state.config and state.config.workspace_folders
|
|
59
|
+
if type(configured) == "table" and #configured > 0 then
|
|
60
|
+
return configured
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
local folders = { vim.fn.getcwd() }
|
|
64
|
+
for _, client in pairs(vim.lsp.get_clients and vim.lsp.get_clients() or {}) do
|
|
65
|
+
for _, folder in ipairs(client.workspace_folders or {}) do
|
|
66
|
+
if folder.name and folder.name ~= "" then
|
|
67
|
+
table.insert(folders, folder.name)
|
|
68
|
+
elseif folder.uri then
|
|
69
|
+
local path = vim.uri_to_fname(folder.uri)
|
|
70
|
+
if path and path ~= "" then
|
|
71
|
+
table.insert(folders, path)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
local seen = {}
|
|
78
|
+
local result = {}
|
|
79
|
+
for _, folder in ipairs(folders) do
|
|
80
|
+
local normalized = vim.fn.fnamemodify(folder, ":p"):gsub("/$", "")
|
|
81
|
+
if normalized ~= "" and not seen[normalized] then
|
|
82
|
+
seen[normalized] = true
|
|
83
|
+
table.insert(result, normalized)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
return result
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
local function is_path_inside(parent, child)
|
|
90
|
+
parent = vim.fn.fnamemodify(parent, ":p"):gsub("/$", "")
|
|
91
|
+
child = vim.fn.fnamemodify(child, ":p")
|
|
92
|
+
if child:gsub("/$", "") == parent then
|
|
93
|
+
return true
|
|
94
|
+
end
|
|
95
|
+
return child:sub(1, #parent + 1) == parent .. "/"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
local function best_workspace(file_path)
|
|
99
|
+
local best = nil
|
|
100
|
+
for _, folder in ipairs(workspace_folders()) do
|
|
101
|
+
if is_path_inside(folder, file_path) and (not best or #folder > #best) then
|
|
102
|
+
best = folder
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
return best or workspace_folders()[1]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
local function byte_to_utf16(line, byte_col)
|
|
109
|
+
return vim.str_utfindex(line or "", "utf-16", math.max(0, byte_col), false)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
local function byte_after_char(line, byte_col)
|
|
113
|
+
line = line or ""
|
|
114
|
+
if byte_col >= #line then
|
|
115
|
+
return #line
|
|
116
|
+
end
|
|
117
|
+
local utf16 = byte_to_utf16(line, byte_col)
|
|
118
|
+
return math.min(#line, vim.str_byteindex(line, "utf-16", utf16 + 1, false))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
local function compare_positions(a, b)
|
|
122
|
+
if a.line ~= b.line then
|
|
123
|
+
return a.line < b.line
|
|
124
|
+
end
|
|
125
|
+
return a.col <= b.col
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
local function sorted_positions(a, b)
|
|
129
|
+
if compare_positions(a, b) then
|
|
130
|
+
return a, b
|
|
131
|
+
end
|
|
132
|
+
return b, a
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
local function get_lines(buf, start_row, end_row)
|
|
136
|
+
return vim.api.nvim_buf_get_lines(buf, start_row, end_row + 1, false)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
local function text_from_range(buf, start_row, start_col, end_row, end_col)
|
|
140
|
+
return table.concat(vim.api.nvim_buf_get_text(buf, start_row, start_col, end_row, end_col, {}), "\n")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
local function range_from_bytes(buf, start_row, start_col, end_row, end_col)
|
|
144
|
+
local lines = get_lines(buf, start_row, end_row)
|
|
145
|
+
local start_line = lines[1] or ""
|
|
146
|
+
local end_line = lines[#lines] or ""
|
|
147
|
+
return {
|
|
148
|
+
text = text_from_range(buf, start_row, start_col, end_row, end_col),
|
|
149
|
+
selection = {
|
|
150
|
+
start = { line = start_row, character = byte_to_utf16(start_line, start_col) },
|
|
151
|
+
["end"] = { line = end_row, character = byte_to_utf16(end_line, end_col) },
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
local function pos_from_getpos(pos)
|
|
157
|
+
return { line = math.max(0, pos[2] - 1), col = math.max(0, pos[3] - 1), bufnr = pos[1] }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
local function active_visual_range(buf, mode)
|
|
161
|
+
local start_pos = pos_from_getpos(vim.fn.getpos("v"))
|
|
162
|
+
local end_pos = pos_from_getpos(vim.fn.getpos("."))
|
|
163
|
+
if start_pos.bufnr ~= 0 and start_pos.bufnr ~= buf then
|
|
164
|
+
return {}
|
|
165
|
+
end
|
|
166
|
+
if end_pos.bufnr ~= 0 and end_pos.bufnr ~= buf then
|
|
167
|
+
return {}
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
start_pos, end_pos = sorted_positions(start_pos, end_pos)
|
|
171
|
+
|
|
172
|
+
if mode == "V" then
|
|
173
|
+
local lines = get_lines(buf, start_pos.line, end_pos.line)
|
|
174
|
+
return { range_from_bytes(buf, start_pos.line, 0, end_pos.line, #(lines[#lines] or "")) }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
if mode == "\022" then
|
|
178
|
+
local start_col = math.min(start_pos.col, end_pos.col)
|
|
179
|
+
local end_col = math.max(start_pos.col, end_pos.col)
|
|
180
|
+
local ranges = {}
|
|
181
|
+
local lines = get_lines(buf, start_pos.line, end_pos.line)
|
|
182
|
+
for offset, line in ipairs(lines) do
|
|
183
|
+
local row = start_pos.line + offset - 1
|
|
184
|
+
local col_start = math.min(start_col, #line)
|
|
185
|
+
local col_end = byte_after_char(line, math.min(end_col, #line))
|
|
186
|
+
if col_start < col_end then
|
|
187
|
+
table.insert(ranges, range_from_bytes(buf, row, col_start, row, col_end))
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
return ranges
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
local end_line = get_lines(buf, end_pos.line, end_pos.line)[1] or ""
|
|
194
|
+
return { range_from_bytes(buf, start_pos.line, start_pos.col, end_pos.line, byte_after_char(end_line, end_pos.col)) }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
local function marked_visual_range(buf)
|
|
198
|
+
if not state.visual_mark_valid then
|
|
199
|
+
return {}
|
|
200
|
+
end
|
|
201
|
+
local start_mark = vim.api.nvim_buf_get_mark(buf, "<")
|
|
202
|
+
local end_mark = vim.api.nvim_buf_get_mark(buf, ">")
|
|
203
|
+
if start_mark[1] == 0 or end_mark[1] == 0 then
|
|
204
|
+
return {}
|
|
205
|
+
end
|
|
206
|
+
local start_pos = { line = start_mark[1] - 1, col = start_mark[2], bufnr = buf }
|
|
207
|
+
local end_pos = { line = end_mark[1] - 1, col = end_mark[2], bufnr = buf }
|
|
208
|
+
start_pos, end_pos = sorted_positions(start_pos, end_pos)
|
|
209
|
+
local end_line = get_lines(buf, end_pos.line, end_pos.line)[1] or ""
|
|
210
|
+
return { range_from_bytes(buf, start_pos.line, start_pos.col, end_pos.line, byte_after_char(end_line, end_pos.col)) }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
function M.snapshot(opts)
|
|
214
|
+
opts = opts or {}
|
|
215
|
+
local buf = vim.api.nvim_get_current_buf()
|
|
216
|
+
if vim.bo[buf].buftype ~= "" then
|
|
217
|
+
return state.latest_snapshot
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
local file_path = vim.api.nvim_buf_get_name(buf)
|
|
221
|
+
if file_path == "" then
|
|
222
|
+
return nil
|
|
223
|
+
end
|
|
224
|
+
file_path = vim.fn.fnamemodify(file_path, ":p")
|
|
225
|
+
|
|
226
|
+
local mode = vim.fn.mode()
|
|
227
|
+
local ranges = {}
|
|
228
|
+
if mode == "v" or mode == "V" or mode == "\022" then
|
|
229
|
+
ranges = active_visual_range(buf, mode)
|
|
230
|
+
elseif opts.prefer_marks then
|
|
231
|
+
ranges = marked_visual_range(buf)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
local snapshot = {
|
|
235
|
+
source = "nvim",
|
|
236
|
+
filePath = file_path,
|
|
237
|
+
workspaceFolder = best_workspace(file_path),
|
|
238
|
+
ranges = ranges,
|
|
239
|
+
}
|
|
240
|
+
state.latest_snapshot = snapshot
|
|
241
|
+
return snapshot
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
local function relative_path(snapshot)
|
|
245
|
+
local base = snapshot.workspaceFolder or vim.fn.getcwd()
|
|
246
|
+
local rel = vim.fn.fnamemodify(snapshot.filePath, ":p")
|
|
247
|
+
if vim.fs and vim.fs.relpath then
|
|
248
|
+
local ok, value = pcall(vim.fs.relpath, base, snapshot.filePath)
|
|
249
|
+
if ok and value and value ~= "" then
|
|
250
|
+
return value
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
local prefix = vim.fn.fnamemodify(base, ":p"):gsub("/$", "") .. "/"
|
|
254
|
+
if rel:sub(1, #prefix) == prefix then
|
|
255
|
+
return rel:sub(#prefix + 1)
|
|
256
|
+
end
|
|
257
|
+
return snapshot.filePath
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
local function line_span(range)
|
|
261
|
+
local start_line = range.selection.start.line + 1
|
|
262
|
+
local end_line = range.selection["end"].line + 1
|
|
263
|
+
if start_line == end_line then
|
|
264
|
+
return "#L" .. start_line
|
|
265
|
+
end
|
|
266
|
+
if state.config and state.config.range_format == "dash" then
|
|
267
|
+
return "#L" .. start_line .. "-L" .. end_line
|
|
268
|
+
end
|
|
269
|
+
return "#L" .. start_line .. "," .. end_line
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
function M.format_range_mention(snapshot)
|
|
273
|
+
local rel = relative_path(snapshot)
|
|
274
|
+
if not snapshot.ranges or #snapshot.ranges == 0 then
|
|
275
|
+
return "@" .. rel
|
|
276
|
+
end
|
|
277
|
+
return "@" .. rel .. line_span(snapshot.ranges[1])
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
local function publish_snapshot()
|
|
281
|
+
if not state.job_id then
|
|
282
|
+
return
|
|
283
|
+
end
|
|
284
|
+
local snapshot = M.snapshot()
|
|
285
|
+
if not snapshot then
|
|
286
|
+
send({ type = "selection_cleared", reason = "no-active-editor" })
|
|
287
|
+
return
|
|
288
|
+
end
|
|
289
|
+
send({ type = "selection_changed", snapshot = snapshot })
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
local function schedule_publish(delay)
|
|
293
|
+
delay = delay or (state.config and state.config.debounce_ms or defaults.debounce_ms)
|
|
294
|
+
if state.timer then
|
|
295
|
+
pcall(state.timer.stop, state.timer)
|
|
296
|
+
pcall(state.timer.close, state.timer)
|
|
297
|
+
end
|
|
298
|
+
state.timer = vim.defer_fn(publish_snapshot, delay)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
function M.start()
|
|
302
|
+
if state.job_id then
|
|
303
|
+
return true
|
|
304
|
+
end
|
|
305
|
+
state.config = state.config or normalize_config()
|
|
306
|
+
local cmd = state.config.sidecar_cmd or default_sidecar_cmd()
|
|
307
|
+
state.job_id = vim.fn.jobstart(cmd, {
|
|
308
|
+
stdin = "pipe",
|
|
309
|
+
detach = false,
|
|
310
|
+
on_exit = function(_, code)
|
|
311
|
+
state.job_id = nil
|
|
312
|
+
if code ~= 0 then
|
|
313
|
+
notify("sidecar exited with code " .. tostring(code), vim.log.levels.WARN)
|
|
314
|
+
end
|
|
315
|
+
end,
|
|
316
|
+
on_stderr = function(_, data)
|
|
317
|
+
for _, line in ipairs(data or {}) do
|
|
318
|
+
if line ~= "" then
|
|
319
|
+
vim.schedule(function()
|
|
320
|
+
notify(line, vim.log.levels.WARN)
|
|
321
|
+
end)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end,
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
if state.job_id <= 0 then
|
|
328
|
+
state.job_id = nil
|
|
329
|
+
notify("failed to start sidecar", vim.log.levels.ERROR)
|
|
330
|
+
return false
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
send({ workspaceFolders = workspace_folders(), name = "Neovim" })
|
|
334
|
+
schedule_publish(0)
|
|
335
|
+
return true
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
function M.stop()
|
|
339
|
+
if state.timer then
|
|
340
|
+
pcall(state.timer.stop, state.timer)
|
|
341
|
+
pcall(state.timer.close, state.timer)
|
|
342
|
+
state.timer = nil
|
|
343
|
+
end
|
|
344
|
+
if state.job_id then
|
|
345
|
+
send({ type = "shutdown" })
|
|
346
|
+
vim.fn.chanclose(state.job_id, "stdin")
|
|
347
|
+
state.job_id = nil
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
function M.attach()
|
|
352
|
+
if not state.job_id and not M.start() then
|
|
353
|
+
return
|
|
354
|
+
end
|
|
355
|
+
local snapshot = M.snapshot({ prefer_marks = true })
|
|
356
|
+
if not snapshot then
|
|
357
|
+
notify("no active file to attach", vim.log.levels.WARN)
|
|
358
|
+
return
|
|
359
|
+
end
|
|
360
|
+
local range_text = M.format_range_mention(snapshot)
|
|
361
|
+
send({ type = "at_mentioned", snapshot = vim.tbl_extend("force", snapshot, { rangeText = range_text }), rangeText = range_text })
|
|
362
|
+
notify("attached " .. range_text)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
function M.status()
|
|
366
|
+
notify(state.job_id and "sidecar running" or "sidecar stopped")
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
local function setup_autocmds()
|
|
370
|
+
if state.augroup then
|
|
371
|
+
pcall(vim.api.nvim_del_augroup_by_id, state.augroup)
|
|
372
|
+
end
|
|
373
|
+
state.augroup = vim.api.nvim_create_augroup("PiXIde", { clear = true })
|
|
374
|
+
vim.api.nvim_create_autocmd({ "BufEnter", "WinEnter", "CursorMoved", "CursorMovedI", "TextChanged", "TextChangedI", "DirChanged" }, {
|
|
375
|
+
group = state.augroup,
|
|
376
|
+
callback = function(event)
|
|
377
|
+
if event.event == "DirChanged" then
|
|
378
|
+
send({ type = "workspace_changed", workspaceFolders = workspace_folders() })
|
|
379
|
+
end
|
|
380
|
+
schedule_publish()
|
|
381
|
+
end,
|
|
382
|
+
})
|
|
383
|
+
vim.api.nvim_create_autocmd("ModeChanged", {
|
|
384
|
+
group = state.augroup,
|
|
385
|
+
callback = function(event)
|
|
386
|
+
if event.match and event.match:match("^[vV\022].*:n$") then
|
|
387
|
+
state.visual_mark_valid = true
|
|
388
|
+
end
|
|
389
|
+
schedule_publish(0)
|
|
390
|
+
end,
|
|
391
|
+
})
|
|
392
|
+
vim.api.nvim_create_autocmd("VimLeavePre", {
|
|
393
|
+
group = state.augroup,
|
|
394
|
+
callback = function()
|
|
395
|
+
M.stop()
|
|
396
|
+
end,
|
|
397
|
+
})
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
local function setup_commands()
|
|
401
|
+
local commands = {
|
|
402
|
+
PiXIdeStart = M.start,
|
|
403
|
+
PiXIdeStop = M.stop,
|
|
404
|
+
PiXIdeStatus = M.status,
|
|
405
|
+
PiXIdeAttach = M.attach,
|
|
406
|
+
}
|
|
407
|
+
for name, fn in pairs(commands) do
|
|
408
|
+
pcall(vim.api.nvim_create_user_command, name, fn, {})
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
local function setup_keymap()
|
|
413
|
+
if not state.config.keymap then
|
|
414
|
+
return
|
|
415
|
+
end
|
|
416
|
+
vim.keymap.set({ "n", "v" }, state.config.keymap, M.attach, { desc = "Pi x IDE: attach selection" })
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
function M.setup(opts)
|
|
420
|
+
state.config = normalize_config(opts)
|
|
421
|
+
setup_commands()
|
|
422
|
+
setup_autocmds()
|
|
423
|
+
setup_keymap()
|
|
424
|
+
if state.config.enabled then
|
|
425
|
+
M.start()
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
function M._test_set_visual_mark_valid(value)
|
|
430
|
+
state.visual_mark_valid = value
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
function M._test_reset()
|
|
434
|
+
M.stop()
|
|
435
|
+
state.config = normalize_config({ enabled = false })
|
|
436
|
+
state.latest_snapshot = nil
|
|
437
|
+
state.visual_mark_valid = false
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
setup_commands()
|
|
441
|
+
|
|
442
|
+
return M
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
if vim.g.loaded_pi_x_ide == 1 then
|
|
2
|
+
return
|
|
3
|
+
end
|
|
4
|
+
vim.g.loaded_pi_x_ide = 1
|
|
5
|
+
|
|
6
|
+
local function command(name, fn)
|
|
7
|
+
pcall(vim.api.nvim_create_user_command, name, function()
|
|
8
|
+
require("pi_x_ide")[fn]()
|
|
9
|
+
end, {})
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
command("PiXIdeStart", "start")
|
|
13
|
+
command("PiXIdeStop", "stop")
|
|
14
|
+
command("PiXIdeStatus", "status")
|
|
15
|
+
command("PiXIdeAttach", "attach")
|
package/package.json
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-x-ide",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "Pi extension package for IDE selection context integration.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
7
7
|
"schemas",
|
|
8
|
-
"src"
|
|
8
|
+
"src",
|
|
9
|
+
"nvim/lua",
|
|
10
|
+
"nvim/plugin",
|
|
11
|
+
"nvim/doc",
|
|
12
|
+
"nvim/bin"
|
|
9
13
|
],
|
|
10
14
|
"keywords": [
|
|
11
15
|
"pi-package",
|
|
12
16
|
"pi",
|
|
13
17
|
"ide",
|
|
14
18
|
"vscode",
|
|
15
|
-
"zed"
|
|
19
|
+
"zed",
|
|
20
|
+
"neovim",
|
|
21
|
+
"nvim"
|
|
16
22
|
],
|
|
17
23
|
"license": "Apache-2.0",
|
|
18
24
|
"repository": {
|
|
@@ -54,7 +60,7 @@
|
|
|
54
60
|
]
|
|
55
61
|
},
|
|
56
62
|
"scripts": {
|
|
57
|
-
"build": "tsc -p tsconfig.json && pnpm --filter './vscode' compile",
|
|
63
|
+
"build": "tsc -p tsconfig.json && node scripts/build-nvim-sidecar.mjs --production && pnpm --filter './vscode' compile",
|
|
58
64
|
"typecheck": "tsc -p tsconfig.json --noEmit && pnpm --filter './vscode' typecheck",
|
|
59
65
|
"generate:config-schema": "node scripts/generate-config-schema.cjs",
|
|
60
66
|
"check:config-schema": "node scripts/generate-config-schema.cjs --check",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { AtMentionedParams, EditorSelectionSnapshot, SelectionClearedParams } from "../shared/protocol";
|
|
2
|
+
import { isAtMentionedParams, isEditorSelectionSnapshot, isSelectionClearedParams } from "../shared/schema";
|
|
3
|
+
|
|
4
|
+
export interface SidecarConfig {
|
|
5
|
+
workspaceFolders?: string[];
|
|
6
|
+
name?: string;
|
|
7
|
+
lockDir?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type NvimSidecarMessage =
|
|
11
|
+
| { type: "selection_changed"; snapshot: EditorSelectionSnapshot }
|
|
12
|
+
| { type: "selection_cleared"; reason?: SelectionClearedParams["reason"] }
|
|
13
|
+
| { type: "at_mentioned"; snapshot: AtMentionedParams; rangeText?: string }
|
|
14
|
+
| { type: "workspace_changed"; workspaceFolders: string[] }
|
|
15
|
+
| { type: "shutdown" };
|
|
16
|
+
|
|
17
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
18
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isStringArray(value: unknown): value is string[] {
|
|
22
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseSidecarConfig(value: unknown): SidecarConfig | undefined {
|
|
26
|
+
if (!isRecord(value)) return undefined;
|
|
27
|
+
const workspaceFolders = value.workspaceFolders;
|
|
28
|
+
const name = value.name;
|
|
29
|
+
const lockDir = value.lockDir;
|
|
30
|
+
if (workspaceFolders !== undefined && !isStringArray(workspaceFolders)) return undefined;
|
|
31
|
+
if (name !== undefined && typeof name !== "string") return undefined;
|
|
32
|
+
if (lockDir !== undefined && typeof lockDir !== "string") return undefined;
|
|
33
|
+
return { workspaceFolders, name, lockDir };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseNvimSidecarMessage(value: unknown): NvimSidecarMessage | undefined {
|
|
37
|
+
if (!isRecord(value) || typeof value.type !== "string") return undefined;
|
|
38
|
+
|
|
39
|
+
switch (value.type) {
|
|
40
|
+
case "selection_changed":
|
|
41
|
+
return isEditorSelectionSnapshot(value.snapshot)
|
|
42
|
+
? { type: "selection_changed", snapshot: value.snapshot }
|
|
43
|
+
: undefined;
|
|
44
|
+
case "selection_cleared": {
|
|
45
|
+
const params = { source: "nvim", reason: value.reason ?? "no-active-editor" };
|
|
46
|
+
return isSelectionClearedParams(params) ? { type: "selection_cleared", reason: params.reason } : undefined;
|
|
47
|
+
}
|
|
48
|
+
case "at_mentioned": {
|
|
49
|
+
const snapshot = value.snapshot;
|
|
50
|
+
if (!isAtMentionedParams(snapshot)) return undefined;
|
|
51
|
+
const rangeText = typeof value.rangeText === "string" ? value.rangeText : snapshot.rangeText;
|
|
52
|
+
return { type: "at_mentioned", snapshot, rangeText };
|
|
53
|
+
}
|
|
54
|
+
case "workspace_changed":
|
|
55
|
+
return isStringArray(value.workspaceFolders)
|
|
56
|
+
? { type: "workspace_changed", workspaceFolders: value.workspaceFolders }
|
|
57
|
+
: undefined;
|
|
58
|
+
case "shutdown":
|
|
59
|
+
return { type: "shutdown" };
|
|
60
|
+
default:
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseJsonLine(line: string): unknown {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(line) as unknown;
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|