@@ -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+
245425pub fn @"textDocument/completion" (
246426 self : * Handler ,
247427 arena : std.mem.Allocator ,
0 commit comments