const std = @import("std");const image_attachments = @import("../images/image_attachments.zig");const debug_trace = @import("../shared/debug_trace.zig");const entity_spans = @import("../shared/entity_spans.zig");const types = @import("../shared/types.zig");const skill_contract = @import("../skills/skill_contract.zig");const editor_state = @import("editor_state.zig");const input_limit_rejection = @import("input_limit_rejection.zig");const paste_blocks = @import("pasted_blocks.zig");const picker_state = @import("picker_state.zig");const registered_entities = @import("registered_entities.zig");const text_boundaries = @import("text_boundaries.zig");const vertical_navigation = @import("vertical_navigation.zig"); const Allocator = std.mem.Allocator; pub const ImageBlocks = std.ArrayList(types.ImageAttachment); pub const YankResult = union(enum) { inactive, limit_exceeded: usize, inserted: usize,}; pub const LineBounds = struct { start: usize, cursor: usize, end: usize,}; pub const DeleteRange = struct { start: usize, end: usize,}; pub const ActiveComposer = struct { edit: *editor_state.State, entities: *registered_entities.State, picker: *picker_state.State, vertical_navigation: *vertical_navigation.State, input_limit_rejection: *input_limit_rejection.State, images: ?*ImageBlocks,}; pub const State = struct { text: std.ArrayList(u8) = .empty, images: ImageBlocks = .empty, image_tokens: std.ArrayList(entity_spans.ImageTokenSpan) = .empty, skill_tokens: std.ArrayList(registered_entities.SkillTokenSpan) = .empty, owns_snapshots: bool = false, pub fn deinit(self: *State, alloc: Allocator) void { self.text.deinit(alloc); for (self.images.items) |image| { if (self.owns_snapshots) { image_attachments.discardImageAttachment(alloc, image); } else { types.freeImageAttachment(alloc, image); } } self.images.deinit(alloc); self.image_tokens.deinit(alloc); for (self.skill_tokens.items) |token| { alloc.free(token.name); alloc.free(token.path); } self.skill_tokens.deinit(alloc); self.* = .{}; } pub fn resetForSession(self: *State, alloc: Allocator) void { if (self.text.items.len > 0) { debug_trace.logf( "input", "kill ring dropped bytes={d} images={d} skills={d} reason=session_reset", .{ self.text.items.len, self.images.items.len, self.skill_tokens.items.len, }, ); } self.deinit(alloc); } pub fn deleteRange( self: *State, alloc: Allocator, active: ActiveComposer, bounds: LineBounds, requested: DeleteRange, ) !bool { if (requested.start >= requested.end) return false; const actual = actualDeleteRange( active.entities.pasted_blocks.items, active.entities.image_tokens.items, bounds, requested, ) orelse return false; var next = try capturePayload(alloc, active, actual); errdefer next.deinit(alloc); active.picker.clearModelPickerFlow(); active.picker.resetFilePickerIndex(); removeBackingMetadata(alloc, active, actual); next.owns_snapshots = true; deleteInputRange(alloc, active, actual.start, actual.end); _ = active.edit.setCursor(actual.start); active.picker.reconcileInlinePickerAfterEdit(active.edit); active.picker.resetActiveModelPickerIndex(); var previous = self.*; self.* = next; previous.deinit(alloc); return true; } pub fn yank( self: *const State, alloc: Allocator, active: ActiveComposer, first_image_id: usize, max_len: usize, ) !YankResult { if (self.text.items.len == 0) return .inactive; const replacement_len = try self.replacementLen(first_image_id); const selection = active.edit.selectionRange(); const admitted = if (selection) |range| canReplaceInputRange(active, range.start, range.end, replacement_len, max_len) else canInsertInput(active, replacement_len, max_len); if (!admitted) return .{ .limit_exceeded = replacement_len }; var prepared = try self.prepareYank(alloc, first_image_id); defer prepared.deinit(alloc); std.debug.assert(prepared.text.items.len == replacement_len); const image_count = prepared.images.items.len; const blocks = if (image_count > 0) active.images orelse return error.MissingImageAttachmentOwner else null; try active.edit.input.ensureUnusedCapacity(alloc, prepared.text.items.len); try active.entities.image_tokens.ensureUnusedCapacity(alloc, prepared.image_tokens.items.len); try active.entities.skill_tokens.ensureUnusedCapacity(alloc, prepared.skill_tokens.items.len); if (blocks) |active_images| { try active_images.ensureUnusedCapacity(alloc, prepared.images.items.len); if (active_images.items.len > 0) { std.debug.assert(active_images.items[active_images.items.len - 1].id < first_image_id); } } const insertion_start = if (selection) |range| range.start else active.edit.cursor; active.vertical_navigation.reset(); if (selection) |range| { removeRegisteredEntitiesOverlapping(alloc, active, range.start, range.end); deleteInputRange(alloc, active, range.start, range.end); _ = active.edit.setCursor(range.start); _ = active.edit.clearSelection(); } insertInputSliceAssumeCapacity(alloc, active, prepared.text.items); if (blocks) |active_images| { active_images.appendSliceAssumeCapacity(prepared.images.items); prepared.images.clearRetainingCapacity(); } for (prepared.image_tokens.items) |token| { active.entities.registerImageTokenAssumeCapacity(.{ .id = token.id, .span = .{ .raw_start = insertion_start + token.span.raw_start, .raw_end = insertion_start + token.span.raw_end, }, }); } for (prepared.skill_tokens.items) |token| { active.entities.registerSkillTokenAssumeCapacity(.{ .raw_start = insertion_start + token.raw_start, .raw_end = insertion_start + token.raw_end, .name = token.name, .path = token.path, .display_source = token.display_source, .owns_trailing_separator = token.owns_trailing_separator, }); } prepared.skill_tokens.clearRetainingCapacity(); active.input_limit_rejection.* = input_limit_rejection.clear(); return .{ .inserted = image_count }; } fn replacementLen(self: *const State, first_image_id: usize) !usize { _ = try std.math.add(usize, first_image_id, self.image_tokens.items.len); var replacement_len = self.text.items.len; var previous_end: usize = 0; for (self.image_tokens.items, 0..) |token, index| { if (!token.span.isValid(self.text.items.len) or token.span.raw_start < previous_end) { return error.InvalidKillPayload; } const match = image_attachments.matchImagePlaceholder( self.text.items, token.span.raw_start, ) orelse return error.InvalidKillPayload; if (match.id != token.id or token.span.raw_start + match.length != token.span.raw_end) { return error.InvalidKillPayload; } const next_id = try std.math.add(usize, first_image_id, index); var placeholder_buf: [64]u8 = undefined; const placeholder = try image_attachments.formatImagePlaceholder( &placeholder_buf, next_id, ); replacement_len = try std.math.sub( usize, replacement_len, token.span.raw_end - token.span.raw_start, ); replacement_len = try std.math.add(usize, replacement_len, placeholder.len); previous_end = token.span.raw_end; } return replacement_len; } fn prepareYank( self: *const State, alloc: Allocator, first_image_id: usize, ) !State { var prepared = State{ .owns_snapshots = true }; errdefer prepared.deinit(alloc); var source_offset: usize = 0; for (self.image_tokens.items, 0..) |token, index| { if (index >= self.images.items.len or self.images.items[index].id != token.id) { return error.InvalidKillPayload; } try prepared.text.appendSlice( alloc, self.text.items[source_offset..token.span.raw_start], ); const relative_start = prepared.text.items.len; const next_id = try std.math.add(usize, first_image_id, index); var placeholder_buf: [64]u8 = undefined; const placeholder = try image_attachments.formatImagePlaceholder( &placeholder_buf, next_id, ); try prepared.text.appendSlice(alloc, placeholder); try prepared.appendYankedImage( alloc, self.images.items[index], .{ .id = next_id, .span = .{ .raw_start = relative_start, .raw_end = relative_start + placeholder.len, }, }, ); source_offset = token.span.raw_end; } if (self.images.items.len != self.image_tokens.items.len) { return error.InvalidKillPayload; } try prepared.text.appendSlice(alloc, self.text.items[source_offset..]); for (self.skill_tokens.items) |token| { if (token.raw_start >= token.raw_end or token.raw_end > self.text.items.len) { return error.InvalidKillPayload; } const raw = self.text.items[token.raw_start..token.raw_end]; if (raw.len != token.name.len + 1 or raw[0] != '$' or !std.mem.eql(u8, raw[1..], token.name)) { return error.InvalidKillPayload; } const relative_start = image_attachments.projectOffsetThroughPlaceholderRewrite( self.image_tokens.items, prepared.image_tokens.items, self.text.items.len, token.raw_start, ) orelse return error.InvalidKillPayload; const relative_end = image_attachments.projectOffsetThroughPlaceholderRewrite( self.image_tokens.items, prepared.image_tokens.items, self.text.items.len, token.raw_end, ) orelse return error.InvalidKillPayload; try prepared.appendSkillCopy( alloc, relative_start, relative_end, token.name, token.path, token.display_source, token.owns_trailing_separator, ); } return prepared; } fn appendImageCopy( self: *State, alloc: Allocator, attachment: types.ImageAttachment, token: entity_spans.ImageTokenSpan, ) !void { try self.image_tokens.append(alloc, token); errdefer self.image_tokens.items.len -= 1; const path = try alloc.dupe(u8, attachment.path); errdefer alloc.free(path); const media_type = try alloc.dupe(u8, attachment.media_type); errdefer alloc.free(media_type); const snapshot_path = if (attachment.snapshot_path) |value| try alloc.dupe(u8, value) else null; errdefer if (snapshot_path) |value| alloc.free(value); const snapshot_sha256 = if (attachment.snapshot_sha256) |value| try alloc.dupe(u8, value) else null; errdefer if (snapshot_sha256) |value| alloc.free(value); try self.images.append(alloc, .{ .id = token.id, .path = path, .media_type = media_type, .snapshot_path = snapshot_path, .snapshot_sha256 = snapshot_sha256, }); } fn appendYankedImage( self: *State, alloc: Allocator, attachment: types.ImageAttachment, token: entity_spans.ImageTokenSpan, ) !void { try self.image_tokens.append(alloc, token); errdefer self.image_tokens.items.len -= 1; const cloned = try image_attachments.cloneVerifiedImageAttachment( alloc, attachment, token.id, ); errdefer image_attachments.discardImageAttachment(alloc, cloned); try self.images.append(alloc, cloned); } fn appendSkillCopy( self: *State, alloc: Allocator, raw_start: usize, raw_end: usize, name: []const u8, path: []const u8, display_source: ?skill_contract.SkillSource, owns_trailing_separator: bool, ) !void { const owned_name = try alloc.dupe(u8, name); errdefer alloc.free(owned_name); const owned_path = try alloc.dupe(u8, path); errdefer alloc.free(owned_path); try self.skill_tokens.append(alloc, .{ .raw_start = raw_start, .raw_end = raw_end, .name = owned_name, .path = owned_path, .display_source = display_source, .owns_trailing_separator = owns_trailing_separator, }); }}; pub fn lineBounds(input: []const u8, cursor: usize) LineBounds { const clamped = @min(cursor, input.len); return .{ .start = text_boundaries.logicalLineStart(input, clamped), .cursor = clamped, .end = text_boundaries.logicalLineEnd(input, clamped), };} fn actualDeleteRange( pasted: []const paste_blocks.PastedBlock, image_tokens: []const entity_spans.ImageTokenSpan, bounds: LineBounds, requested: DeleteRange,) ?DeleteRange { if (requested.start >= requested.end) return requested; var actual = requested; while (true) { var changed = false; for (pasted) |block| { const span_range = DeleteRange{ .start = block.span.raw_start, .end = block.span.raw_end, }; if (rangesOverlap(actual, span_range) and !rangeContains(actual, span_range)) { actual.start = @min(actual.start, span_range.start); actual.end = @max(actual.end, span_range.end); if (actual.start < bounds.start or actual.end > bounds.end) return null; changed = true; } } for (image_tokens) |token| { const span_range = DeleteRange{ .start = token.span.raw_start, .end = token.span.raw_end, }; if (rangesOverlap(actual, span_range) and !rangeContains(actual, span_range)) { actual.start = @min(actual.start, span_range.start); actual.end = @max(actual.end, span_range.end); if (actual.start < bounds.start or actual.end > bounds.end) return null; changed = true; } } if (!changed) break; } return actual;} fn capturePayload( alloc: Allocator, active: ActiveComposer, actual: DeleteRange,) !State { var payload: State = .{}; errdefer payload.deinit(alloc); const expanded = try paste_blocks.expandRange( alloc, active.edit.input.items, active.entities.pasted_blocks.items, actual.start, actual.end, ); defer if (expanded.owned) alloc.free(expanded.text); try payload.text.appendSlice(alloc, expanded.text); const expanded_origin = paste_blocks.projectOffsetThroughExpansion( active.edit.input.items, active.entities.pasted_blocks.items, actual.start, ) orelse return error.InvalidKillRange; const actual_span = entity_spans.Span{ .raw_start = actual.start, .raw_end = actual.end, }; for (active.entities.image_tokens.items) |token| { if (!entity_spans.containsSpan(actual_span, token.span)) continue; if (!active.entities.isValidImageToken(active.edit.input.items, token)) continue; const blocks = active.images orelse return error.MissingImageAttachment; const attachment = image_attachments.findById(blocks.items, token.id) orelse return error.MissingImageAttachment; const relative_start = try projectKillOffset( active, actual, expanded_origin, token.span.raw_start, ); const relative_end = try projectKillOffset( active, actual, expanded_origin, token.span.raw_end, ); try payload.appendImageCopy(alloc, attachment, .{ .id = token.id, .span = .{ .raw_start = relative_start, .raw_end = relative_end, }, }); } for (active.entities.skill_tokens.items) |token| { const span = entity_spans.Span{ .raw_start = token.raw_start, .raw_end = token.raw_end, }; if (!entity_spans.containsSpan(actual_span, span)) continue; if (!active.entities.isValidSkillToken(active.edit.input.items, token)) continue; const relative_start = try projectKillOffset( active, actual, expanded_origin, token.raw_start, ); const relative_end = try projectKillOffset( active, actual,Lines 1–500 of 806. Load more
const std = @import("std"); const image_attachments = @import("../images/image_attachments.zig"); const debug_trace = @import("../shared/debug_trace.zig"); const entity_spans = @import("../shared/entity_spans.zig"); const types = @import("../shared/types.zig"); const skill_contract = @import("../skills/skill_contract.zig"); const editor_state = @import("editor_state.zig"); const input_limit_rejection = @import("input_limit_rejection.zig"); const paste_blocks = @import("pasted_blocks.zig"); const picker_state = @import("picker_state.zig"); const registered_entities = @import("registered_entities.zig"); const text_boundaries = @import("text_boundaries.zig"); const vertical_navigation = @import("vertical_navigation.zig"); const Allocator = std.mem.Allocator; pub const ImageBlocks = std.ArrayList(types.ImageAttachment); pub const YankResult = union(enum) { inactive, limit_exceeded: usize, inserted: usize, }; pub const LineBounds = struct { start: usize, cursor: usize, end: usize, }; pub const DeleteRange = struct { start: usize, end: usize, }; pub const ActiveComposer = struct { edit: *editor_state.State, entities: *registered_entities.State, picker: *picker_state.State, vertical_navigation: *vertical_navigation.State, input_limit_rejection: *input_limit_rejection.State, images: ?*ImageBlocks, }; pub const State = struct { text: std.ArrayList(u8) = .empty, images: ImageBlocks = .empty, image_tokens: std.ArrayList(entity_spans.ImageTokenSpan) = .empty, skill_tokens: std.ArrayList(registered_entities.SkillTokenSpan) = .empty, owns_snapshots: bool = false, pub fn deinit(self: *State, alloc: Allocator) void { self.text.deinit(alloc); for (self.images.items) |image| { if (self.owns_snapshots) { image_attachments.discardImageAttachment(alloc, image); } else { types.freeImageAttachment(alloc, image); } } self.images.deinit(alloc); self.image_tokens.deinit(alloc); for (self.skill_tokens.items) |token| { alloc.free(token.name); alloc.free(token.path); } self.skill_tokens.deinit(alloc); self.* = .{}; } pub fn resetForSession(self: *State, alloc: Allocator) void { if (self.text.items.len > 0) { debug_trace.logf( "input", "kill ring dropped bytes={d} images={d} skills={d} reason=session_reset", .{ self.text.items.len, self.images.items.len, self.skill_tokens.items.len, }, ); } self.deinit(alloc); } pub fn deleteRange( self: *State, alloc: Allocator, active: ActiveComposer, bounds: LineBounds, requested: DeleteRange, ) !bool { if (requested.start >= requested.end) return false; const actual = actualDeleteRange( active.entities.pasted_blocks.items, active.entities.image_tokens.items, bounds, requested, ) orelse return false; var next = try capturePayload(alloc, active, actual); errdefer next.deinit(alloc); active.picker.clearModelPickerFlow(); active.picker.resetFilePickerIndex(); removeBackingMetadata(alloc, active, actual); next.owns_snapshots = true; deleteInputRange(alloc, active, actual.start, actual.end); _ = active.edit.setCursor(actual.start); active.picker.reconcileInlinePickerAfterEdit(active.edit); active.picker.resetActiveModelPickerIndex(); var previous = self.*; self.* = next; previous.deinit(alloc); return true; } pub fn yank( self: *const State, alloc: Allocator, active: ActiveComposer, first_image_id: usize, max_len: usize, ) !YankResult { if (self.text.items.len == 0) return .inactive; const replacement_len = try self.replacementLen(first_image_id); const selection = active.edit.selectionRange(); const admitted = if (selection) |range| canReplaceInputRange(active, range.start, range.end, replacement_len, max_len) else canInsertInput(active, replacement_len, max_len); if (!admitted) return .{ .limit_exceeded = replacement_len }; var prepared = try self.prepareYank(alloc, first_image_id); defer prepared.deinit(alloc); std.debug.assert(prepared.text.items.len == replacement_len); const image_count = prepared.images.items.len; const blocks = if (image_count > 0) active.images orelse return error.MissingImageAttachmentOwner else null; try active.edit.input.ensureUnusedCapacity(alloc, prepared.text.items.len); try active.entities.image_tokens.ensureUnusedCapacity(alloc, prepared.image_tokens.items.len); try active.entities.skill_tokens.ensureUnusedCapacity(alloc, prepared.skill_tokens.items.len); if (blocks) |active_images| { try active_images.ensureUnusedCapacity(alloc, prepared.images.items.len); if (active_images.items.len > 0) { std.debug.assert(active_images.items[active_images.items.len - 1].id < first_image_id); } } const insertion_start = if (selection) |range| range.start else active.edit.cursor; active.vertical_navigation.reset(); if (selection) |range| { removeRegisteredEntitiesOverlapping(alloc, active, range.start, range.end); deleteInputRange(alloc, active, range.start, range.end); _ = active.edit.setCursor(range.start); _ = active.edit.clearSelection(); } insertInputSliceAssumeCapacity(alloc, active, prepared.text.items); if (blocks) |active_images| { active_images.appendSliceAssumeCapacity(prepared.images.items); prepared.images.clearRetainingCapacity(); } for (prepared.image_tokens.items) |token| { active.entities.registerImageTokenAssumeCapacity(.{ .id = token.id, .span = .{ .raw_start = insertion_start + token.span.raw_start, .raw_end = insertion_start + token.span.raw_end, }, }); } for (prepared.skill_tokens.items) |token| { active.entities.registerSkillTokenAssumeCapacity(.{ .raw_start = insertion_start + token.raw_start, .raw_end = insertion_start + token.raw_end, .name = token.name, .path = token.path, .display_source = token.display_source, .owns_trailing_separator = token.owns_trailing_separator, }); } prepared.skill_tokens.clearRetainingCapacity(); active.input_limit_rejection.* = input_limit_rejection.clear(); return .{ .inserted = image_count }; } fn replacementLen(self: *const State, first_image_id: usize) !usize { _ = try std.math.add(usize, first_image_id, self.image_tokens.items.len); var replacement_len = self.text.items.len; var previous_end: usize = 0; for (self.image_tokens.items, 0..) |token, index| { if (!token.span.isValid(self.text.items.len) or token.span.raw_start < previous_end) { return error.InvalidKillPayload; } const match = image_attachments.matchImagePlaceholder( self.text.items, token.span.raw_start, ) orelse return error.InvalidKillPayload; if (match.id != token.id or token.span.raw_start + match.length != token.span.raw_end) { return error.InvalidKillPayload; } const next_id = try std.math.add(usize, first_image_id, index); var placeholder_buf: [64]u8 = undefined; const placeholder = try image_attachments.formatImagePlaceholder( &placeholder_buf, next_id, ); replacement_len = try std.math.sub( usize, replacement_len, token.span.raw_end - token.span.raw_start, ); replacement_len = try std.math.add(usize, replacement_len, placeholder.len); previous_end = token.span.raw_end; } return replacement_len; } fn prepareYank( self: *const State, alloc: Allocator, first_image_id: usize, ) !State { var prepared = State{ .owns_snapshots = true }; errdefer prepared.deinit(alloc); var source_offset: usize = 0; for (self.image_tokens.items, 0..) |token, index| { if (index >= self.images.items.len or self.images.items[index].id != token.id) { return error.InvalidKillPayload; } try prepared.text.appendSlice( alloc, self.text.items[source_offset..token.span.raw_start], ); const relative_start = prepared.text.items.len; const next_id = try std.math.add(usize, first_image_id, index); var placeholder_buf: [64]u8 = undefined; const placeholder = try image_attachments.formatImagePlaceholder( &placeholder_buf, next_id, ); try prepared.text.appendSlice(alloc, placeholder); try prepared.appendYankedImage( alloc, self.images.items[index], .{ .id = next_id, .span = .{ .raw_start = relative_start, .raw_end = relative_start + placeholder.len, }, }, ); source_offset = token.span.raw_end; } if (self.images.items.len != self.image_tokens.items.len) { return error.InvalidKillPayload; } try prepared.text.appendSlice(alloc, self.text.items[source_offset..]); for (self.skill_tokens.items) |token| { if (token.raw_start >= token.raw_end or token.raw_end > self.text.items.len) { return error.InvalidKillPayload; } const raw = self.text.items[token.raw_start..token.raw_end]; if (raw.len != token.name.len + 1 or raw[0] != '$' or !std.mem.eql(u8, raw[1..], token.name)) { return error.InvalidKillPayload; } const relative_start = image_attachments.projectOffsetThroughPlaceholderRewrite( self.image_tokens.items, prepared.image_tokens.items, self.text.items.len, token.raw_start, ) orelse return error.InvalidKillPayload; const relative_end = image_attachments.projectOffsetThroughPlaceholderRewrite( self.image_tokens.items, prepared.image_tokens.items, self.text.items.len, token.raw_end, ) orelse return error.InvalidKillPayload; try prepared.appendSkillCopy( alloc, relative_start, relative_end, token.name, token.path, token.display_source, token.owns_trailing_separator, ); } return prepared; } fn appendImageCopy( self: *State, alloc: Allocator, attachment: types.ImageAttachment, token: entity_spans.ImageTokenSpan, ) !void { try self.image_tokens.append(alloc, token); errdefer self.image_tokens.items.len -= 1; const path = try alloc.dupe(u8, attachment.path); errdefer alloc.free(path); const media_type = try alloc.dupe(u8, attachment.media_type); errdefer alloc.free(media_type); const snapshot_path = if (attachment.snapshot_path) |value| try alloc.dupe(u8, value) else null; errdefer if (snapshot_path) |value| alloc.free(value); const snapshot_sha256 = if (attachment.snapshot_sha256) |value| try alloc.dupe(u8, value) else null; errdefer if (snapshot_sha256) |value| alloc.free(value); try self.images.append(alloc, .{ .id = token.id, .path = path, .media_type = media_type, .snapshot_path = snapshot_path, .snapshot_sha256 = snapshot_sha256, }); } fn appendYankedImage( self: *State, alloc: Allocator, attachment: types.ImageAttachment, token: entity_spans.ImageTokenSpan, ) !void { try self.image_tokens.append(alloc, token); errdefer self.image_tokens.items.len -= 1; const cloned = try image_attachments.cloneVerifiedImageAttachment( alloc, attachment, token.id, ); errdefer image_attachments.discardImageAttachment(alloc, cloned); try self.images.append(alloc, cloned); } fn appendSkillCopy( self: *State, alloc: Allocator, raw_start: usize, raw_end: usize, name: []const u8, path: []const u8, display_source: ?skill_contract.SkillSource, owns_trailing_separator: bool, ) !void { const owned_name = try alloc.dupe(u8, name); errdefer alloc.free(owned_name); const owned_path = try alloc.dupe(u8, path); errdefer alloc.free(owned_path); try self.skill_tokens.append(alloc, .{ .raw_start = raw_start, .raw_end = raw_end, .name = owned_name, .path = owned_path, .display_source = display_source, .owns_trailing_separator = owns_trailing_separator, }); } }; pub fn lineBounds(input: []const u8, cursor: usize) LineBounds { const clamped = @min(cursor, input.len); return .{ .start = text_boundaries.logicalLineStart(input, clamped), .cursor = clamped, .end = text_boundaries.logicalLineEnd(input, clamped), }; } fn actualDeleteRange( pasted: []const paste_blocks.PastedBlock, image_tokens: []const entity_spans.ImageTokenSpan, bounds: LineBounds, requested: DeleteRange, ) ?DeleteRange { if (requested.start >= requested.end) return requested; var actual = requested; while (true) { var changed = false; for (pasted) |block| { const span_range = DeleteRange{ .start = block.span.raw_start, .end = block.span.raw_end, }; if (rangesOverlap(actual, span_range) and !rangeContains(actual, span_range)) { actual.start = @min(actual.start, span_range.start); actual.end = @max(actual.end, span_range.end); if (actual.start < bounds.start or actual.end > bounds.end) return null; changed = true; } } for (image_tokens) |token| { const span_range = DeleteRange{ .start = token.span.raw_start, .end = token.span.raw_end, }; if (rangesOverlap(actual, span_range) and !rangeContains(actual, span_range)) { actual.start = @min(actual.start, span_range.start); actual.end = @max(actual.end, span_range.end); if (actual.start < bounds.start or actual.end > bounds.end) return null; changed = true; } } if (!changed) break; } return actual; } fn capturePayload( alloc: Allocator, active: ActiveComposer, actual: DeleteRange, ) !State { var payload: State = .{}; errdefer payload.deinit(alloc); const expanded = try paste_blocks.expandRange( alloc, active.edit.input.items, active.entities.pasted_blocks.items, actual.start, actual.end, ); defer if (expanded.owned) alloc.free(expanded.text); try payload.text.appendSlice(alloc, expanded.text); const expanded_origin = paste_blocks.projectOffsetThroughExpansion( active.edit.input.items, active.entities.pasted_blocks.items, actual.start, ) orelse return error.InvalidKillRange; const actual_span = entity_spans.Span{ .raw_start = actual.start, .raw_end = actual.end, }; for (active.entities.image_tokens.items) |token| { if (!entity_spans.containsSpan(actual_span, token.span)) continue; if (!active.entities.isValidImageToken(active.edit.input.items, token)) continue; const blocks = active.images orelse return error.MissingImageAttachment; const attachment = image_attachments.findById(blocks.items, token.id) orelse return error.MissingImageAttachment; const relative_start = try projectKillOffset( active, actual, expanded_origin, token.span.raw_start, ); const relative_end = try projectKillOffset( active, actual, expanded_origin, token.span.raw_end, ); try payload.appendImageCopy(alloc, attachment, .{ .id = token.id, .span = .{ .raw_start = relative_start, .raw_end = relative_end, }, }); } for (active.entities.skill_tokens.items) |token| { const span = entity_spans.Span{ .raw_start = token.raw_start, .raw_end = token.raw_end, }; if (!entity_spans.containsSpan(actual_span, span)) continue; if (!active.entities.isValidSkillToken(active.edit.input.items, token)) continue; const relative_start = try projectKillOffset( active, actual, expanded_origin, token.raw_start, ); const relative_end = try projectKillOffset( active, actual,