diff --git a/src/features/code_actions.zig b/src/features/code_actions.zig index d01ad553b..99885ce5f 100644 --- a/src/features/code_actions.zig +++ b/src/features/code_actions.zig @@ -8,6 +8,7 @@ const DocumentStore = @import("../DocumentStore.zig"); const DocumentScope = @import("../DocumentScope.zig"); const Analyser = @import("../analysis.zig"); const ast = @import("../ast.zig"); +const diff = @import("../diff.zig"); const types = @import("lsp").types; const offsets = @import("../offsets.zig"); const tracy = @import("tracy"); @@ -682,6 +683,9 @@ fn handleUnorganizedImport(builder: *Builder) error{OutOfMemory}!void { } } + const resulting_source = try diff.applyTextEdits(builder.arena, tree.source, edits.items, builder.offset_encoding); + if (std.mem.eql(u8, resulting_source, tree.source)) return; + const workspace_edit = try builder.createWorkspaceEdit(edits.items); try builder.actions.append(builder.arena, .{ diff --git a/tests/lsp_features/code_actions.zig b/tests/lsp_features/code_actions.zig index 0d8e58353..a5504d879 100644 --- a/tests/lsp_features/code_actions.zig +++ b/tests/lsp_features/code_actions.zig @@ -762,6 +762,31 @@ test "organize imports - edge cases" { ); } +test "organize imports - no action when already organized" { + // Single import plus the trailing blank line that organize would normalize to. + // https://github.com/zigtools/zls/issues/2523 + try testOrganizeImportsNoAction( + \\const a = @import("a"); + \\ + \\ + ); + // Sorted imports from different kinds with the expected group separator and trailing blank line. + try testOrganizeImportsNoAction( + \\const std = @import("std"); + \\ + \\const abc = @import("abc.zig"); + \\ + \\ + ); + // Imports already sorted, separated from following decls by the expected blank line. + try testOrganizeImportsNoAction( + \\const std = @import("std"); + \\ + \\fn main() void {} + \\ + ); +} + test "convert multiline string literal" { try testConvertString( \\const foo = \\Hello @@ -960,6 +985,42 @@ fn testOrganizeImports(before: []const u8, after: []const u8) !void { try testDiagnostic(before, after, .{ .filter_kind = .@"source.organizeImports" }); } +fn testOrganizeImportsNoAction(source: []const u8) !void { + var ctx: Context = try .init(); + defer ctx.deinit(); + + var phr = try helper.collectClearPlaceholders(allocator, source); + defer phr.deinit(allocator); + const clean_source = phr.new_source; + + const range: types.Range = .{ + .start = .{ .line = 0, .character = 0 }, + .end = offsets.indexToPosition(clean_source, clean_source.len, ctx.server.offset_encoding), + }; + + const uri = try ctx.addDocument(.{ .source = clean_source }); + + const params: types.CodeAction.Params = .{ + .textDocument = .{ .uri = uri.raw }, + .range = range, + .context = .{ + .diagnostics = &.{}, + .only = &.{.@"source.organizeImports"}, + }, + }; + + @setEvalBranchQuota(5000); + const response = try ctx.server.sendRequestSync(ctx.arena.allocator(), "textDocument/codeAction", params) orelse return; + + for (response) |action| { + const kind = action.code_action.kind orelse continue; + if (kind == .@"source.organizeImports") { + std.debug.print("expected no organize-imports code action for already-organized source, got: {s}\n", .{action.code_action.title}); + return error.UnexpectedOrganizeImportsAction; + } + } +} + fn testConvertString(before: []const u8, after: []const u8) !void { try testDiagnostic(before, after, .{ .filter_kind = .refactor }); }