Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code action to convert string literal #2097

Merged
merged 10 commits into from
Dec 5, 2024
1 change: 1 addition & 0 deletions src/Server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1660,6 +1660,7 @@ fn codeActionHandler(server: *Server, arena: std.mem.Allocator, request: types.C

var actions: std.ArrayListUnmanaged(types.CodeAction) = .{};
try builder.generateCodeAction(error_bundle, &actions);
try builder.generateCodeActionsInRange(request.range, &actions);

const Result = lsp.types.getRequestMetadata("textDocument/codeAction").?.Result;
const result = try arena.alloc(std.meta.Child(std.meta.Child(Result)), actions.items.len);
Expand Down
52 changes: 32 additions & 20 deletions src/analysis.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3404,6 +3404,33 @@ pub const PositionContext = union(enum) {
=> return null,
};
}

/// Expects that `self` is one of the following:
/// - `.import_string_literal`
/// - `.cinclude_string_literal`
/// - `.embedfile_string_literal`
/// - `.string_literal`
pub fn content_loc(self: PositionContext, source: []const u8) ?offsets.Loc {
var location = switch (self) {
.import_string_literal,
.cinclude_string_literal,
.embedfile_string_literal,
.string_literal,
=> |l| l,
else => return null,
};

const string_literal_slice = offsets.locToSlice(source, location);
if (std.mem.startsWith(u8, string_literal_slice, "\"")) {
location.start += 1;
if (std.mem.endsWith(u8, string_literal_slice[1..], "\"")) {
location.end -= 1;
}
} else if (std.mem.startsWith(u8, string_literal_slice, "\\")) {
location.start += 2;
}
return location;
}
};

const StackState = struct {
Expand Down Expand Up @@ -3488,10 +3515,7 @@ pub fn getPositionContext(
// `tok` is the latter of the two.
if (!should_do_lookahead) break;
switch (tok.tag) {
.identifier,
.builtin,
.number_literal,
=> should_do_lookahead = false,
.identifier, .builtin, .number_literal, .string_literal, .multiline_string_literal_line => should_do_lookahead = false,
else => break,
}
}
Expand All @@ -3518,19 +3542,11 @@ pub fn getPositionContext(
var curr_ctx = try peek(allocator, &stack);
switch (tok.tag) {
.string_literal, .multiline_string_literal_line => string_lit_block: {
const string_literal_slice = offsets.locToSlice(tree.source, tok.loc);
var string_literal_loc = tok.loc;

if (std.mem.startsWith(u8, string_literal_slice, "\"")) {
string_literal_loc.start += 1;
if (std.mem.endsWith(u8, string_literal_slice[1..], "\"")) {
string_literal_loc.end -= 1;
}
} else if (std.mem.startsWith(u8, string_literal_slice, "\\")) {
string_literal_loc.start += 2;
}
const string_literal_loc = tok.loc;

if (!(string_literal_loc.start <= source_index and source_index <= string_literal_loc.end)) break :string_lit_block;
if (string_literal_loc.start > source_index or source_index > string_literal_loc.end) break :string_lit_block;
if (tok.tag != .multiline_string_literal_line and lookahead and source_index == string_literal_loc.end) break :string_lit_block;
curr_ctx.ctx = .{ .string_literal = string_literal_loc };

if (curr_ctx.stack_id == .Paren and stack.items.len >= 2) {
const perhaps_builtin = stack.items[stack.items.len - 2];
Expand All @@ -3540,19 +3556,15 @@ pub fn getPositionContext(
const builtin_name = tree.source[loc.start..loc.end];
if (std.mem.eql(u8, builtin_name, "@import")) {
curr_ctx.ctx = .{ .import_string_literal = string_literal_loc };
break :string_lit_block;
} else if (std.mem.eql(u8, builtin_name, "@cInclude")) {
curr_ctx.ctx = .{ .cinclude_string_literal = string_literal_loc };
break :string_lit_block;
} else if (std.mem.eql(u8, builtin_name, "@embedFile")) {
curr_ctx.ctx = .{ .embedfile_string_literal = string_literal_loc };
break :string_lit_block;
}
},
else => {},
}
}
curr_ctx.ctx = .{ .string_literal = string_literal_loc };
},
.identifier => switch (curr_ctx.ctx) {
.enum_literal => curr_ctx.ctx = .{ .enum_literal = tokenLocAppend(curr_ctx.ctx.loc().?, tok) },
Expand Down
137 changes: 134 additions & 3 deletions src/features/code_actions.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const std = @import("std");
const Ast = std.zig.Ast;
const Token = std.zig.Token;

const DocumentStore = @import("../DocumentStore.zig");
const DocumentScope = @import("../DocumentScope.zig");
Expand Down Expand Up @@ -75,6 +76,33 @@ pub const Builder = struct {
return only_kinds.contains(kind);
}

pub fn generateCodeActionsInRange(
builder: *Builder,
range: types.Range,
actions: *std.ArrayListUnmanaged(types.CodeAction),
) error{OutOfMemory}!void {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();

const tree = builder.handle.tree;
const source_index = offsets.positionToIndex(tree.source, range.start, builder.offset_encoding);

const token_idx = offsets.sourceIndexToTokenIndex(tree, source_index);
const token_tags = tree.tokens.items(.tag);
const position_token = token_tags[token_idx];

const ctx = try Analyser.getPositionContext(builder.arena, builder.handle.tree, source_index, true);

switch (ctx) {
.string_literal => switch (position_token) {
.multiline_string_literal_line => try generateMultilineStringCodeActions(builder, token_idx, actions),
.string_literal => try generateStringLiteralCodeActions(builder, token_idx, actions),
else => {},
},
else => {},
}
}

pub fn createTextEditLoc(self: *Builder, loc: offsets.Loc, new_text: []const u8) types.TextEdit {
const range = offsets.locToRange(self.handle.tree.source, loc, self.offset_encoding);
return types.TextEdit{ .range = range, .newText = new_text };
Expand All @@ -93,6 +121,109 @@ pub const Builder = struct {
}
};

pub fn generateStringLiteralCodeActions(
builder: *Builder,
token: Ast.TokenIndex,
actions: *std.ArrayListUnmanaged(types.CodeAction),
) !void {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();

if (!builder.wantKind(.refactor)) return;

const tags = builder.handle.tree.tokens.items(.tag);
switch (tags[token -| 1]) {
// Not covered by position context
.keyword_test, .keyword_extern => return,
else => {},
}

const token_text = offsets.tokenToSlice(builder.handle.tree, token); // Includes quotes
const parsed = std.zig.string_literal.parseAlloc(builder.arena, token_text) catch |err| switch (err) {
error.InvalidLiteral => return,
else => |other| return other,
};
// Check for disallowed characters and utf-8 validity
for (parsed) |c| {
if (c == '\n' or c == '\r') continue;
if (std.ascii.isControl(c)) return;
}
if (!std.unicode.utf8ValidateSlice(parsed)) return;
const with_slashes = try std.mem.replaceOwned(u8, builder.arena, parsed, "\n", "\n \\\\"); // Hardcoded 4 spaces

var result = try std.ArrayListUnmanaged(u8).initCapacity(builder.arena, with_slashes.len + 3);
result.appendSliceAssumeCapacity("\\\\");
result.appendSliceAssumeCapacity(with_slashes);
result.appendAssumeCapacity('\n');

const loc = offsets.tokenToLoc(builder.handle.tree, token);
try actions.append(builder.arena, .{
.title = "convert to a multiline string literal",
.kind = .refactor,
.isPreferred = false,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(loc, result.items)}),
});
}

pub fn generateMultilineStringCodeActions(
builder: *Builder,
token: Ast.TokenIndex,
actions: *std.ArrayListUnmanaged(types.CodeAction),
) !void {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();

if (!builder.wantKind(.refactor)) return;

const token_tags = builder.handle.tree.tokens.items(.tag);
std.debug.assert(.multiline_string_literal_line == token_tags[token]);
// Collect (exclusive) token range of the literal (one token per literal line)
const start = if (std.mem.lastIndexOfNone(Token.Tag, token_tags[0..(token + 1)], &.{.multiline_string_literal_line})) |i| i + 1 else 0;
const end = std.mem.indexOfNonePos(Token.Tag, token_tags, token, &.{.multiline_string_literal_line}) orelse token_tags.len;

// collect the text in the literal
const loc = offsets.tokensToLoc(builder.handle.tree, @intCast(start), @intCast(end));
var str_escaped = try std.ArrayListUnmanaged(u8).initCapacity(builder.arena, 2 * (loc.end - loc.start));
str_escaped.appendAssumeCapacity('"');
for (start..end) |i| {
std.debug.assert(token_tags[i] == .multiline_string_literal_line);
const string_part = offsets.tokenToSlice(builder.handle.tree, @intCast(i));
// Iterate without the leading \\
for (string_part[2..]) |c| {
const chunk = switch (c) {
'\\' => "\\\\",
'"' => "\\\"",
'\n' => "\\n",
0x01...0x09, 0x0b...0x0c, 0x0e...0x1f, 0x7f => unreachable,
else => &.{c},
};
str_escaped.appendSliceAssumeCapacity(chunk);
}
if (i != end - 1) {
str_escaped.appendSliceAssumeCapacity("\\n");
}
}
str_escaped.appendAssumeCapacity('"');

// Get Loc of the whole literal to delete it
// Multiline string literal ends before the \n or \r, but it must be deleted too
const first_token_start = builder.handle.tree.tokens.items(.start)[start];
const last_token_end = std.mem.indexOfNonePos(
u8,
builder.handle.tree.source,
offsets.tokenToLoc(builder.handle.tree, @intCast(end - 1)).end + 1,
"\n\r",
) orelse builder.handle.tree.source.len;
const remove_loc = offsets.Loc{ .start = first_token_start, .end = last_token_end };

try actions.append(builder.arena, .{
.title = "convert to a string literal",
.kind = .refactor,
.isPreferred = false,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(remove_loc, str_escaped.items)}),
});
}

/// To report server capabilities
pub const supported_code_actions: []const types.CodeActionKind = &.{
.quickfix,
Expand Down Expand Up @@ -120,7 +251,7 @@ pub fn collectAutoDiscardDiagnostics(
var i: usize = 0;
while (i < tree.tokens.len) {
const first_token: Ast.TokenIndex = @intCast(std.mem.indexOfPos(
std.zig.Token.Tag,
Token.Tag,
token_tags,
i,
&.{ .identifier, .equal, .identifier, .semicolon },
Expand Down Expand Up @@ -334,7 +465,7 @@ fn handleUnusedCapture(

const identifier_name = offsets.locToSlice(source, loc);

const capture_end: Ast.TokenIndex = @intCast(std.mem.indexOfScalarPos(std.zig.Token.Tag, token_tags, identifier_token, .pipe) orelse return);
const capture_end: Ast.TokenIndex = @intCast(std.mem.indexOfScalarPos(Token.Tag, token_tags, identifier_token, .pipe) orelse return);

var lbrace_token = capture_end + 1;

Expand Down Expand Up @@ -464,7 +595,7 @@ fn handleUnorganizedImport(builder: *Builder, actions: *std.ArrayListUnmanaged(t
try writer.writeByte('\n');

const tokens = tree.tokens.items(.tag);
const first_token = std.mem.indexOfNone(std.zig.Token.Tag, tokens, &.{.container_doc_comment}) orelse tokens.len;
const first_token = std.mem.indexOfNone(Token.Tag, tokens, &.{.container_doc_comment}) orelse tokens.len;
const insert_pos = offsets.tokenToPosition(tree, @intCast(first_token), builder.offset_encoding);

try edits.append(builder.arena, .{
Expand Down
2 changes: 1 addition & 1 deletion src/features/completions.zig
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ fn completeFileSystemStringLiteral(builder: *Builder, pos_context: Analyser.Posi
const store = &builder.server.document_store;
const source = builder.orig_handle.tree.source;

var string_content_loc = pos_context.loc().?;
var string_content_loc = pos_context.content_loc(source).?;

// the position context is without lookahead so we have to do it ourself
while (string_content_loc.end < source.len) : (string_content_loc.end += 1) {
Expand Down
Loading
Loading