summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig117
1 files changed, 106 insertions, 11 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 4b9e8b1d..4ce4d101 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -4321,16 +4321,24 @@ pub const Pardes = struct {
/// this one" — the id bump makes the older answer stale and lspResponse
/// drops it. A queue would only buy the right to render an answer nobody
/// is waiting for any more.
- /// `arg` rides along only so the buffer the answer opens can record what
- /// was asked (a rename's new name, a symbol query) — the query itself has
- /// it in the effect already.
- /// `row`/`col` are where the cursor was when the question was asked. Only
- /// `completion` reads them, and only to undo itself: Tab diverted instead
- /// of indenting, so an empty answer has to put the indent back — but only
- /// if the cursor has not moved since, or four spaces appear under someone
- /// who kept typing.
+ /// `arg` holds the replacement name for rename and the query for workspace
+ /// symbols. `serial` rejects a response after its pane slot was reused;
+ /// `revision` makes a mutating rename conditional on the source snapshot
+ /// the worker actually analysed. `row`/`col` are where the cursor was when
+ /// the question was asked. Only `completion` reads them, and only to undo
+ /// itself: Tab diverted instead of indenting, so an empty answer has to put
+ /// the indent back — but only if the cursor has not moved since.
lsp_seq: u32 = 0,
- lsp_wait: ?struct { id: u32, kind: lsp.Kind, pane: usize, arg: Effect.Buf(128), row: i32 = 0, col: i32 = 0 } = null,
+ lsp_wait: ?struct {
+ id: u32,
+ kind: lsp.Kind,
+ pane: usize,
+ serial: u32,
+ revision: u32,
+ arg: Effect.Buf(128),
+ row: i32 = 0,
+ col: i32 = 0,
+ } = null,
/// One current shell-filter request. A newer submit frees and supersedes
/// it; old worker answers then fail the id check. The request itself owns
@@ -7642,6 +7650,7 @@ pub const Pardes = struct {
if (f.output != null) return;
}
if (arg.len > 128) return; // the effect's arg is a Buf(128)
+ if (kind == .rename and (!std.zig.isValidId(arg) or std.zig.isUnderscore(arg))) return;
const off = if (pane.file) |f| modal.hxOff(f.content, .{
.row = @intCast(@max(0, pane.cur_row)),
.col = @intCast(@max(0, pane.cur_col)),
@@ -7651,6 +7660,8 @@ pub const Pardes = struct {
.id = p.lsp_seq,
.kind = kind,
.pane = id,
+ .serial = pane.serial,
+ .revision = if (pane.file) |f| f.revision else 0,
.arg = .from(arg),
.row = pane.cur_row,
.col = pane.cur_col,
@@ -7664,8 +7675,90 @@ pub const Pardes = struct {
} });
}
- /// A worker answered. Rows are `+Search` format with ABSOLUTE paths, so
- /// both dispositions below are the ordinary look path:
+ const LspEdit = struct { start: usize, end: usize };
+
+ fn parseLspEdits(p: *Pardes, bytes: []const u8) ?[]LspEdit {
+ if (bytes.len == 0 or bytes[bytes.len - 1] != '\n') return null;
+ const edits = p.scratch.allocator().alloc(LspEdit, std.mem.count(u8, bytes, "\n")) catch return null;
+ var lines = std.mem.splitScalar(u8, bytes, '\n');
+ var n: usize = 0;
+ while (lines.next()) |line| {
+ if (line.len == 0) {
+ if (lines.peek() == null) break;
+ return null;
+ }
+ var fields = std.mem.tokenizeScalar(u8, line, ' ');
+ if (!std.mem.eql(u8, fields.next() orelse return null, "@edit")) return null;
+ const start = std.fmt.parseInt(usize, fields.next() orelse return null, 10) catch return null;
+ const end = std.fmt.parseInt(usize, fields.next() orelse return null, 10) catch return null;
+ if (fields.next() != null) return null;
+ edits[n] = .{ .start = start, .end = end };
+ n += 1;
+ }
+ return if (n == 0) null else edits[0..n];
+ }
+
+ fn mapLspEditOffset(edits: []const LspEdit, replacement_len: usize, old: usize) usize {
+ var old_at: usize = 0;
+ var new_at: usize = 0;
+ for (edits) |e| {
+ if (old < e.start) return new_at + (old - old_at);
+ new_at += e.start - old_at;
+ if (old < e.end) return new_at + @min(old - e.start, replacement_len - 1);
+ new_at += replacement_len;
+ if (old == e.end) return new_at;
+ old_at = e.end;
+ }
+ return new_at + (old - old_at);
+ }
+
+ fn applyLspRename(p: *Pardes, pane: *Pane, revision: u32, new_name: []const u8, bytes: []const u8) void {
+ const f = if (pane.file) |*file| file else return;
+ if (f.revision != revision) return;
+ const edits = p.parseLspEdits(bytes) orelse return;
+
+ var removed: usize = 0;
+ var previous_end: usize = 0;
+ for (edits) |e| {
+ if (e.start < previous_end or e.start >= e.end or e.end > f.content.len) return;
+ removed = std.math.add(usize, removed, e.end - e.start) catch return;
+ previous_end = e.end;
+ }
+ if (std.mem.eql(u8, f.content[edits[0].start..edits[0].end], new_name)) return;
+ const added = std.math.mul(usize, edits.len, new_name.len) catch return;
+ const final_len = std.math.add(usize, f.content.len - removed, added) catch return;
+ const replacement = p.gpa.alloc(u8, final_len) catch return;
+
+ const old_cursor = modal.hxOff(f.content, .{
+ .row = @intCast(@max(0, pane.cur_row)),
+ .col = @intCast(@max(0, pane.cur_col)),
+ });
+ const mapped_cursor = mapLspEditOffset(edits, new_name.len, old_cursor);
+ var read_at: usize = 0;
+ var write_at: usize = 0;
+ for (edits) |e| {
+ @memcpy(replacement[write_at .. write_at + (e.start - read_at)], f.content[read_at..e.start]);
+ write_at += e.start - read_at;
+ @memcpy(replacement[write_at .. write_at + new_name.len], new_name);
+ write_at += new_name.len;
+ read_at = e.end;
+ }
+ @memcpy(replacement[write_at..], f.content[read_at..]);
+
+ p.pushUndo(pane);
+ file_pane.setContent(p, f, replacement);
+ const cursor = modal.hxPos(f.content, mapped_cursor);
+ pane.cur_row = @intCast(cursor.row);
+ pane.cur_col = @intCast(cursor.col);
+ pane.vsel.active = false;
+ pane.msel.active = false;
+ pane.select = false;
+ pane.sticky_col = -1;
+ pane.ensureCursorVisible();
+ }
+
+ /// A worker answered. Rename's edit records are consumed first and never
+ /// rendered. Every other response is the ordinary look/output path:
/// one row, a goto -> jump straight there (helix jumps on a single
/// location and shows a picker on several)
/// anything else -> an output buffer, which n/N already steps. That
@@ -7675,6 +7768,8 @@ pub const Pardes = struct {
if (w.id != id) return; // superseded by a newer press, or the pane died
p.lsp_wait = null;
const pane = p.panes[w.pane] orelse return;
+ if (pane.serial != w.serial) return;
+ if (w.kind == .rename) return p.applyLspRename(pane, w.revision, w.arg.slice(), rows);
if (rows.len == 0) {
// No rows is a legal answer everywhere except here. Tab DIVERTED
// instead of indenting, so an empty answer would eat the keystroke