diff --git a/src/analysis.zig b/src/analysis.zig
index cf92e92..7fdd9e5 100644
--- a/src/analysis.zig
+++ b/src/analysis.zig
@@ -1553,9 +1553,15 @@ fn tokenLocAppend(prev: offsets.Loc, token: std.zig.Token) offsets.Loc {
};
}
+/// 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,
+/// 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.
pub fn getPositionContext(allocator: std.mem.Allocator, text: []const u8, doc_index: usize) !PositionContext {
- const line_loc = offsets.lineLocUntilIndex(text, doc_index);
+ const line_loc = offsets.lineLocAtIndex(text, doc_index);
const line = offsets.locToSlice(text, line_loc);
+ const prev_char = if (doc_index > 0) text[doc_index - 1] else 0;
const is_comment = std.mem.startsWith(u8, std.mem.trimLeft(u8, line, " \t"), "//");
if (is_comment) return .comment;
@@ -1576,10 +1582,16 @@ pub fn getPositionContext(allocator: std.mem.Allocator, text: []const u8, doc_in
while (true) {
const tok = tokenizer.next();
// Early exits.
+ if (tok.loc.start > doc_index) break;
+ if (tok.loc.start == doc_index) {
+ // Tie-breaking, the curosr is exactly between two tokens, and
+ // `tok` is the latter of the two.
+ if (tok.tag != .identifier) break;
+ }
switch (tok.tag) {
.invalid => {
// Single '@' do not return a builtin token so we check this on our own.
- if (line[line.len - 1] == '@') {
+ if (prev_char == '@') {
return PositionContext{
.builtin = .{
.start = line_loc.end - 1,
@@ -1685,7 +1697,7 @@ pub fn getPositionContext(allocator: std.mem.Allocator, text: []const u8, doc_in
.label => |filled| {
// We need to check this because the state could be a filled
// label if only a space follows it
- if (!filled or line[line.len - 1] != ' ') {
+ if (!filled or prev_char != ' ') {
return state.ctx;
}
},
diff --git a/tests/lsp_features/definition.zig b/tests/lsp_features/definition.zig
index caed193..ea4e2b0 100644
--- a/tests/lsp_features/definition.zig
+++ b/tests/lsp_features/definition.zig
@@ -28,15 +28,10 @@ test "definition - cursor is at the end of an identifier" {
}
test "definition - cursor is at the start of an identifier" {
- testDefinition(
+ try testDefinition(
\\fn main() void { <>foo(); }
\\fn foo() void {}
- ) catch |err| switch (err) {
- error.UnresolvedDefinition => {
- // TODO: #891
- },
- else => return err,
- };
+ );
}
fn testDefinition(source: []const u8) !void {
diff --git a/tests/utility/position_context.zig b/tests/utility/position_context.zig
index 2735061..4fd778a 100644
--- a/tests/utility/position_context.zig
+++ b/tests/utility/position_context.zig
@@ -12,7 +12,7 @@ test "position context - var access" {
\\const this_var = identifier;
,
.var_access,
- "id",
+ "identifier",
);
try testContext(
\\const this_var = identifier;
@@ -40,37 +40,37 @@ test "position context - field access" {
\\if (foo.field == foo) {
,
.field_access,
- "foo.",
+ "foo.field",
);
try testContext(
\\if (foo.member.field == foo) {
,
.field_access,
- "foo.member.",
+ "foo.member.field",
);
try testContext(
\\if (foo.*.?.field == foo) {
,
.field_access,
- "foo.*.?.",
+ "foo.*.?.field",
);
try testContext(
\\if (foo[0].field == foo) {
,
.field_access,
- "foo[0].",
+ "foo[0].field",
);
try testContext(
\\if (foo.@"field" == foo) {
,
.field_access,
- "foo.",
+ "foo.@\"field\"",
);
try testContext(
\\const arr = std.ArrayList(SomeStruct(a, b, c, d)).init(allocator);
,
.field_access,
- "std.ArrayList(SomeStruct(a, b, c, d)).in",
+ "std.ArrayList(SomeStruct(a, b, c, d)).init",
);
try testContext(
\\fn foo() !Foo.b {
@@ -122,13 +122,13 @@ test "position context - import/embedfile string literal" {
\\const std = @import("st");
,
.import_string_literal,
- "\"st", // maybe report just "st"
+ "\"st\"", // maybe report just "st"
);
try testContext(
\\const std = @embedFile("file.");
,
.embedfile_string_literal,
- "\"file.", // maybe report just "file."
+ "\"file.\"", // maybe report just "file."
);
}
@@ -137,13 +137,13 @@ test "position context - string literal" {
\\var foo = "hello world!";
,
.string_literal,
- "\"he", // maybe report just "he"
+ "\"hello world!\"", // maybe report just "hello world!"
);
try testContext(
\\var foo = \\hello;
,
.string_literal,
- "\\\\hello", // maybe report just "hello"
+ "\\\\hello;", // maybe report just "hello;"
);
}
@@ -237,7 +237,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 + "".len ..] });
defer allocator.free(final_line);
- const ctx = try analysis.getPositionContext(allocator, line, cursor_idx);
+ const ctx = try analysis.getPositionContext(allocator, final_line, cursor_idx);
if (std.meta.activeTag(ctx) != tag) {
std.debug.print("Expected tag `{s}`, got `{s}`\n", .{ @tagName(tag), @tagName(std.meta.activeTag(ctx)) });
@@ -253,7 +253,7 @@ fn testContext(line: []const u8, tag: std.meta.Tag(analysis.PositionContext), ma
const expected_range = maybe_range orelse {
std.debug.print("Expected null range, got `{s}`\n", .{
- line[actual_loc.start..actual_loc.end],
+ final_line[actual_loc.start..actual_loc.end],
});
return error.DifferentRange;
};
@@ -263,8 +263,8 @@ fn testContext(line: []const u8, tag: std.meta.Tag(analysis.PositionContext), ma
if (expected_range_start != actual_loc.start or expected_range_end != actual_loc.end) {
std.debug.print("Expected range `{s}` ({}..{}), got `{s}` ({}..{})\n", .{
- line[expected_range_start..expected_range_end], expected_range_start, expected_range_end,
- line[actual_loc.start..actual_loc.end], actual_loc.start, actual_loc.end,
+ final_line[expected_range_start..expected_range_end], expected_range_start, expected_range_end,
+ final_line[actual_loc.start..actual_loc.end], actual_loc.start, actual_loc.end,
});
return error.DifferentRange;
}