Skip to content

Commit

Permalink
Merge pull request #2097 from Sekky61/code-action-convert-string-literal
Browse files Browse the repository at this point in the history
  • Loading branch information
Techatrix authored Dec 5, 2024
2 parents 532cc25 + 9316eaa commit 70f9e7c
Show file tree
Hide file tree
Showing 6 changed files with 434 additions and 60 deletions.
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
56 changes: 42 additions & 14 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,
};
}

/// Asserts that `self` is one of the following:
/// - `.import_string_literal`
/// - `.cinclude_string_literal`
/// - `.embedfile_string_literal`
/// - `.string_literal`
pub fn stringLiteralContentLoc(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 => unreachable,
};

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 @@ -3491,6 +3518,8 @@ pub fn getPositionContext(
.identifier,
.builtin,
.number_literal,
.string_literal,
.multiline_string_literal_line,
=> should_do_lookahead = false,
else => break,
}
Expand Down Expand Up @@ -3518,41 +3547,40 @@ pub fn getPositionContext(
var curr_ctx = try peek(allocator, &stack);
switch (tok.tag) {
.string_literal, .multiline_string_literal_line => string_lit_block: {
curr_ctx.ctx = .{ .string_literal = tok.loc };
if (tok.tag != .string_literal) break :string_lit_block;

const string_literal_slice = offsets.locToSlice(tree.source, tok.loc);
var string_literal_loc = tok.loc;
var content_loc = tok.loc;

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

if (!(string_literal_loc.start <= source_index and source_index <= string_literal_loc.end)) break :string_lit_block;
if (source_index < content_loc.start or content_loc.end < source_index) break :string_lit_block;

if (curr_ctx.stack_id == .Paren and stack.items.len >= 2) {
if (curr_ctx.stack_id == .Paren and
stack.items.len >= 2)
{
const perhaps_builtin = stack.items[stack.items.len - 2];

switch (perhaps_builtin.ctx) {
.builtin => |loc| {
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;
curr_ctx.ctx = .{ .import_string_literal = tok.loc };
} else if (std.mem.eql(u8, builtin_name, "@cInclude")) {
curr_ctx.ctx = .{ .cinclude_string_literal = string_literal_loc };
break :string_lit_block;
curr_ctx.ctx = .{ .cinclude_string_literal = tok.loc };
} else if (std.mem.eql(u8, builtin_name, "@embedFile")) {
curr_ctx.ctx = .{ .embedfile_string_literal = string_literal_loc };
break :string_lit_block;
curr_ctx.ctx = .{ .embedfile_string_literal = tok.loc };
}
},
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
141 changes: 138 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,37 @@ 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 token_tags = tree.tokens.items(.tag);

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

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

var token_idx = offsets.sourceIndexToTokenIndex(tree, source_index);

// if `offsets.sourceIndexToTokenIndex` is called with a source index between two tokens, it will be the token to the right.
switch (token_tags[token_idx]) {
.string_literal, .multiline_string_literal_line => {},
else => token_idx -|= 1,
}

switch (token_tags[token_idx]) {
.multiline_string_literal_line => try generateMultilineStringCodeActions(builder, token_idx, actions),
.string_literal => try generateStringLiteralCodeActions(builder, token_idx, actions),
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 +125,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') 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 +255,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 +469,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 +599,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
15 changes: 6 additions & 9 deletions src/features/completions.zig
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ fn completeDot(builder: *Builder, loc: offsets.Loc) error{OutOfMemory}!void {
try globalSetCompletions(builder, .enum_set);
}

/// Expects that `pos_context` is one of the following:
/// Asserts that `pos_context` is one of the following:
/// - `.import_string_literal`
/// - `.cinclude_string_literal`
/// - `.embedfile_string_literal`
Expand All @@ -697,17 +697,14 @@ 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().?;
if (pos_context == .string_literal and !DocumentStore.isBuildFile(builder.orig_handle.uri)) return;

var string_content_loc = pos_context.stringLiteralContentLoc(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) {
switch (source[string_content_loc.end]) {
0, '\n', '\r', '\"' => break,
else => continue,
}
}
string_content_loc.end = std.mem.indexOfAnyPos(u8, source, string_content_loc.end, &.{ 0, '\n', '\r', '\"' }) orelse source.len;

if (pos_context == .string_literal and !DocumentStore.isBuildFile(builder.orig_handle.uri)) return;
if (builder.source_index < string_content_loc.start or string_content_loc.end < builder.source_index) return;

const previous_separator_index: ?usize = blk: {
var index: usize = builder.source_index;
Expand Down
Loading

0 comments on commit 70f9e7c

Please sign in to comment.