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;
|
||||
- compact-first hover/signature cards with expandable scroll/search panels;
|
||||
- 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;
|
||||
- Insert pending-space rail, including `Space` pause then `n` to Normal;
|
||||
- counts/repetition grammar with direct digit and mobile repeat-rail paths;
|
||||
|
||||
+9
-1
@@ -30,6 +30,10 @@ pub const Action = union(enum) {
|
||||
hover,
|
||||
signature,
|
||||
expand_hover,
|
||||
language_format,
|
||||
language_format_policy,
|
||||
language_organize_imports,
|
||||
language_code_actions,
|
||||
diagnostics_open,
|
||||
diagnostics_next,
|
||||
diagnostics_previous,
|
||||
@@ -220,7 +224,11 @@ pub const Leader = struct {
|
||||
self.mode = .idle;
|
||||
if (std.mem.eql(u8, text, "h")) return .hover;
|
||||
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";
|
||||
return .{ .not_built = .lsp };
|
||||
},
|
||||
|
||||
+91
@@ -23,6 +23,8 @@ pub const Error = error{
|
||||
InvalidProvider,
|
||||
MissingProvider,
|
||||
AmbiguousProvider,
|
||||
StaleEdit,
|
||||
OverlappingEdit,
|
||||
};
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
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 {
|
||||
var rows = std.ArrayList([]const u8).empty;
|
||||
errdefer {
|
||||
|
||||
+358
@@ -44,6 +44,8 @@ const PanelContext = enum {
|
||||
none,
|
||||
file_picker,
|
||||
project_search,
|
||||
language_provider_picker,
|
||||
code_actions,
|
||||
};
|
||||
|
||||
const EditSnapshot = struct {
|
||||
@@ -155,6 +157,11 @@ pub const Client = struct {
|
||||
hover_rows: std.ArrayList([]u8) = .empty,
|
||||
signature_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_index: usize = 0,
|
||||
diagnostic_selected: bool = false,
|
||||
@@ -192,6 +199,8 @@ pub const Client = struct {
|
||||
self.freeOwnedClientRows(&self.hover_rows);
|
||||
self.freeOwnedClientRows(&self.signature_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);
|
||||
self.freeSnapshotStack(&self.undo_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, "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, "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, "job_profile ")) return self.openJobProfileByName(line[12..]);
|
||||
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) {
|
||||
.file_picker => try self.openRepoPathFromPanel(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",
|
||||
}
|
||||
}
|
||||
@@ -1310,6 +1325,198 @@ pub const Client = struct {
|
||||
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 };
|
||||
|
||||
fn diagnosticMatchesFilter(self: *const Client, row: []const u8) bool {
|
||||
@@ -1947,6 +2154,10 @@ pub const Client = struct {
|
||||
.hover => try self.showCompactHover(),
|
||||
.signature => try self.showCompactSignature(),
|
||||
.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_next => try self.gotoDiagnostic(.next),
|
||||
.diagnostics_previous => try self.gotoDiagnostic(.previous),
|
||||
@@ -1999,6 +2210,12 @@ pub const Client = struct {
|
||||
}
|
||||
|
||||
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();
|
||||
if (snap.bytes.len > diagnostics_mod.max_file_bytes) {
|
||||
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.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