Add language format and code action edits
This commit is contained in:
@@ -392,7 +392,8 @@ Design-only and needing future implementation slices:
|
|||||||
- parameter navigation in function calls;
|
- parameter navigation in function calls;
|
||||||
- compact-first hover/signature cards with expandable scroll/search panels;
|
- compact-first hover/signature cards with expandable scroll/search panels;
|
||||||
- full provider arbitration for multiple LSP/tool sources;
|
- full provider arbitration for multiple LSP/tool sources;
|
||||||
- source-profile format-on-save policy and one-shot save-without-format;
|
- source-profile-backed format, organize-imports, code-action edit application;
|
||||||
|
- format-on-save policy and one-shot save-without-format;
|
||||||
- contextual rails for all prefixes listed in this document;
|
- contextual rails for all prefixes listed in this document;
|
||||||
- Insert pending-space rail, including `Space` pause then `n` to Normal;
|
- Insert pending-space rail, including `Space` pause then `n` to Normal;
|
||||||
- counts/repetition grammar with direct digit and mobile repeat-rail paths;
|
- counts/repetition grammar with direct digit and mobile repeat-rail paths;
|
||||||
|
|||||||
+9
-1
@@ -30,6 +30,10 @@ pub const Action = union(enum) {
|
|||||||
hover,
|
hover,
|
||||||
signature,
|
signature,
|
||||||
expand_hover,
|
expand_hover,
|
||||||
|
language_format,
|
||||||
|
language_format_policy,
|
||||||
|
language_organize_imports,
|
||||||
|
language_code_actions,
|
||||||
diagnostics_open,
|
diagnostics_open,
|
||||||
diagnostics_next,
|
diagnostics_next,
|
||||||
diagnostics_previous,
|
diagnostics_previous,
|
||||||
@@ -220,7 +224,11 @@ pub const Leader = struct {
|
|||||||
self.mode = .idle;
|
self.mode = .idle;
|
||||||
if (std.mem.eql(u8, text, "h")) return .hover;
|
if (std.mem.eql(u8, text, "h")) return .hover;
|
||||||
if (std.mem.eql(u8, text, "s")) return .signature;
|
if (std.mem.eql(u8, text, "s")) return .signature;
|
||||||
if (std.mem.eql(u8, text, "o")) return .expand_hover;
|
if (std.mem.eql(u8, text, "o")) return .language_organize_imports;
|
||||||
|
if (std.mem.eql(u8, text, "f")) return .language_format;
|
||||||
|
if (std.mem.eql(u8, text, "F")) return .language_format_policy;
|
||||||
|
if (std.mem.eql(u8, text, "w")) return .language_format_policy;
|
||||||
|
if (std.mem.eql(u8, text, "a")) return .language_code_actions;
|
||||||
self.message = "language action is not built in this profile yet";
|
self.message = "language action is not built in this profile yet";
|
||||||
return .{ .not_built = .lsp };
|
return .{ .not_built = .lsp };
|
||||||
},
|
},
|
||||||
|
|||||||
+91
@@ -23,6 +23,8 @@ pub const Error = error{
|
|||||||
InvalidProvider,
|
InvalidProvider,
|
||||||
MissingProvider,
|
MissingProvider,
|
||||||
AmbiguousProvider,
|
AmbiguousProvider,
|
||||||
|
StaleEdit,
|
||||||
|
OverlappingEdit,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const ProviderKind = enum {
|
pub const ProviderKind = enum {
|
||||||
@@ -209,6 +211,95 @@ pub fn compactSignatureCardAlloc(allocator: std.mem.Allocator, row: []const u8)
|
|||||||
return std.fmt.allocPrint(allocator, "[{s}] {s}", .{ provider, body });
|
return std.fmt.allocPrint(allocator, "[{s}] {s}", .{ provider, body });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const ProviderEdit = struct {
|
||||||
|
provider: []const u8,
|
||||||
|
capability: ProviderCapability,
|
||||||
|
version: u64,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
replacement: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn providerCapabilityFromName(name: []const u8) !ProviderCapability {
|
||||||
|
if (std.mem.eql(u8, name, "format")) return .format;
|
||||||
|
if (std.mem.eql(u8, name, "organize_imports")) return .organize_imports;
|
||||||
|
if (std.mem.eql(u8, name, "code_action")) return .code_action;
|
||||||
|
return Error.InvalidProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn editRowAlloc(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
provider: []const u8,
|
||||||
|
capability: ProviderCapability,
|
||||||
|
version: u64,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
replacement: []const u8,
|
||||||
|
) ![]u8 {
|
||||||
|
if (provider.len == 0 or start > end) return Error.InvalidEdit;
|
||||||
|
return std.fmt.allocPrint(allocator, "provider:{s}:edit:{s}:{d}:{d}:{d}:{s}", .{ provider, @tagName(capability), version, start, end, replacement });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parseProviderEditRow(row: []const u8) !ProviderEdit {
|
||||||
|
const provider = try providerIdFromRow(row);
|
||||||
|
const payload = try providerPayloadFromRow(row);
|
||||||
|
if (!std.mem.startsWith(u8, payload, "edit:")) return Error.InvalidEdit;
|
||||||
|
var parts = std.mem.splitScalar(u8, payload, ':');
|
||||||
|
_ = parts.next() orelse return Error.InvalidEdit;
|
||||||
|
const capability_name = parts.next() orelse return Error.InvalidEdit;
|
||||||
|
const version_text = parts.next() orelse return Error.InvalidEdit;
|
||||||
|
const start_text = parts.next() orelse return Error.InvalidEdit;
|
||||||
|
const end_text = parts.next() orelse return Error.InvalidEdit;
|
||||||
|
const replacement = parts.rest();
|
||||||
|
const start = std.fmt.parseUnsigned(usize, start_text, 10) catch return Error.InvalidEdit;
|
||||||
|
const end = std.fmt.parseUnsigned(usize, end_text, 10) catch return Error.InvalidEdit;
|
||||||
|
if (start > end) return Error.InvalidEdit;
|
||||||
|
return .{
|
||||||
|
.provider = provider,
|
||||||
|
.capability = try providerCapabilityFromName(capability_name),
|
||||||
|
.version = std.fmt.parseUnsigned(u64, version_text, 10) catch return Error.InvalidEdit,
|
||||||
|
.start = start,
|
||||||
|
.end = end,
|
||||||
|
.replacement = replacement,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validateEditBatch(rows: []const []const u8, document_version: u64, document_len: usize) !void {
|
||||||
|
var previous_start: ?usize = null;
|
||||||
|
var previous_end: ?usize = null;
|
||||||
|
for (rows) |row| {
|
||||||
|
const edit = try parseProviderEditRow(row);
|
||||||
|
if (edit.version != document_version) return Error.StaleEdit;
|
||||||
|
if (edit.end > document_len) return Error.InvalidEdit;
|
||||||
|
if (previous_start) |start| {
|
||||||
|
if (!(edit.end <= start or edit.start >= previous_end.?)) return Error.OverlappingEdit;
|
||||||
|
}
|
||||||
|
previous_start = edit.start;
|
||||||
|
previous_end = edit.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn actionPanelRowAlloc(allocator: std.mem.Allocator, row: []const u8) ![]const u8 {
|
||||||
|
const edit = try parseProviderEditRow(row);
|
||||||
|
return std.fmt.allocPrint(allocator, "action:{s}:provider:{s}:scope:{d}-{d}:apply", .{ @tagName(edit.capability), edit.provider, edit.start, edit.end });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn providerFromActionPanelRow(row: []const u8) ![]const u8 {
|
||||||
|
const marker = ":provider:";
|
||||||
|
const start = std.mem.indexOf(u8, row, marker) orelse return Error.InvalidProvider;
|
||||||
|
const rest = row[start + marker.len ..];
|
||||||
|
const end = std.mem.indexOfScalar(u8, rest, ':') orelse return Error.InvalidProvider;
|
||||||
|
if (end == 0) return Error.InvalidProvider;
|
||||||
|
return rest[0..end];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn capabilityFromActionPanelRow(row: []const u8) !ProviderCapability {
|
||||||
|
if (!std.mem.startsWith(u8, row, "action:")) return Error.InvalidProvider;
|
||||||
|
const rest = row[7..];
|
||||||
|
const end = std.mem.indexOfScalar(u8, rest, ':') orelse return Error.InvalidProvider;
|
||||||
|
return providerCapabilityFromName(rest[0..end]);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn providerPickerRowsAlloc(allocator: std.mem.Allocator, capability: ProviderCapability, providers: []const Provider) ![][]const u8 {
|
pub fn providerPickerRowsAlloc(allocator: std.mem.Allocator, capability: ProviderCapability, providers: []const Provider) ![][]const u8 {
|
||||||
var rows = std.ArrayList([]const u8).empty;
|
var rows = std.ArrayList([]const u8).empty;
|
||||||
errdefer {
|
errdefer {
|
||||||
|
|||||||
+358
@@ -44,6 +44,8 @@ const PanelContext = enum {
|
|||||||
none,
|
none,
|
||||||
file_picker,
|
file_picker,
|
||||||
project_search,
|
project_search,
|
||||||
|
language_provider_picker,
|
||||||
|
code_actions,
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditSnapshot = struct {
|
const EditSnapshot = struct {
|
||||||
@@ -155,6 +157,11 @@ pub const Client = struct {
|
|||||||
hover_rows: std.ArrayList([]u8) = .empty,
|
hover_rows: std.ArrayList([]u8) = .empty,
|
||||||
signature_rows: std.ArrayList([]u8) = .empty,
|
signature_rows: std.ArrayList([]u8) = .empty,
|
||||||
diagnostic_rows: std.ArrayList([]u8) = .empty,
|
diagnostic_rows: std.ArrayList([]u8) = .empty,
|
||||||
|
language_edit_rows: std.ArrayList([]u8) = .empty,
|
||||||
|
format_provider_default: ?[]u8 = null,
|
||||||
|
format_on_save: bool = true,
|
||||||
|
skip_next_save_format: bool = false,
|
||||||
|
pending_language_capability: ?lsp_mod.ProviderCapability = null,
|
||||||
diagnostic_filter: ?[]u8 = null,
|
diagnostic_filter: ?[]u8 = null,
|
||||||
diagnostic_index: usize = 0,
|
diagnostic_index: usize = 0,
|
||||||
diagnostic_selected: bool = false,
|
diagnostic_selected: bool = false,
|
||||||
@@ -192,6 +199,8 @@ pub const Client = struct {
|
|||||||
self.freeOwnedClientRows(&self.hover_rows);
|
self.freeOwnedClientRows(&self.hover_rows);
|
||||||
self.freeOwnedClientRows(&self.signature_rows);
|
self.freeOwnedClientRows(&self.signature_rows);
|
||||||
self.freeOwnedClientRows(&self.diagnostic_rows);
|
self.freeOwnedClientRows(&self.diagnostic_rows);
|
||||||
|
self.freeOwnedClientRows(&self.language_edit_rows);
|
||||||
|
if (self.format_provider_default) |provider| self.allocator.free(provider);
|
||||||
if (self.diagnostic_filter) |filter| self.allocator.free(filter);
|
if (self.diagnostic_filter) |filter| self.allocator.free(filter);
|
||||||
self.freeSnapshotStack(&self.undo_stack);
|
self.freeSnapshotStack(&self.undo_stack);
|
||||||
self.freeSnapshotStack(&self.redo_stack);
|
self.freeSnapshotStack(&self.redo_stack);
|
||||||
@@ -216,6 +225,10 @@ pub const Client = struct {
|
|||||||
if (std.mem.startsWith(u8, line, "lsp_signature_fixture ")) return self.addSignatureFixture(line[22..]);
|
if (std.mem.startsWith(u8, line, "lsp_signature_fixture ")) return self.addSignatureFixture(line[22..]);
|
||||||
if (std.mem.startsWith(u8, line, "diagnostic_fixture ")) return self.addDiagnosticFixture(line[19..]);
|
if (std.mem.startsWith(u8, line, "diagnostic_fixture ")) return self.addDiagnosticFixture(line[19..]);
|
||||||
if (std.mem.startsWith(u8, line, "diagnostic_filter ")) return self.setDiagnosticFilter(line[18..]);
|
if (std.mem.startsWith(u8, line, "diagnostic_filter ")) return self.setDiagnosticFilter(line[18..]);
|
||||||
|
if (std.mem.startsWith(u8, line, "language_edit ")) return self.addLanguageEditFixture(line[14..]);
|
||||||
|
if (std.mem.startsWith(u8, line, "language_default_format ")) return self.setFormatProviderDefault(line[24..]);
|
||||||
|
if (std.mem.startsWith(u8, line, "format_on_save ")) return self.setFormatOnSavePolicy(line[15..]);
|
||||||
|
if (std.mem.eql(u8, line, "save_without_format")) return self.saveWithoutFormat();
|
||||||
if (std.mem.startsWith(u8, line, "current_file ")) return self.setCurrentPath(line[13..]);
|
if (std.mem.startsWith(u8, line, "current_file ")) return self.setCurrentPath(line[13..]);
|
||||||
if (std.mem.startsWith(u8, line, "job_profile ")) return self.openJobProfileByName(line[12..]);
|
if (std.mem.startsWith(u8, line, "job_profile ")) return self.openJobProfileByName(line[12..]);
|
||||||
if (std.mem.startsWith(u8, line, "job_missing_tool ")) return self.openMissingToolJob(line[17..]);
|
if (std.mem.startsWith(u8, line, "job_missing_tool ")) return self.openMissingToolJob(line[17..]);
|
||||||
@@ -1205,6 +1218,8 @@ pub const Client = struct {
|
|||||||
switch (self.panel_context) {
|
switch (self.panel_context) {
|
||||||
.file_picker => try self.openRepoPathFromPanel(owned),
|
.file_picker => try self.openRepoPathFromPanel(owned),
|
||||||
.project_search => try self.openProjectSearchResult(owned),
|
.project_search => try self.openProjectSearchResult(owned),
|
||||||
|
.language_provider_picker => try self.applyLanguageProviderChoice(owned),
|
||||||
|
.code_actions => try self.applyCodeActionRow(owned),
|
||||||
.none => self.message = "panel item has no action",
|
.none => self.message = "panel item has no action",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1310,6 +1325,198 @@ pub const Client = struct {
|
|||||||
try self.openListRows("hover", rows, .none);
|
try self.openListRows("hover", rows, .none);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn addLanguageEditFixture(self: *Client, payload: []const u8) !void {
|
||||||
|
var parts = std.mem.splitScalar(u8, payload, '|');
|
||||||
|
const provider = parts.next() orelse return Error.ProtocolRejected;
|
||||||
|
const capability_name = parts.next() orelse return Error.ProtocolRejected;
|
||||||
|
const version_text = parts.next() orelse return Error.ProtocolRejected;
|
||||||
|
const start_text = parts.next() orelse return Error.ProtocolRejected;
|
||||||
|
const end_text = parts.next() orelse return Error.ProtocolRejected;
|
||||||
|
const replacement = parts.rest();
|
||||||
|
const capability = lsp_mod.providerCapabilityFromName(capability_name) catch return Error.ProtocolRejected;
|
||||||
|
const version = std.fmt.parseUnsigned(u64, version_text, 10) catch return Error.ProtocolRejected;
|
||||||
|
const start = std.fmt.parseUnsigned(usize, start_text, 10) catch return Error.ProtocolRejected;
|
||||||
|
const end = std.fmt.parseUnsigned(usize, end_text, 10) catch return Error.ProtocolRejected;
|
||||||
|
const row = lsp_mod.editRowAlloc(self.allocator, provider, capability, version, start, end, replacement) catch return Error.ProtocolRejected;
|
||||||
|
try self.language_edit_rows.append(self.allocator, row);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setFormatProviderDefault(self: *Client, provider: []const u8) !void {
|
||||||
|
if (self.format_provider_default) |old| self.allocator.free(old);
|
||||||
|
self.format_provider_default = if (std.mem.eql(u8, provider, "none")) null else try self.allocator.dupe(u8, provider);
|
||||||
|
const message = try std.fmt.allocPrint(self.allocator, "format default:{s}", .{self.format_provider_default orelse "none"});
|
||||||
|
self.setOwnedStatusMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setFormatOnSavePolicy(self: *Client, policy: []const u8) !void {
|
||||||
|
if (std.mem.eql(u8, policy, "on")) self.format_on_save = true else if (std.mem.eql(u8, policy, "off")) self.format_on_save = false else return Error.ProtocolRejected;
|
||||||
|
try self.showFormatPolicy();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn showFormatPolicy(self: *Client) !void {
|
||||||
|
const provider = self.format_provider_default orelse "select";
|
||||||
|
const message = try std.fmt.allocPrint(self.allocator, "format-on-save:{s} provider:{s} one-shot:save_without_format", .{ if (self.format_on_save) "on" else "off", provider });
|
||||||
|
self.setOwnedStatusMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn saveWithoutFormat(self: *Client) !void {
|
||||||
|
self.skip_next_save_format = true;
|
||||||
|
try self.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn formatCurrentBuffer(self: *Client) !void {
|
||||||
|
try self.applyLanguageMutation(.format, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn organizeImports(self: *Client) !void {
|
||||||
|
if (!self.hasLanguageProvider(.organize_imports) and self.hover_rows.items.len != 0) return self.openExpandedHover();
|
||||||
|
try self.applyLanguageMutation(.organize_imports, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hasLanguageProvider(self: *Client, capability: lsp_mod.ProviderCapability) bool {
|
||||||
|
for (self.language_edit_rows.items) |row| {
|
||||||
|
const edit = lsp_mod.parseProviderEditRow(row) catch continue;
|
||||||
|
if (edit.capability == capability) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn openCodeActions(self: *Client) !void {
|
||||||
|
const rows = try self.actionRowsForCapability(.code_action);
|
||||||
|
defer freeOwnedRows(self.allocator, rows);
|
||||||
|
if (rows.len == 0) {
|
||||||
|
self.message = "NoProvider:code_action";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try self.openListRows("code-actions", rows, .code_actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn actionRowsForCapability(self: *Client, capability: lsp_mod.ProviderCapability) ![][]const u8 {
|
||||||
|
var rows = std.ArrayList([]const u8).empty;
|
||||||
|
errdefer freeOwnedRows(self.allocator, rows.items);
|
||||||
|
for (self.language_edit_rows.items) |row| {
|
||||||
|
const edit = lsp_mod.parseProviderEditRow(row) catch continue;
|
||||||
|
if (edit.capability != capability) continue;
|
||||||
|
try rows.append(self.allocator, try lsp_mod.actionPanelRowAlloc(self.allocator, row));
|
||||||
|
}
|
||||||
|
return rows.toOwnedSlice(self.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn applyCodeActionRow(self: *Client, row: []const u8) !void {
|
||||||
|
const provider = lsp_mod.providerFromActionPanelRow(row) catch return Error.ProtocolRejected;
|
||||||
|
const capability = lsp_mod.capabilityFromActionPanelRow(row) catch return Error.ProtocolRejected;
|
||||||
|
try self.applyLanguageMutation(capability, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn applyLanguageProviderChoice(self: *Client, row: []const u8) !void {
|
||||||
|
const provider = lsp_mod.providerFromActionPanelRow(row) catch return Error.ProtocolRejected;
|
||||||
|
const capability = self.pending_language_capability orelse return Error.ProtocolRejected;
|
||||||
|
self.pending_language_capability = null;
|
||||||
|
try self.applyLanguageMutation(capability, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn applyLanguageMutation(self: *Client, capability: lsp_mod.ProviderCapability, forced_provider: ?[]const u8) !void {
|
||||||
|
const provider = try self.chooseLanguageProvider(capability, forced_provider);
|
||||||
|
if (provider == null) return;
|
||||||
|
try self.applyProviderEdits(capability, provider.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chooseLanguageProvider(self: *Client, capability: lsp_mod.ProviderCapability, forced_provider: ?[]const u8) !?[]const u8 {
|
||||||
|
if (forced_provider) |provider| return provider;
|
||||||
|
var providers = std.ArrayList([]const u8).empty;
|
||||||
|
defer providers.deinit(self.allocator);
|
||||||
|
for (self.language_edit_rows.items) |row| {
|
||||||
|
const edit = lsp_mod.parseProviderEditRow(row) catch continue;
|
||||||
|
if (edit.capability != capability) continue;
|
||||||
|
var seen = false;
|
||||||
|
for (providers.items) |provider| {
|
||||||
|
if (std.mem.eql(u8, provider, edit.provider)) seen = true;
|
||||||
|
}
|
||||||
|
if (!seen) try providers.append(self.allocator, edit.provider);
|
||||||
|
}
|
||||||
|
if (providers.items.len == 0) {
|
||||||
|
const message = try std.fmt.allocPrint(self.allocator, "NoProvider:{s}", .{@tagName(capability)});
|
||||||
|
self.setOwnedStatusMessage(message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (capability == .format) {
|
||||||
|
if (self.format_provider_default) |preferred| {
|
||||||
|
for (providers.items) |provider| if (std.mem.eql(u8, provider, preferred)) return provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (providers.items.len == 1) return providers.items[0];
|
||||||
|
var rows = std.ArrayList([]const u8).empty;
|
||||||
|
errdefer freeOwnedRows(self.allocator, rows.items);
|
||||||
|
for (providers.items) |provider| try rows.append(self.allocator, try std.fmt.allocPrint(self.allocator, "action:{s}:provider:{s}:scope:buffer:choose", .{ @tagName(capability), provider }));
|
||||||
|
const owned = try rows.toOwnedSlice(self.allocator);
|
||||||
|
defer freeOwnedRows(self.allocator, owned);
|
||||||
|
self.pending_language_capability = capability;
|
||||||
|
try self.openListRows("language-providers", owned, .language_provider_picker);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditOrder = struct { start: usize, row: []const u8 };
|
||||||
|
|
||||||
|
fn applyProviderEdits(self: *Client, capability: lsp_mod.ProviderCapability, provider: []const u8) !void {
|
||||||
|
const snap = try self.session.snapshot();
|
||||||
|
var matches = std.ArrayList([]const u8).empty;
|
||||||
|
defer matches.deinit(self.allocator);
|
||||||
|
for (self.language_edit_rows.items) |row| {
|
||||||
|
const edit = lsp_mod.parseProviderEditRow(row) catch {
|
||||||
|
self.message = "InvalidEdit";
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (edit.capability == capability and std.mem.eql(u8, edit.provider, provider)) try matches.append(self.allocator, row);
|
||||||
|
}
|
||||||
|
if (matches.items.len == 0) {
|
||||||
|
const message = try std.fmt.allocPrint(self.allocator, "NoProvider:{s}", .{@tagName(capability)});
|
||||||
|
self.setOwnedStatusMessage(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var ordered = std.ArrayList(EditOrder).empty;
|
||||||
|
defer ordered.deinit(self.allocator);
|
||||||
|
for (matches.items) |row| {
|
||||||
|
const edit = lsp_mod.parseProviderEditRow(row) catch {
|
||||||
|
self.message = "InvalidEdit";
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (edit.version != self.document_version) {
|
||||||
|
self.message = "StaleEdit";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (edit.end > snap.bytes.len) {
|
||||||
|
self.message = "InvalidEdit";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (ordered.items) |existing| {
|
||||||
|
const prev = lsp_mod.parseProviderEditRow(existing.row) catch unreachable;
|
||||||
|
if (!(edit.end <= prev.start or edit.start >= prev.end)) {
|
||||||
|
self.message = "OverlappingEdit";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try ordered.append(self.allocator, .{ .start = edit.start, .row = row });
|
||||||
|
}
|
||||||
|
std.mem.sort(EditOrder, ordered.items, {}, struct {
|
||||||
|
fn lessThan(_: void, lhs: EditOrder, rhs: EditOrder) bool {
|
||||||
|
return lhs.start > rhs.start;
|
||||||
|
}
|
||||||
|
}.lessThan);
|
||||||
|
try self.recordUndo();
|
||||||
|
self.clearRedo();
|
||||||
|
for (ordered.items) |item| {
|
||||||
|
const edit = lsp_mod.parseProviderEditRow(item.row) catch unreachable;
|
||||||
|
self.session.replaceRange(edit.start, edit.end, edit.replacement) catch |err| {
|
||||||
|
self.dropLastUndoSnapshot();
|
||||||
|
self.message = @errorName(err);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
self.noteDocumentChanged();
|
||||||
|
const message = try std.fmt.allocPrint(self.allocator, "{s}:{s}:applied", .{ @tagName(capability), provider });
|
||||||
|
self.setOwnedStatusMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
const DiagnosticDirection = enum { next, previous };
|
const DiagnosticDirection = enum { next, previous };
|
||||||
|
|
||||||
fn diagnosticMatchesFilter(self: *const Client, row: []const u8) bool {
|
fn diagnosticMatchesFilter(self: *const Client, row: []const u8) bool {
|
||||||
@@ -1947,6 +2154,10 @@ pub const Client = struct {
|
|||||||
.hover => try self.showCompactHover(),
|
.hover => try self.showCompactHover(),
|
||||||
.signature => try self.showCompactSignature(),
|
.signature => try self.showCompactSignature(),
|
||||||
.expand_hover => try self.openExpandedHover(),
|
.expand_hover => try self.openExpandedHover(),
|
||||||
|
.language_format => try self.formatCurrentBuffer(),
|
||||||
|
.language_format_policy => try self.showFormatPolicy(),
|
||||||
|
.language_organize_imports => try self.organizeImports(),
|
||||||
|
.language_code_actions => try self.openCodeActions(),
|
||||||
.diagnostics_open => try self.openDiagnosticsPanel(),
|
.diagnostics_open => try self.openDiagnosticsPanel(),
|
||||||
.diagnostics_next => try self.gotoDiagnostic(.next),
|
.diagnostics_next => try self.gotoDiagnostic(.next),
|
||||||
.diagnostics_previous => try self.gotoDiagnostic(.previous),
|
.diagnostics_previous => try self.gotoDiagnostic(.previous),
|
||||||
@@ -1999,6 +2210,12 @@ pub const Client = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn save(self: *Client) !void {
|
fn save(self: *Client) !void {
|
||||||
|
if (self.format_on_save and !self.skip_next_save_format and self.hasLanguageProvider(.format)) {
|
||||||
|
const before_format_version = self.document_version;
|
||||||
|
try self.applyLanguageMutation(.format, null);
|
||||||
|
if (self.document_version == before_format_version) return;
|
||||||
|
}
|
||||||
|
self.skip_next_save_format = false;
|
||||||
const snap = try self.session.snapshot();
|
const snap = try self.session.snapshot();
|
||||||
if (snap.bytes.len > diagnostics_mod.max_file_bytes) {
|
if (snap.bytes.len > diagnostics_mod.max_file_bytes) {
|
||||||
self.message = "diagnostic:save_failed:file_too_large";
|
self.message = "diagnostic:save_failed:file_too_large";
|
||||||
@@ -4223,3 +4440,144 @@ test "regular: Space t jump opens selected diagnostic row and does not mix outpu
|
|||||||
try std.testing.expectEqualStrings("one\nabcdTARGET\n", snap.bytes);
|
try std.testing.expectEqualStrings("one\nabcdTARGET\n", snap.bytes);
|
||||||
try std.testing.expectEqual(@as(usize, 8), snap.cursor_byte);
|
try std.testing.expectEqual(@as(usize, 8), snap.cursor_byte);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "regular: language format applies provider edit and undo restores" {
|
||||||
|
var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 6 });
|
||||||
|
defer client.deinit();
|
||||||
|
try client.handleTraceLine("open dirty");
|
||||||
|
try client.handleTraceLine("language_edit zls|format|1|0|5|clean");
|
||||||
|
|
||||||
|
try client.handleInput(" ");
|
||||||
|
try client.handleInput("l");
|
||||||
|
try client.handleInput("f");
|
||||||
|
var snap = try client.session.snapshot();
|
||||||
|
try std.testing.expectEqualStrings("clean", snap.bytes);
|
||||||
|
|
||||||
|
try client.handleInput("u");
|
||||||
|
snap = try client.session.snapshot();
|
||||||
|
try std.testing.expectEqualStrings("dirty", snap.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: format on save and one-shot save without format" {
|
||||||
|
var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 6 });
|
||||||
|
defer client.deinit();
|
||||||
|
try client.handleTraceLine("open abc");
|
||||||
|
try client.handleTraceLine("language_default_format zls");
|
||||||
|
try client.handleTraceLine("language_edit zls|format|1|0|3|ABC");
|
||||||
|
try client.handleTraceLine("save");
|
||||||
|
try std.testing.expectEqualStrings("ABC", try client.saved());
|
||||||
|
|
||||||
|
var ambiguous = try Client.init(std.testing.allocator, .{ .width = 80, .height = 6 });
|
||||||
|
defer ambiguous.deinit();
|
||||||
|
try ambiguous.handleTraceLine("open abc");
|
||||||
|
try ambiguous.handleTraceLine("language_edit zls|format|1|0|3|ZLS");
|
||||||
|
try ambiguous.handleTraceLine("language_edit prettier|format|1|0|3|PRETTY");
|
||||||
|
try ambiguous.handleTraceLine("save");
|
||||||
|
try std.testing.expectError(Error.NothingSaved, ambiguous.saved());
|
||||||
|
const ambiguous_frame = try ambiguous.render(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(ambiguous_frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, ambiguous_frame, "action:format:provider:zls") != null);
|
||||||
|
|
||||||
|
var skip = try Client.init(std.testing.allocator, .{ .width = 64, .height = 6 });
|
||||||
|
defer skip.deinit();
|
||||||
|
try skip.handleTraceLine("open abc");
|
||||||
|
try skip.handleTraceLine("language_default_format zls");
|
||||||
|
try skip.handleTraceLine("language_edit zls|format|1|0|3|ABC");
|
||||||
|
try skip.handleTraceLine("save_without_format");
|
||||||
|
try std.testing.expectEqualStrings("abc", try skip.saved());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: provider picker applies selected formatter" {
|
||||||
|
var client = try Client.init(std.testing.allocator, .{ .width = 80, .height = 7 });
|
||||||
|
defer client.deinit();
|
||||||
|
try client.handleTraceLine("open abc");
|
||||||
|
try client.handleTraceLine("language_edit zls|format|1|0|3|ZLS");
|
||||||
|
try client.handleTraceLine("language_edit prettier|format|1|0|3|PRETTY");
|
||||||
|
|
||||||
|
try client.handleInput(" ");
|
||||||
|
try client.handleInput("l");
|
||||||
|
try client.handleInput("f");
|
||||||
|
const picker_frame = try client.render(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(picker_frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, picker_frame, "action:format:provider:zls") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, picker_frame, "action:format:provider:prettier") != null);
|
||||||
|
|
||||||
|
try client.handleTraceLine("list_filter prettier");
|
||||||
|
try client.handleTraceLine("key enter");
|
||||||
|
const snap = try client.session.snapshot();
|
||||||
|
try std.testing.expectEqualStrings("PRETTY", snap.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: organize imports and code action show source before mutation" {
|
||||||
|
var client = try Client.init(std.testing.allocator, .{ .width = 88, .height = 7 });
|
||||||
|
defer client.deinit();
|
||||||
|
try client.handleTraceLine("open imports\nbody");
|
||||||
|
try client.handleTraceLine("language_edit zls|organize_imports|1|0|7|sorted");
|
||||||
|
try client.handleInput(" ");
|
||||||
|
try client.handleInput("l");
|
||||||
|
try client.handleInput("o");
|
||||||
|
var snap = try client.session.snapshot();
|
||||||
|
try std.testing.expectEqualStrings("sorted\nbody", snap.bytes);
|
||||||
|
|
||||||
|
var action = try Client.init(std.testing.allocator, .{ .width = 88, .height = 7 });
|
||||||
|
defer action.deinit();
|
||||||
|
try action.handleTraceLine("open abcdef");
|
||||||
|
try action.handleTraceLine("language_edit zls|code_action|1|1|4|XYZ");
|
||||||
|
try action.handleInput(" ");
|
||||||
|
try action.handleInput("l");
|
||||||
|
try action.handleInput("a");
|
||||||
|
const frame = try action.render(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "action:code_action:provider:zls:scope:1-4") != null);
|
||||||
|
try action.handleTraceLine("key enter");
|
||||||
|
snap = try action.session.snapshot();
|
||||||
|
try std.testing.expectEqualStrings("aXYZef", snap.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "adversarial: language mutations report missing stale and overlapping providers" {
|
||||||
|
var missing = try Client.init(std.testing.allocator, .{ .width = 80, .height = 6 });
|
||||||
|
defer missing.deinit();
|
||||||
|
try missing.handleTraceLine("open abc");
|
||||||
|
try missing.handleInput(" ");
|
||||||
|
try missing.handleInput("l");
|
||||||
|
try missing.handleInput("f");
|
||||||
|
const missing_frame = try missing.render(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(missing_frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, missing_frame, "NoProvider:format") != null);
|
||||||
|
|
||||||
|
var stale = try Client.init(std.testing.allocator, .{ .width = 80, .height = 6 });
|
||||||
|
defer stale.deinit();
|
||||||
|
try stale.handleTraceLine("open abc");
|
||||||
|
try stale.handleTraceLine("language_edit zls|format|0|0|3|ABC");
|
||||||
|
try stale.handleInput(" ");
|
||||||
|
try stale.handleInput("l");
|
||||||
|
try stale.handleInput("f");
|
||||||
|
const stale_frame = try stale.render(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(stale_frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, stale_frame, "StaleEdit") != null);
|
||||||
|
|
||||||
|
var overlap = try Client.init(std.testing.allocator, .{ .width = 80, .height = 6 });
|
||||||
|
defer overlap.deinit();
|
||||||
|
try overlap.handleTraceLine("open abcdef");
|
||||||
|
try overlap.handleTraceLine("language_edit zls|format|1|0|3|AAA");
|
||||||
|
try overlap.handleTraceLine("language_edit zls|format|1|2|5|BBB");
|
||||||
|
try overlap.handleInput(" ");
|
||||||
|
try overlap.handleInput("l");
|
||||||
|
try overlap.handleInput("f");
|
||||||
|
const overlap_frame = try overlap.render(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(overlap_frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, overlap_frame, "OverlappingEdit") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: provider edit rows parse source scope and replacement" {
|
||||||
|
const row = try lsp_mod.editRowAlloc(std.testing.allocator, "zls", .code_action, 7, 2, 5, "XYZ");
|
||||||
|
defer std.testing.allocator.free(row);
|
||||||
|
const edit = try lsp_mod.parseProviderEditRow(row);
|
||||||
|
try std.testing.expectEqualStrings("zls", edit.provider);
|
||||||
|
try std.testing.expectEqual(lsp_mod.ProviderCapability.code_action, edit.capability);
|
||||||
|
try std.testing.expectEqual(@as(u64, 7), edit.version);
|
||||||
|
try std.testing.expectEqualStrings("XYZ", edit.replacement);
|
||||||
|
const action_row = try lsp_mod.actionPanelRowAlloc(std.testing.allocator, row);
|
||||||
|
defer std.testing.allocator.free(action_row);
|
||||||
|
try std.testing.expectEqualStrings("zls", try lsp_mod.providerFromActionPanelRow(action_row));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user