Fix lsp weird behaviour on block cursors (#891) (#905)

* Fix lsp weird behaviour on block cursors (#891)

Adds lookahead option to getPositionContext.
This commit is contained in:
Álan Crístoffer 2023-01-22 21:47:53 +01:00 committed by GitHub
parent 8e98bd439b
commit 903f85ab94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 336 additions and 45 deletions

View File

@ -837,12 +837,12 @@ pub fn identifierFromPosition(pos_index: usize, handle: DocumentStore.Handle) []
if (pos_index + 1 >= handle.text.len) return ""; if (pos_index + 1 >= handle.text.len) return "";
var start_idx = pos_index; var start_idx = pos_index;
while (start_idx > 0 and isSymbolChar(handle.text[start_idx - 1])) { while (start_idx > 0 and analysis.isSymbolChar(handle.text[start_idx - 1])) {
start_idx -= 1; start_idx -= 1;
} }
var end_idx = pos_index; var end_idx = pos_index;
while (end_idx < handle.text.len and isSymbolChar(handle.text[end_idx])) { while (end_idx < handle.text.len and analysis.isSymbolChar(handle.text[end_idx])) {
end_idx += 1; end_idx += 1;
} }
@ -850,10 +850,6 @@ pub fn identifierFromPosition(pos_index: usize, handle: DocumentStore.Handle) []
return handle.text[start_idx..end_idx]; return handle.text[start_idx..end_idx];
} }
fn isSymbolChar(char: u8) bool {
return std.ascii.isAlphanumeric(char) or char == '_';
}
fn gotoDefinitionSymbol( fn gotoDefinitionSymbol(
server: *Server, server: *Server,
decl_handle: analysis.DeclWithHandle, decl_handle: analysis.DeclWithHandle,
@ -2124,7 +2120,7 @@ fn completionHandler(server: *Server, request: types.CompletionParams) Error!?ty
} }
const source_index = offsets.positionToIndex(handle.text, request.position, server.offset_encoding); const source_index = offsets.positionToIndex(handle.text, request.position, server.offset_encoding);
const pos_context = try analysis.getPositionContext(server.arena.allocator(), handle.text, source_index); const pos_context = try analysis.getPositionContext(server.arena.allocator(), handle.text, source_index, false);
const maybe_completions = switch (pos_context) { const maybe_completions = switch (pos_context) {
.builtin => try server.completeBuiltin(), .builtin => try server.completeBuiltin(),
@ -2205,7 +2201,7 @@ fn gotoHandler(server: *Server, request: types.TextDocumentPositionParams, resol
if (request.position.character == 0) return null; if (request.position.character == 0) return null;
const source_index = offsets.positionToIndex(handle.text, request.position, server.offset_encoding); const source_index = offsets.positionToIndex(handle.text, request.position, server.offset_encoding);
const pos_context = try analysis.getPositionContext(server.arena.allocator(), handle.text, source_index); const pos_context = try analysis.getPositionContext(server.arena.allocator(), handle.text, source_index, true);
return switch (pos_context) { return switch (pos_context) {
.var_access => try server.gotoDefinitionGlobal(source_index, handle, resolve_alias), .var_access => try server.gotoDefinitionGlobal(source_index, handle, resolve_alias),
@ -2245,7 +2241,7 @@ fn hoverHandler(server: *Server, request: types.HoverParams) Error!?types.Hover
if (request.position.character == 0) return null; if (request.position.character == 0) return null;
const source_index = offsets.positionToIndex(handle.text, request.position, server.offset_encoding); const source_index = offsets.positionToIndex(handle.text, request.position, server.offset_encoding);
const pos_context = try analysis.getPositionContext(server.arena.allocator(), handle.text, source_index); const pos_context = try analysis.getPositionContext(server.arena.allocator(), handle.text, source_index, true);
const response = switch (pos_context) { const response = switch (pos_context) {
.builtin => try server.hoverDefinitionBuiltin(source_index, handle), .builtin => try server.hoverDefinitionBuiltin(source_index, handle),
@ -2391,7 +2387,7 @@ fn generalReferencesHandler(server: *Server, request: GeneralReferencesRequest)
if (request.position().character <= 0) return null; if (request.position().character <= 0) return null;
const source_index = offsets.positionToIndex(handle.text, request.position(), server.offset_encoding); const source_index = offsets.positionToIndex(handle.text, request.position(), server.offset_encoding);
const pos_context = try analysis.getPositionContext(server.arena.allocator(), handle.text, source_index); const pos_context = try analysis.getPositionContext(server.arena.allocator(), handle.text, source_index, true);
const decl = switch (pos_context) { const decl = switch (pos_context) {
.var_access => try server.getSymbolGlobal(source_index, handle), .var_access => try server.getSymbolGlobal(source_index, handle),

View File

@ -1553,15 +1553,32 @@ fn tokenLocAppend(prev: offsets.Loc, token: std.zig.Token) offsets.Loc {
}; };
} }
pub fn isSymbolChar(char: u8) bool {
return std.ascii.isAlphanumeric(char) or char == '_';
}
/// Given a byte index in a document (typically cursor offset), classify what kind of entity is at that index. /// Given a byte index in a document (typically cursor offset), classify what kind of entity is at that index.
/// ///
/// Classification is based on the lexical structure -- we fetch the line containin index, tokenize it, /// Classification is based on the lexical structure -- we fetch the line containin index, tokenize it,
/// and look at the sequence of tokens just before the cursor. Due to the nice way zig is designed (only line /// and look at the sequence of tokens just before the cursor. Due to the nice way zig is designed (only line
/// comments, etc) lexing just a single line is always correct. /// comments, etc) lexing just a single line is always correct.
pub fn getPositionContext(allocator: std.mem.Allocator, text: []const u8, doc_index: usize) !PositionContext { pub fn getPositionContext(
const line_loc = offsets.lineLocAtIndex(text, doc_index); allocator: std.mem.Allocator,
text: []const u8,
doc_index: usize,
/// Should we look to the end of the current context? Yes for goto def, no for completions
lookahead: bool,
) !PositionContext {
var new_index = doc_index;
if (lookahead and new_index < text.len and isSymbolChar(text[new_index])) {
new_index += 1;
} else if (lookahead and new_index + 1 < text.len and text[new_index] == '@') {
new_index += 2;
}
const line_loc = if (!lookahead) offsets.lineLocAtIndex(text, new_index) else offsets.lineLocUntilIndex(text, new_index);
const line = offsets.locToSlice(text, line_loc); const line = offsets.locToSlice(text, line_loc);
const prev_char = if (doc_index > 0) text[doc_index - 1] else 0; const prev_char = if (new_index > 0) text[new_index - 1] else 0;
const is_comment = std.mem.startsWith(u8, std.mem.trimLeft(u8, line, " \t"), "//"); const is_comment = std.mem.startsWith(u8, std.mem.trimLeft(u8, line, " \t"), "//");
if (is_comment) return .comment; if (is_comment) return .comment;
@ -1582,8 +1599,8 @@ pub fn getPositionContext(allocator: std.mem.Allocator, text: []const u8, doc_in
while (true) { while (true) {
const tok = tokenizer.next(); const tok = tokenizer.next();
// Early exits. // Early exits.
if (tok.loc.start > doc_index) break; if (tok.loc.start > new_index) break;
if (tok.loc.start == doc_index) { if (tok.loc.start == new_index) {
// Tie-breaking, the curosr is exactly between two tokens, and // Tie-breaking, the curosr is exactly between two tokens, and
// `tok` is the latter of the two. // `tok` is the latter of the two.
if (tok.tag != .identifier) break; if (tok.tag != .identifier) break;

View File

@ -9,16 +9,47 @@ const allocator = std.testing.allocator;
test "position context - var access" { test "position context - var access" {
try testContext( try testContext(
\\const this_var = id<cursor>entifier; \\const a_var =<cursor> identifier;
,
.empty,
null,
);
try testContext(
\\const a_var = <cursor>identifier;
,
.var_access,
"i",
);
try testContext(
\\const a_var = iden<cursor>tifier;
,
.var_access,
"ident",
);
try testContext(
\\const a_var = identifier<cursor>;
, ,
.var_access, .var_access,
"identifier", "identifier",
); );
try testContext( try testContext(
\\const this_var = identifier<cursor>; \\const a_var = identifier;<cursor>
,
.empty,
null,
);
try testContext(
\\ fn foo() !<cursor>Str {
, ,
.var_access, .var_access,
"identifier", "S",
);
try testContext(
\\ fn foo() !St<cursor>r {
,
.var_access,
"Str",
); );
try testContext( try testContext(
\\ fn foo() !Str<cursor> { \\ fn foo() !Str<cursor> {
@ -26,74 +57,259 @@ test "position context - var access" {
.var_access, .var_access,
"Str", "Str",
); );
// TODO fix failing test! try testContext(
\\ fn foo() !Str <cursor>{
,
.var_access,
"Str",
);
// TODO fix failing tests
// try testContext(
// \\ fn foo() <cursor>Err!void {
// ,
// .var_access,
// "E",
// );
// try testContext(
// \\ fn foo() Er<cursor>r!void {
// ,
// .var_access,
// "Err",
// );
// try testContext( // try testContext(
// \\ fn foo() Err<cursor>!void { // \\ fn foo() Err<cursor>!void {
// , // ,
// .var_access, // .var_access,
// "Err", // "Err",
// ); // );
// try testContext(
// \\ fn foo() Err!<cursor>void {
// ,
// .var_access,
// "v",
// );
try testContext(
\\if (<cursor>bar.field == foo) {
,
.var_access,
"b",
);
try testContext(
\\if (ba<cursor>r.field == foo) {
,
.var_access,
"bar",
);
try testContext(
\\if (bar<cursor>.field == foo) {
,
.var_access,
"bar",
);
try testContext(
\\if (bar[0]<cursor>.field == foo) {
,
.var_access,
"bar",
);
} }
test "position context - field access" { test "position context - field access" {
try testContext( try testContext(
\\if (foo.<cursor>field == foo) { \\if (bar.<cursor>field == foo) {
, ,
.field_access, .field_access,
"foo.field", "bar.f",
); );
try testContext( try testContext(
\\if (foo.member.<cursor>field == foo) { \\if (bar.fie<cursor>ld == foo) {
, ,
.field_access, .field_access,
"foo.member.field", "bar.fiel",
); );
try testContext( try testContext(
\\if (foo.*.?.<cursor>field == foo) { \\if (bar.field<cursor> == foo) {
, ,
.field_access, .field_access,
"foo.*.?.field", "bar.field",
);
try testContext(
\\if (bar.member<cursor>.field == foo) {
,
.field_access,
"bar.member",
); );
try testContext( try testContext(
\\if (foo[0].<cursor>field == foo) { \\if (bar.member.<cursor>field == foo) {
, ,
.field_access, .field_access,
"foo[0].field", "bar.member.f",
); );
try testContext( try testContext(
\\if (foo.<cursor>@"field" == foo) { \\if (bar.member.fie<cursor>ld == foo) {
, ,
.field_access, .field_access,
"foo.@\"field\"", "bar.member.fiel",
);
try testContext(
\\if (bar.member.field<cursor> == foo) {
,
.field_access,
"bar.member.field",
);
try testContext(
\\if (bar.*.?<cursor>.field == foo) {
,
.field_access,
"bar.*.?",
);
try testContext(
\\if (bar.*.?.<cursor>field == foo) {
,
.field_access,
"bar.*.?.f",
);
try testContext(
\\if (bar[0].<cursor>field == foo) {
,
.field_access,
"bar[0].f",
);
try testContext(
\\if (bar.<cursor>@"field" == foo) {
,
.field_access,
"bar.@\"",
);
try testContext(
\\if (bar.@"fie<cursor>ld" == foo) {
,
.field_access,
"bar.@\"fiel",
);
try testContext(
\\if (bar.@"field"<cursor> == foo) {
,
.field_access,
"bar.@\"field\"",
);
try testContext(
\\const arr = std.ArrayList(SomeStruct(a, b, c, d)).<cursor>init(allocator);
,
.field_access,
"std.ArrayList(SomeStruct(a, b, c, d)).i",
); );
try testContext( try testContext(
\\const arr = std.ArrayList(SomeStruct(a, b, c, d)).in<cursor>it(allocator); \\const arr = std.ArrayList(SomeStruct(a, b, c, d)).in<cursor>it(allocator);
, ,
.field_access, .field_access,
"std.ArrayList(SomeStruct(a, b, c, d)).init", "std.ArrayList(SomeStruct(a, b, c, d)).ini",
); );
try testContext( try testContext(
\\fn foo() !Foo.b<cursor> { \\const arr = std.ArrayList(SomeStruct(a, b, c, d)).init<cursor>(allocator);
,
.field_access,
"std.ArrayList(SomeStruct(a, b, c, d)).init",
);
try testContext(
\\fn foo() !Foo.<cursor>bar {
, ,
.field_access, .field_access,
"Foo.b", "Foo.b",
); );
// TODO fix failing test! try testContext(
\\fn foo() !Foo.ba<cursor>r {
,
.field_access,
"Foo.bar",
);
try testContext(
\\fn foo() !Foo.bar<cursor> {
,
.field_access,
"Foo.bar",
);
// TODO fix failing tests
// try testContext( // try testContext(
// \\fn foo() Foo.b<cursor>!void { // \\fn foo() Foo.<cursor>bar!void {
// , // ,
// .field_access, // .field_access,
// "Foo.b", // "Foo.b",
// ); // );
// try testContext(
// \\fn foo() Foo.ba<cursor>r!void {
// ,
// .field_access,
// "Foo.bar",
// );
// try testContext(
// \\fn foo() Foo.bar<cursor>!void {
// ,
// .field_access,
// "Foo.bar",
// );
} }
test "position context - builtin" { test "position context - builtin" {
try testContext(
\\var foo = <cursor>@
,
.empty,
null,
);
try testContext(
\\var foo = <cursor>@intC(u32, 5);
,
.builtin,
"@i",
);
try testContext(
\\var foo = @<cursor>intC(u32, 5);
,
.builtin,
"@i",
);
try testContext(
\\var foo = @int<cursor>C(u32, 5);
,
.builtin,
"@intC",
);
try testContext( try testContext(
\\var foo = @intC<cursor>(u32, 5); \\var foo = @intC<cursor>(u32, 5);
, ,
.builtin, .builtin,
"@intC", "@intC",
); );
try testContext(
\\fn foo() void { <cursor>@setRuntime(false); };
,
.builtin,
"@s",
);
try testContext(
\\fn foo() void { @<cursor>setRuntime(false); };
,
.builtin,
"@s",
);
try testContext(
\\fn foo() void { @set<cursor>Runtime(false); };
,
.builtin,
"@setR",
);
try testContext( try testContext(
\\fn foo() void { @setRuntime<cursor>(false); }; \\fn foo() void { @setRuntime<cursor>(false); };
, ,
@ -118,17 +334,29 @@ test "position context - comment" {
} }
test "position context - import/embedfile string literal" { test "position context - import/embedfile string literal" {
try testContext(
\\const std = @import("s<cursor>t");
,
.import_string_literal,
"\"st", // maybe report just "st"
);
try testContext( try testContext(
\\const std = @import("st<cursor>"); \\const std = @import("st<cursor>");
, ,
.import_string_literal, .import_string_literal,
"\"st\"", // maybe report just "st" "\"st", // maybe report just "st"
); );
try testContext( try testContext(
\\const std = @embedFile("file.<cursor>"); \\const std = @embedFile("file.<cursor>");
, ,
.embedfile_string_literal, .embedfile_string_literal,
"\"file.\"", // maybe report just "file." "\"file.", // maybe report just "file."
);
try testContext(
\\const std = @embedFile("file<cursor>.");
,
.embedfile_string_literal,
"\"file", // maybe report just "file."
); );
} }
@ -137,29 +365,49 @@ test "position context - string literal" {
\\var foo = "he<cursor>llo world!"; \\var foo = "he<cursor>llo world!";
, ,
.string_literal, .string_literal,
"\"hello world!\"", // maybe report just "hello world!" "\"hel", // maybe report just "he"
); );
try testContext( try testContext(
\\var foo = \\hello<cursor>; \\var foo = \\hell<cursor>o;
, ,
.string_literal, .string_literal,
"\\\\hello;", // maybe report just "hello;" "\\\\hello", // maybe report just "hello;"
); );
} }
test "position context - global error set" { test "position context - global error set" {
// TODO why is this a .var_access instead of a .global_error_set?
// try testContext(
// \\fn foo() <cursor>error!void {
// ,
// .global_error_set,
// null,
// );
try testContext(
\\fn foo() erro<cursor>r!void {
,
.global_error_set,
null,
);
try testContext( try testContext(
\\fn foo() error<cursor>!void { \\fn foo() error<cursor>!void {
, ,
.global_error_set, .global_error_set,
null, null,
); );
try testContext(
\\fn foo() error<cursor>.!void {
,
.global_error_set,
null,
);
try testContext( try testContext(
\\fn foo() error.<cursor>!void { \\fn foo() error.<cursor>!void {
, ,
.global_error_set, .global_error_set,
null, null,
); );
// TODO this should probably also be .global_error_set // TODO this should probably also be .global_error_set
// try testContext( // try testContext(
// \\fn foo() error{<cursor>}!void { // \\fn foo() error{<cursor>}!void {
@ -176,12 +424,30 @@ test "position context - global error set" {
} }
test "position context - enum literal" { test "position context - enum literal" {
try testContext(
\\var foo = .<cursor>tag;
,
.enum_literal,
null,
);
try testContext(
\\var foo = .ta<cursor>g;
,
.enum_literal,
null,
);
try testContext( try testContext(
\\var foo = .tag<cursor>; \\var foo = .tag<cursor>;
, ,
.enum_literal, .enum_literal,
null, null,
); );
try testContext(
\\var foo = <cursor>.;
,
.empty,
null,
);
try testContext( try testContext(
\\var foo = .<cursor>; \\var foo = .<cursor>;
, ,
@ -191,6 +457,24 @@ test "position context - enum literal" {
} }
test "position context - label" { test "position context - label" {
try testContext(
\\var foo = blk: { break <cursor>:blk null };
,
.pre_label,
null,
);
try testContext(
\\var foo = blk: { break :<cursor>blk null };
,
.label,
null,
);
try testContext(
\\var foo = blk: { break :bl<cursor>k null };
,
.label,
null,
);
try testContext( try testContext(
\\var foo = blk: { break :blk<cursor> null }; \\var foo = blk: { break :blk<cursor> null };
, ,
@ -206,12 +490,6 @@ test "position context - empty" {
.empty, .empty,
null, null,
); );
try testContext(
\\<cursor>const foo = struct {};
,
.empty,
null,
);
try testContext( try testContext(
\\try foo(arg, slice[<cursor>]); \\try foo(arg, slice[<cursor>]);
, ,
@ -237,7 +515,7 @@ fn testContext(line: []const u8, tag: std.meta.Tag(analysis.PositionContext), ma
const final_line = try std.mem.concat(allocator, u8, &.{ line[0..cursor_idx], line[cursor_idx + "<cursor>".len ..] }); const final_line = try std.mem.concat(allocator, u8, &.{ line[0..cursor_idx], line[cursor_idx + "<cursor>".len ..] });
defer allocator.free(final_line); defer allocator.free(final_line);
const ctx = try analysis.getPositionContext(allocator, final_line, cursor_idx); const ctx = try analysis.getPositionContext(allocator, final_line, cursor_idx, true);
if (std.meta.activeTag(ctx) != tag) { if (std.meta.activeTag(ctx) != tag) {
std.debug.print("Expected tag `{s}`, got `{s}`\n", .{ @tagName(tag), @tagName(std.meta.activeTag(ctx)) }); std.debug.print("Expected tag `{s}`, got `{s}`\n", .{ @tagName(tag), @tagName(std.meta.activeTag(ctx)) });