diff --git a/src/analysis.zig b/src/analysis.zig index 94909a645..a51b06c9b 100644 --- a/src/analysis.zig +++ b/src/analysis.zig @@ -4754,7 +4754,7 @@ pub const PositionContext = union(enum) { /// XXX: Internal use only, currently points to the loc of the first l_paren parens_expr: offsets.Loc, keyword: Ast.TokenIndex, - error_access, + error_access: offsets.Loc, comment, other, empty, @@ -5100,7 +5100,7 @@ pub fn getPositionContext( stack.pop(curr_ctx.scope == .braces); continue; }, - .keyword_error => .error_access, + .keyword_error => .{ .error_access = tok.loc }, .number_literal => { if (tok.loc.start <= source_index and tok.loc.end >= source_index) { return .{ .number_literal = tok.loc }; diff --git a/src/features/completions.zig b/src/features/completions.zig index 2eadc383b..a019c4729 100644 --- a/src/features/completions.zig +++ b/src/features/completions.zig @@ -731,6 +731,50 @@ fn completeDot(builder: *Builder, loc: offsets.Loc) Analyser.Error!void { } } +fn completeError(builder: *Builder, loc: offsets.Loc) Analyser.Error!void { + const tracy_zone = tracy.trace(@src()); + defer tracy_zone.end(); + + const tree = &builder.orig_handle.tree; + + const err_token_index = offsets.sourceIndexToTokenIndex(tree, loc.start).pickPreferred(&.{.period}, tree) orelse return; + if (err_token_index < 2) return; + const dot_token_index = err_token_index + 1; + + const nodes = try ast.nodesOverlappingIndexIncludingParseErrors(builder.arena, tree, loc.start); + const dot_context = getSwitchOrStructInitContext(tree, dot_token_index, nodes) orelse return; + const used_members_set = try collectUsedMembersSet(builder, dot_context.likely, dot_token_index); + const containers = try collectContainerNodes(builder, builder.orig_handle, dot_context); + for (containers) |container| { + try collectErrorSetNames(builder, container, used_members_set); + } +} + +fn collectErrorSetNames( + builder: *Builder, + container: Analyser.Type, + omit_members: std.BufSet, +) Analyser.Error!void { + const ip = builder.analyser.ip; + const key = switch (container.data) { + .ip_index => |info| ip.indexToKey(info.type), + else => return, + }; + if (key != .error_set_type) return; + const names = key.error_set_type.names; + for (0..names.len) |idx| { + const str = names.at(@intCast(idx), ip); + const name = try std.fmt.allocPrint(builder.arena, "{f}", .{str.fmt(ip.io, &ip.string_pool)}); + if (omit_members.contains(name)) continue; + try builder.completions.append(builder.arena, .{ + .label = try std.fmt.allocPrint(builder.arena, "error.{s}", .{name}), + .filterText = name, + .insertText = name, + .kind = .Constant, + }); + } +} + /// Asserts that `pos_context` is one of the following: /// - `.import_string_literal` /// - `.cinclude_string_literal` @@ -953,6 +997,7 @@ pub fn completionAtIndex( .var_access, .empty => try completeGlobal(&builder), .field_access => |loc| try completeFieldAccess(&builder, loc), .enum_literal => |loc| try completeDot(&builder, loc), + .error_access => |loc| try completeError(&builder, loc), .label_access, .label_decl => try completeLabel(&builder), .import_string_literal, .cinclude_string_literal, diff --git a/tests/lsp_features/completion.zig b/tests/lsp_features/completion.zig index c1cdea388..9044bbf95 100644 --- a/tests/lsp_features/completion.zig +++ b/tests/lsp_features/completion.zig @@ -1925,6 +1925,125 @@ test "switch cases" { }); } +test "switch on error set - all values are suggested" { + try testCompletion( + \\const err: error{E1, E2} = undefined; + \\switch(err) { + \\ error. + \\} + , &.{ + .{ .label = "error.E1", .kind = .Constant }, + .{ .label = "error.E2", .kind = .Constant }, + }); +} + +test "switch on error set - already used values are not suggested" { + try testCompletion( + \\const err: error{E1, E2} = undefined; + \\switch(err) { + \\ error.E1 => {}, + \\ error. + \\} + , &.{ + .{ .label = "error.E2", .kind = .Constant }, + }); +} + +test "switch on error set - error unions get all values" { + try testCompletion( + \\const Err2 = error{F1, F2}; + \\const Err = error{E1, E2} || Err2; + \\const err: Err = undefined; + \\switch(err) { + \\ error. + \\} + , &.{ + .{ .label = "error.E1", .kind = .Constant }, + .{ .label = "error.E2", .kind = .Constant }, + .{ .label = "error.F1", .kind = .Constant }, + .{ .label = "error.F2", .kind = .Constant }, + }); +} + +test "switch on error set - text edits result" { + try testCompletionTextEdit(.{ + .source = + \\const err: error{E1, E2} = undefined; + \\switch(err) { + \\ error. + \\} + , + .label = "error.E1", + .expected_insert_line = " error.E1", + .expected_replace_line = " error.E1", + .enable_snippets = false, + }); +} +test "switch on error set - insert/replace text edits" { + try testCompletionTextEdit(.{ + .source = + \\const err: error{Err1, Err2} = undefined; + \\switch(err) { + \\ error.E0 + \\} + , + .label = "error.Err1", + .expected_insert_line = " error.Err10", + .expected_replace_line = " error.Err1", + .enable_snippets = false, + }); +} + +test "switch on error set - completion inside catch block works" { + try testCompletion( + \\fn idk() error{ E1, E2 }!void {} + \\test { + \\ idk() catch |err| { + \\ switch (err) { + \\ error. + \\ } + \\ }; + \\} + , &.{ + .{ .label = "error.E1", .kind = .Constant }, + .{ .label = "error.E2", .kind = .Constant }, + }); +} + +test "switch on error set - completion inside catch statement" { + if (true) return error.SkipZigTest; // TODO un-skip after https://github.com/zigtools/zls/issues/2341 and/or https://github.com/zigtools/zls/issues/1112 + try testCompletion( + \\fn idk() error{ E1, E2 }!void {} + \\test { + \\ idk() catch |err| switch (err) { + \\ error. + \\ }; + \\} + , &.{ + .{ .label = "error.E1", .kind = .Constant }, + .{ .label = "error.E2", .kind = .Constant }, + }); +} + +test "switch on error set - Works in a function of a container" { + if (true) return error.SkipZigTest; // TODO un-skip after https://github.com/zigtools/zls/issues/1535 + try testCompletion( + \\fn idk() error{ E1, E2 }!void {} + \\pub const Manager = struct { + \\ pub fn testIt() void { + \\ _ = idk() catch |err| { + \\ switch(err) { + \\ error. + \\ } + \\ }; + \\ } + \\}; + , &.{ + .{ .label = "error.E1", .kind = .Constant }, + .{ .label = "error.E2", .kind = .Constant }, + }); +} + test "error set" { try testCompletion( \\const E = error {