Skip to content

Commit 1986070

Browse files
committed
lsp: add code action and rename element tag support
code action for now only offers to fix unknown tag names to `div`. rename symbol will let you rename the tag name of an element. if the element has a closing tag, it will be renamed aswell. we also support renaming erroneous end tags.
1 parent 3b2ea0a commit 1986070

File tree

5 files changed

+202
-9
lines changed

5 files changed

+202
-9
lines changed

src/cli.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ pub fn main() !void {
8181
fatalHelp();
8282
};
8383

84-
// if (cmd == .lsp) lsp_mode = true;
84+
if (cmd == .lsp) lsp_mode = true;
8585

8686
_ = switch (cmd) {
8787
.check => check_exe.run(gpa, args[2..]),

src/cli/logging.zig

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub fn logFn(
3838
defer std.debug.unlockStdErr();
3939

4040
var buf: [1024]u8 = undefined;
41-
var fw = l.writer(&buf);
41+
var fw = l.writerStreaming(&buf);
4242
const w = &fw.interface;
4343
w.print(prefix ++ format ++ "\n", args) catch return;
4444
w.flush() catch return;
@@ -54,11 +54,13 @@ pub fn setup(gpa: std.mem.Allocator) void {
5454
}
5555

5656
fn setupInternal(gpa: std.mem.Allocator) !void {
57-
const cache_base = try folders.open(gpa, .cache, .{}) orelse return error.Failure;
58-
try cache_base.makePath("super");
57+
var cache_base = try folders.open(gpa, .cache, .{}) orelse return error.Failure;
58+
errdefer cache_base.close();
5959

60-
const log_path = "superhtml.log";
60+
const log_path = "superhtml.log1";
6161
const file = try cache_base.createFile(log_path, .{ .truncate = false });
62+
errdefer file.close();
63+
6264
const end = try file.getEndPos();
6365
try file.seekTo(end);
6466

src/cli/lsp.zig

Lines changed: 183 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,13 @@ pub fn initialize(
107107
},
108108
},
109109

110-
// .completionProvider = .{
111-
// .triggerCharacters = &.{ "<", "/" },
112-
// },
110+
.codeActionProvider = .{ .bool = true },
111+
112+
.renameProvider = .{ .bool = true },
113+
114+
.completionProvider = .{
115+
.triggerCharacters = &.{ "<", "/" },
116+
},
113117

114118
.documentFormattingProvider = .{ .bool = true },
115119
};
@@ -242,6 +246,182 @@ pub fn @"textDocument/formatting"(
242246
}});
243247
}
244248

249+
pub fn @"textDocument/codeAction"(
250+
self: *Handler,
251+
arena: std.mem.Allocator,
252+
request: types.CodeActionParams,
253+
) error{OutOfMemory}!lsp.ResultType("textDocument/codeAction") {
254+
const doc = self.files.getPtr(request.textDocument.uri) orelse return null;
255+
const offset = lsp.offsets.positionToIndex(
256+
doc.src,
257+
request.range.start,
258+
self.offset_encoding,
259+
);
260+
261+
if (!self.strict) return null;
262+
263+
for (doc.html.errors) |err| {
264+
if (err.tag != .ast or err.tag.ast != .invalid_html_tag_name) continue;
265+
266+
const span = err.main_location;
267+
if (span.start <= offset and span.end > offset) {
268+
const edits = try arena.alloc(lsp.types.TextEdit, 2);
269+
edits[0] = .{
270+
.range = getRange(span, doc.src),
271+
.newText = "div",
272+
};
273+
274+
const edits_len: usize = if (err.node_idx != 0) blk: {
275+
const node = doc.html.nodes[err.node_idx];
276+
if (node.kind != .element) break :blk 1;
277+
278+
const close = node.close;
279+
if (close.end < 2 or close.start > close.end - 2) break :blk 1;
280+
281+
edits[1] = .{
282+
.range = getRange(.{
283+
.start = close.start + 1,
284+
.end = close.end - 1,
285+
}, doc.src),
286+
.newText = "/div",
287+
};
288+
289+
break :blk 2;
290+
} else 1;
291+
292+
const result: lsp.ResultType("textDocument/codeAction") = &.{
293+
.{
294+
.CodeAction = .{
295+
.title = "Replace with 'div'",
296+
.kind = .quickfix,
297+
.isPreferred = true,
298+
.edit = .{
299+
.changes = .{
300+
.map = try .init(
301+
arena,
302+
&.{request.textDocument.uri},
303+
&.{edits[0..edits_len]},
304+
),
305+
},
306+
},
307+
},
308+
},
309+
};
310+
311+
return try arena.dupe(@typeInfo(@TypeOf(result.?)).pointer.child, result.?);
312+
}
313+
}
314+
315+
return null;
316+
}
317+
318+
pub fn @"textDocument/rename"(
319+
self: *Handler,
320+
arena: std.mem.Allocator,
321+
request: types.RenameParams,
322+
) error{OutOfMemory}!lsp.ResultType("textDocument/rename") {
323+
const doc = self.files.getPtr(request.textDocument.uri) orelse return null;
324+
const offset = lsp.offsets.positionToIndex(
325+
doc.src,
326+
request.position,
327+
self.offset_encoding,
328+
);
329+
330+
const node_idx: u32 = for (doc.html.errors) |err| {
331+
// Find erroneous end tags in the error list but also any other error that
332+
// has a node associated that happens to match our offset.
333+
const span = err.main_location;
334+
if (span.start <= offset and span.end > offset) {
335+
if (err.tag == .ast and err.tag.ast == .erroneous_end_tag) {
336+
const edits = try arena.alloc(lsp.types.TextEdit, 1);
337+
edits[0] = .{
338+
.range = getRange(span, doc.src),
339+
.newText = request.newName,
340+
};
341+
return .{
342+
.changes = .{
343+
.map = try .init(
344+
arena,
345+
&.{request.textDocument.uri},
346+
&.{edits},
347+
),
348+
},
349+
};
350+
}
351+
if (err.node_idx != 0) break err.node_idx;
352+
}
353+
} else blk: {
354+
// No match found in the error list, must navigate the AST
355+
if (doc.html.nodes.len < 2) return null;
356+
var cur_idx: u32 = 1;
357+
while (cur_idx != 0) {
358+
const n = doc.html.nodes[cur_idx];
359+
if (n.open.start <= offset and n.open.end > offset) {
360+
break;
361+
}
362+
if (n.close.end != 0 and n.close.start <= offset and n.close.end > offset) {
363+
break;
364+
}
365+
366+
if (n.open.end <= offset and n.close.start > offset) {
367+
cur_idx = n.first_child_idx;
368+
} else {
369+
cur_idx = n.next_idx;
370+
}
371+
}
372+
373+
break :blk cur_idx;
374+
};
375+
376+
const node = doc.html.nodes[node_idx];
377+
var edits: []lsp.types.TextEdit = undefined;
378+
blk: switch (node.kind) {
379+
else => return null,
380+
.element => {
381+
edits = try arena.alloc(lsp.types.TextEdit, 2);
382+
383+
const it = node.startTagIterator(doc.src, doc.language);
384+
edits[0] = .{
385+
.range = getRange(it.name_span, doc.src),
386+
.newText = request.newName,
387+
};
388+
389+
const close = node.close;
390+
if (close.end < 2 or close.start > close.end - 2) {
391+
edits = edits[0..1];
392+
break :blk;
393+
}
394+
395+
edits[1] = .{
396+
.range = getRange(.{
397+
.start = close.start + 1,
398+
.end = close.end - 1,
399+
}, doc.src),
400+
.newText = try std.fmt.allocPrint(arena, "/{s}", .{request.newName}),
401+
};
402+
},
403+
.element_void, .element_self_closing => {
404+
edits = try arena.alloc(lsp.types.TextEdit, 1);
405+
406+
const it = node.startTagIterator(doc.src, doc.language);
407+
edits[0] = .{
408+
.range = getRange(it.name_span, doc.src),
409+
.newText = request.newName,
410+
};
411+
},
412+
}
413+
414+
return .{
415+
.changes = .{
416+
.map = try .init(
417+
arena,
418+
&.{request.textDocument.uri},
419+
&.{edits},
420+
),
421+
},
422+
};
423+
}
424+
245425
pub fn @"textDocument/completion"(
246426
self: *Handler,
247427
arena: std.mem.Allocator,

src/html/Ast.zig

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ pub const Error = struct {
291291
},
292292
},
293293
main_location: Span,
294+
node_idx: u32, // 0 = missing node
294295
};
295296

296297
language: Language,
@@ -407,6 +408,7 @@ pub fn init(
407408
.ast = .html_elements_cant_self_close,
408409
},
409410
.main_location = tag.name,
411+
.node_idx = current_idx + 1,
410412
});
411413
}
412414
break :blk .element_self_closing;
@@ -431,6 +433,7 @@ pub fn init(
431433
.ast = .invalid_html_tag_name,
432434
},
433435
.main_location = tag.name,
436+
.node_idx = current_idx + 1,
434437
});
435438
}
436439

@@ -464,6 +467,7 @@ pub fn init(
464467
.ast = .deprecated_and_unsupported,
465468
},
466469
.main_location = tag.name,
470+
.node_idx = current_idx,
467471
});
468472
}
469473

@@ -501,6 +505,7 @@ pub fn init(
501505
.start = attr.name.start,
502506
.end = attr.name.end,
503507
},
508+
.node_idx = current_idx,
504509
});
505510
}
506511
},
@@ -515,6 +520,7 @@ pub fn init(
515520
.ast = .erroneous_end_tag,
516521
},
517522
.main_location = tag.name,
523+
.node_idx = 0,
518524
});
519525
continue;
520526
}
@@ -538,6 +544,7 @@ pub fn init(
538544
try errors.append(.{
539545
.tag = .{ .ast = .erroneous_end_tag },
540546
.main_location = tag.name,
547+
.node_idx = 0,
541548
});
542549
break;
543550
}
@@ -587,6 +594,7 @@ pub fn init(
587594
try errors.append(.{
588595
.tag = .{ .ast = .missing_end_tag },
589596
.main_location = cur_name,
597+
.node_idx = current_idx,
590598
});
591599
}
592600

@@ -661,6 +669,7 @@ pub fn init(
661669
.token = pe.tag,
662670
},
663671
.main_location = pe.span,
672+
.node_idx = current_idx,
664673
});
665674
},
666675
}
@@ -672,9 +681,11 @@ pub fn init(
672681
try errors.append(.{
673682
.tag = .{ .ast = .missing_end_tag },
674683
.main_location = current.open,
684+
.node_idx = current_idx,
675685
});
676686
}
677687

688+
current_idx = current.parent_idx;
678689
current = &nodes.items[current.parent_idx];
679690
}
680691

src/root.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ pub const Language = enum {
9494
};
9595
pub const max_size = 4 * 1024 * 1024 * 1024;
9696

97-
const Range = struct {
97+
pub const Range = struct {
9898
start: Pos,
9999
end: Pos,
100100

0 commit comments

Comments
 (0)