Skip to content

Commit ea0ea75

Browse files
committed
lsp: add find references for CSS classes
place the cursor on a class name and select find all references, all other elements containing that same class will be shown
1 parent 1323113 commit ea0ea75

File tree

3 files changed

+151
-25
lines changed

3 files changed

+151
-25
lines changed

src/cli/logging.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ fn setupInternal(gpa: std.mem.Allocator) !void {
5757
var cache_base = try folders.open(gpa, .cache, .{}) orelse return error.Failure;
5858
errdefer cache_base.close();
5959

60-
const log_path = "superhtml.log1";
60+
const log_path = "superhtml.log";
6161
const file = try cache_base.createFile(log_path, .{ .truncate = false });
6262
errdefer file.close();
6363

src/cli/lsp.zig

Lines changed: 137 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,13 @@ pub fn initialize(
109109

110110
.codeActionProvider = .{ .bool = true },
111111

112-
.renameProvider = .{ .bool = true },
112+
.renameProvider = .{
113+
.RenameOptions = .{
114+
.prepareProvider = true,
115+
},
116+
},
117+
118+
.referencesProvider = .{ .bool = true },
113119

114120
.completionProvider = .{
115121
.triggerCharacters = &.{ "<", "/" },
@@ -315,6 +321,35 @@ pub fn @"textDocument/codeAction"(
315321
return null;
316322
}
317323

324+
pub fn @"textDocument/prepareRename"(
325+
self: *Handler,
326+
arena: std.mem.Allocator,
327+
request: types.PrepareRenameParams,
328+
) error{OutOfMemory}!lsp.ResultType("textDocument/prepareRename") {
329+
_ = arena;
330+
331+
const doc = self.files.getPtr(request.textDocument.uri) orelse return null;
332+
const offset = lsp.offsets.positionToIndex(
333+
doc.src,
334+
request.position,
335+
self.offset_encoding,
336+
);
337+
338+
const node_idx = findNode(doc, @intCast(offset));
339+
if (node_idx == 0) return null;
340+
341+
const node = doc.html.nodes[node_idx];
342+
const it = node.startTagIterator(doc.src, doc.language);
343+
344+
const range = lsp.offsets.locToRange(doc.src, .{
345+
.start = it.name_span.start,
346+
.end = it.name_span.end,
347+
}, self.offset_encoding);
348+
349+
return .{
350+
.Range = range,
351+
};
352+
}
318353
pub fn @"textDocument/rename"(
319354
self: *Handler,
320355
arena: std.mem.Allocator,
@@ -350,28 +385,7 @@ pub fn @"textDocument/rename"(
350385
}
351386
if (err.node_idx != 0) break err.node_idx;
352387
}
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-
};
388+
} else findNode(doc, @intCast(offset));
375389

376390
const node = doc.html.nodes[node_idx];
377391
var edits: []lsp.types.TextEdit = undefined;
@@ -422,6 +436,83 @@ pub fn @"textDocument/rename"(
422436
};
423437
}
424438

439+
pub fn @"textDocument/references"(
440+
self: *Handler,
441+
arena: std.mem.Allocator,
442+
request: types.ReferenceParams,
443+
) error{OutOfMemory}!lsp.ResultType("textDocument/references") {
444+
const doc = self.files.getPtr(request.textDocument.uri) orelse return null;
445+
const offset = lsp.offsets.positionToIndex(
446+
doc.src,
447+
request.position,
448+
self.offset_encoding,
449+
);
450+
451+
const node_idx = findNode(doc, @intCast(offset));
452+
log.debug("------ References request! (node: {}) ------", .{node_idx});
453+
if (node_idx == 0) return null;
454+
455+
const class = blk: {
456+
const node = doc.html.nodes[node_idx];
457+
var it = node.startTagIterator(doc.src, doc.language);
458+
while (it.next(doc.src)) |attr| {
459+
if (std.ascii.eqlIgnoreCase(attr.name.slice(doc.src), "class")) {
460+
const value = attr.value orelse return null;
461+
const slice = value.span.slice(doc.src);
462+
if (slice.len == 0 or slice[0] == '$') return null;
463+
if (offset < value.span.start or offset >= value.span.end) return null;
464+
465+
const rel_offset = offset - value.span.start;
466+
467+
var vit = std.mem.tokenizeScalar(u8, slice, ' ');
468+
469+
while (vit.next()) |cls| {
470+
if (rel_offset < vit.index - cls.len) return null;
471+
if (vit.index > rel_offset) {
472+
break :blk cls;
473+
}
474+
} else return null;
475+
}
476+
} else return null;
477+
};
478+
479+
log.debug("------ CLASS: '{s}' ------", .{class});
480+
481+
var locations: std.ArrayListUnmanaged(lsp.types.Location) = .empty;
482+
for (doc.html.nodes) |n| {
483+
switch (n.kind) {
484+
.element, .element_void, .element_self_closing => {},
485+
else => continue,
486+
}
487+
488+
var it = n.startTagIterator(doc.src, doc.language);
489+
outer: while (it.next(doc.src)) |attr| {
490+
if (std.ascii.eqlIgnoreCase(attr.name.slice(doc.src), "class")) {
491+
const value = attr.value orelse break :outer;
492+
const slice = value.span.slice(doc.src);
493+
if (slice.len == 0 or slice[0] == '$') break :outer;
494+
var vit = std.mem.tokenizeScalar(u8, slice, ' ');
495+
while (vit.next()) |cls| {
496+
if (std.mem.eql(u8, class, cls)) {
497+
const range = lsp.offsets.locToRange(doc.src, .{
498+
.start = value.span.start + vit.index - cls.len,
499+
.end = value.span.start + vit.index,
500+
}, self.offset_encoding);
501+
502+
try locations.append(arena, .{
503+
.uri = request.textDocument.uri,
504+
.range = range,
505+
});
506+
break :outer;
507+
}
508+
}
509+
}
510+
}
511+
}
512+
513+
return locations.items;
514+
}
515+
425516
pub fn @"textDocument/completion"(
426517
self: *Handler,
427518
arena: std.mem.Allocator,
@@ -455,3 +546,26 @@ pub fn getRange(span: super.Span, src: []const u8) types.Range {
455546
.end = .{ .line = r.end.row, .character = r.end.col },
456547
};
457548
}
549+
550+
// Returns a node index, 0 == not found
551+
pub fn findNode(doc: *const Document, offset: u32) u32 {
552+
if (doc.html.nodes.len < 2) return 0;
553+
var cur_idx: u32 = 1;
554+
while (cur_idx != 0) {
555+
const n = doc.html.nodes[cur_idx];
556+
if (n.open.start <= offset and n.open.end > offset) {
557+
break;
558+
}
559+
if (n.close.end != 0 and n.close.start <= offset and n.close.end > offset) {
560+
break;
561+
}
562+
563+
if (n.open.end <= offset and n.close.start > offset) {
564+
cur_idx = n.first_child_idx;
565+
} else {
566+
cur_idx = n.next_idx;
567+
}
568+
}
569+
570+
return cur_idx;
571+
}

src/html/Ast.zig

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ pub fn init(
365365
var current: *Node = &nodes.items[0];
366366
var current_idx: u32 = 0;
367367
var svg_lvl: u32 = 0;
368+
var math_lvl: u32 = 0;
368369
while (tokenizer.next(src)) |t| {
369370
log.debug("cur_idx: {} cur_kind: {s} tok: {any}", .{
370371
current_idx,
@@ -423,8 +424,16 @@ pub fn init(
423424
if (std.ascii.eqlIgnoreCase(tag.name.slice(src), "svg")) {
424425
svg_lvl += 1;
425426
}
427+
if (std.ascii.eqlIgnoreCase(tag.name.slice(src), "math")) {
428+
math_lvl += 1;
429+
}
426430

427-
if (language != .xml and strict_tag_names and svg_lvl == 0) blk: {
431+
if (language != .xml and
432+
strict_tag_names and
433+
svg_lvl == 0 and
434+
math_lvl == 0 and
435+
std.mem.indexOfScalar(u8, name, '-') == null)
436+
blk: {
428437
const valid_html = valid_html_tags.has(name);
429438
const valid_shtml = valid_shtml_tags.has(name);
430439
if (valid_html or (language == .superhtml and valid_shtml)) break :blk;
@@ -575,6 +584,9 @@ pub fn init(
575584
if (std.ascii.eqlIgnoreCase(current_name, "svg")) {
576585
svg_lvl -= 1;
577586
}
587+
if (std.ascii.eqlIgnoreCase(current_name, "math")) {
588+
math_lvl -= 1;
589+
}
578590
current.close = tag.span;
579591
var cur = original_current;
580592
while (cur != current) {

0 commit comments

Comments
 (0)