import { decodeKittyPrintable, getKeybindings, matchesKey, SelectList, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
import { UndoStack } from "./undo_stack.js";
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
const PASTE_MARKER_REGEX = /\[paste #(\d+)( (\+\d+ lines|\d+ chars))?\]/g;
const PASTE_MARKER_SINGLE = /^\[paste #(\d+)( (\+\d+ lines|\d+ chars))?\]$/;
const PUNCTUATION_REGEX = /[(){}[\]<>.,;:'"!?+\-=*/\\|&%^$#@~`]/;
const CSI_PATTERN = "\\x1b\\[[0-9;]*[A-Za-z]";
const OSC_PATTERN = "\\x1b\\][^\\x07]*(?:\\x07|\\x1b\\\\)";
const APC_PATTERN = "\\x1b_[^\\x07\\x1b]*(?:\\x07|\\x1b\\\\)";
const CSI_REGEX = new RegExp(CSI_PATTERN, "g");
const OSC_REGEX = new RegExp(OSC_PATTERN, "g");
const APC_REGEX = new RegExp(APC_PATTERN, "g");
const SELECT_LIST_LAYOUT = {
    minPrimaryColumnWidth: 12,
    maxPrimaryColumnWidth: 32,
    truncatePrimary: ({ text, maxWidth }) => truncateToWidth(text, maxWidth, "…"),
};
const ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS = 20;
function isWhitespaceChar(char) {
    return /\s/.test(char);
}
function isPunctuationChar(char) {
    return PUNCTUATION_REGEX.test(char);
}
function isPasteMarker(segment) {
    return segment.length >= 10 && PASTE_MARKER_SINGLE.test(segment);
}
function segmentWithMarkers(text, validIds) {
    if (validIds.size === 0 || !text.includes("[paste #")) {
        return segmenter.segment(text);
    }
    const markers = [];
    for (const match of text.matchAll(PASTE_MARKER_REGEX)) {
        const id = Number.parseInt(match[1] ?? "", 10);
        if (!validIds.has(id))
            continue;
        markers.push({ start: match.index ?? 0, end: (match.index ?? 0) + match[0].length });
    }
    if (markers.length === 0) {
        return segmenter.segment(text);
    }
    const baseSegments = segmenter.segment(text);
    const result = [];
    let markerIndex = 0;
    for (const seg of baseSegments) {
        while (markerIndex < markers.length && (markers[markerIndex]?.end ?? 0) <= seg.index) {
            markerIndex++;
        }
        const marker = markerIndex < markers.length ? markers[markerIndex] : null;
        if (marker && seg.index >= marker.start && seg.index < marker.end) {
            if (seg.index === marker.start) {
                result.push({
                    segment: text.slice(marker.start, marker.end),
                    index: marker.start,
                    input: text,
                });
            }
            continue;
        }
        result.push(seg);
    }
    return result;
}
function stripAnsiSequences(text) {
    if (!text.includes("\x1b"))
        return text;
    return text
        .replace(CSI_REGEX, "")
        .replace(OSC_REGEX, "")
        .replace(APC_REGEX, "")
        .replaceAll("\x1b", "");
}
function sanitizeInputText(text) {
    return text ? stripAnsiSequences(text) : text;
}
/**
 * Split a line into word-wrapped chunks.
 * Wraps at word boundaries when possible, falling back to character-level
 * wrapping for words longer than the available width.
 *
 * @param line - The text line to wrap
 * @param maxWidth - Maximum visible width per chunk
 * @returns Array of chunks with text and position information
 */
function wordWrapLine(line, maxWidth, preSegmented) {
    if (!line || maxWidth <= 0) {
        return [{ text: "", startIndex: 0, endIndex: 0 }];
    }
    const lineWidth = visibleWidth(line);
    if (lineWidth <= maxWidth) {
        return [{ text: line, startIndex: 0, endIndex: line.length }];
    }
    const chunks = [];
    const segments = preSegmented ?? [...segmenter.segment(line)];
    let currentWidth = 0;
    let chunkStart = 0;
    let wrapOppIndex = -1;
    let wrapOppWidth = 0;
    for (let i = 0; i < segments.length; i++) {
        const seg = segments[i];
        if (!seg)
            continue;
        const grapheme = seg.segment;
        const graphemeWidth = visibleWidth(grapheme);
        const charIndex = seg.index;
        const isWhitespace = !isPasteMarker(grapheme) && isWhitespaceChar(grapheme);
        if (currentWidth + graphemeWidth > maxWidth) {
            if (wrapOppIndex >= 0 && currentWidth - wrapOppWidth + graphemeWidth <= maxWidth) {
                chunks.push({
                    text: line.slice(chunkStart, wrapOppIndex),
                    startIndex: chunkStart,
                    endIndex: wrapOppIndex,
                });
                chunkStart = wrapOppIndex;
                currentWidth -= wrapOppWidth;
            }
            else if (chunkStart < charIndex) {
                chunks.push({
                    text: line.slice(chunkStart, charIndex),
                    startIndex: chunkStart,
                    endIndex: charIndex,
                });
                chunkStart = charIndex;
                currentWidth = 0;
            }
            wrapOppIndex = -1;
        }
        if (graphemeWidth > maxWidth) {
            const subChunks = wordWrapLine(grapheme, maxWidth);
            for (let j = 0; j < subChunks.length - 1; j++) {
                const subChunk = subChunks[j];
                if (!subChunk)
                    continue;
                chunks.push({
                    text: subChunk.text,
                    startIndex: charIndex + subChunk.startIndex,
                    endIndex: charIndex + subChunk.endIndex,
                });
            }
            const lastSubChunk = subChunks[subChunks.length - 1];
            if (lastSubChunk) {
                chunkStart = charIndex + lastSubChunk.startIndex;
                currentWidth = visibleWidth(lastSubChunk.text);
            }
            wrapOppIndex = -1;
            continue;
        }
        currentWidth += graphemeWidth;
        const next = segments[i + 1];
        if (isWhitespace && next && (isPasteMarker(next.segment) || !isWhitespaceChar(next.segment))) {
            wrapOppIndex = next.index;
            wrapOppWidth = currentWidth;
        }
    }
    chunks.push({ text: line.slice(chunkStart), startIndex: chunkStart, endIndex: line.length });
    return chunks;
}
export class Editor {
    state = {
        lines: [""],
        cursorLine: 0,
        cursorCol: 0,
    };
    theme;
    // Store last render width for cursor navigation
    lastWidth = 80;
    // Border color (can be changed dynamically)
    borderColor;
    // Autocomplete support
    autocompleteProvider;
    autocompleteList;
    autocompleteState = null;
    autocompletePrefix = "";
    autocompleteAbort;
    autocompleteDebounceTimer;
    autocompleteRequestTask = Promise.resolve();
    autocompleteStartToken = 0;
    autocompleteRequestId = 0;
    // Paste tracking for large pastes
    pastes = new Map();
    pasteCounter = 0;
    // Bracketed paste mode buffering
    pasteBuffer = "";
    isInPaste = false;
    // Prompt history for up/down navigation
    history = [];
    historyIndex = -1; // -1 = not browsing, 0 = most recent, 1 = older, etc.
    lastAction = null;
    undoStack = new UndoStack();
    onSubmit;
    onChange;
    onUiChange;
    disableSubmit = false;
    constructor(theme) {
        this.theme = theme;
        this.borderColor = theme.borderColor;
    }
    requestUiRender() {
        this.onUiChange?.();
    }
    validPasteIds() {
        return new Set(this.pastes.keys());
    }
    segment(text) {
        return segmentWithMarkers(text, this.validPasteIds());
    }
    normalizeText(text) {
        return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, "    ");
    }
    /** Wraps text in cursor styling (inverse video). Override in subclasses for theme-aware cursor. */
    cursorStyle(text) {
        return `\x1b[7m${text}\x1b[0m`;
    }
    setAutocompleteProvider(provider) {
        this.cancelAutocomplete();
        this.autocompleteProvider = provider;
    }
    setTheme(theme) {
        this.theme = theme;
        this.borderColor = theme.borderColor;
        this.cancelAutocomplete();
    }
    /**
     * Add a prompt to history for up/down arrow navigation.
     * Called after successful submission.
     */
    addToHistory(text) {
        const trimmed = text.trim();
        if (!trimmed)
            return;
        // Don't add consecutive duplicates
        if (this.history.length > 0 && this.history[0] === trimmed)
            return;
        this.history.unshift(trimmed);
        // Limit history size
        if (this.history.length > 100) {
            this.history.pop();
        }
    }
    isEditorEmpty() {
        return this.state.lines.length === 1 && this.state.lines[0] === "";
    }
    isOnFirstVisualLine() {
        const visualLines = this.buildVisualLineMap(this.lastWidth);
        const currentVisualLine = this.findCurrentVisualLine(visualLines);
        return currentVisualLine === 0;
    }
    isOnLastVisualLine() {
        const visualLines = this.buildVisualLineMap(this.lastWidth);
        const currentVisualLine = this.findCurrentVisualLine(visualLines);
        return currentVisualLine === visualLines.length - 1;
    }
    navigateHistory(direction) {
        this.lastAction = null;
        if (this.history.length === 0)
            return;
        const newIndex = this.historyIndex - direction; // Up(-1) increases index, Down(1) decreases
        if (newIndex < -1 || newIndex >= this.history.length)
            return;
        if (this.historyIndex === -1 && newIndex >= 0) {
            this.pushUndoSnapshot();
        }
        this.historyIndex = newIndex;
        if (this.historyIndex === -1) {
            // Returned to "current" state - clear editor
            this.setTextInternal("");
        }
        else {
            this.setTextInternal(this.history[this.historyIndex] || "");
        }
    }
    /** Internal setText that doesn't reset history state - used by navigateHistory */
    setTextInternal(text) {
        const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
        this.state.lines = lines.length === 0 ? [""] : lines;
        this.state.cursorLine = this.state.lines.length - 1;
        this.state.cursorCol = this.state.lines[this.state.cursorLine]?.length || 0;
        if (this.onChange) {
            this.onChange(this.getText());
        }
    }
    invalidate() {
        // No cached state to invalidate currently
    }
    render(width) {
        // Store width for cursor navigation
        this.lastWidth = width;
        const horizontal = this.borderColor("─");
        // Layout the text - use full width
        const layoutLines = this.layoutText(width);
        const result = [];
        // Render top border
        result.push(horizontal.repeat(width));
        // Render each layout line
        for (const layoutLine of layoutLines) {
            let displayText = layoutLine.text;
            let lineVisibleWidth = visibleWidth(layoutLine.text);
            // Add cursor if this line has it
            if (layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {
                const before = displayText.slice(0, layoutLine.cursorPos);
                const after = displayText.slice(layoutLine.cursorPos);
                if (after.length > 0) {
                    // Cursor is on a character (grapheme) - replace it with highlighted version
                    // Get the first segment from 'after'
                    const afterSegments = [...this.segment(after)];
                    const firstSegment = afterSegments[0]?.segment || "";
                    const restAfter = after.slice(firstSegment.length);
                    displayText = before + this.cursorStyle(firstSegment) + restAfter;
                    // lineVisibleWidth stays the same - we're replacing, not adding
                }
                else {
                    // Cursor is at the end - check if we have room for the space
                    if (lineVisibleWidth < width) {
                        // We have room - add highlighted space
                        displayText = before + this.cursorStyle(" ");
                        // lineVisibleWidth increases by 1 - we're adding a space
                        lineVisibleWidth = lineVisibleWidth + 1;
                    }
                    else {
                        // Line is at full width - use reverse video on last segment if possible
                        // or just show cursor at the end without adding space
                        const beforeSegments = [...this.segment(before)];
                        if (beforeSegments.length > 0) {
                            const lastSegment = beforeSegments[beforeSegments.length - 1]?.segment || "";
                            // Rebuild 'before' without the last segment
                            const beforeWithoutLast = beforeSegments
                                .slice(0, -1)
                                .map((g) => g.segment)
                                .join("");
                            displayText = beforeWithoutLast + this.cursorStyle(lastSegment);
                        }
                        // lineVisibleWidth stays the same
                    }
                }
            }
            // Calculate padding based on actual visible width
            const padding = " ".repeat(Math.max(0, width - lineVisibleWidth));
            // Render the line (no side borders, just horizontal lines above and below)
            result.push(displayText + padding);
        }
        // Render bottom border
        result.push(horizontal.repeat(width));
        // Add autocomplete list if active
        if (this.autocompleteState && this.autocompleteList) {
            const autocompleteResult = this.autocompleteList.render(width);
            result.push(...autocompleteResult);
        }
        return result;
    }
    handleInput(data) {
        const kb = getKeybindings();
        // Handle bracketed paste mode
        if (data.includes("\x1b[200~")) {
            this.isInPaste = true;
            this.pasteBuffer = "";
            data = data.replace("\x1b[200~", "");
        }
        if (this.isInPaste) {
            this.pasteBuffer += data;
            const endIndex = this.pasteBuffer.indexOf("\x1b[201~");
            if (endIndex !== -1) {
                const pasteContent = this.pasteBuffer.substring(0, endIndex);
                if (pasteContent.length > 0) {
                    this.handlePaste(pasteContent);
                }
                this.isInPaste = false;
                const remaining = this.pasteBuffer.substring(endIndex + 6);
                this.pasteBuffer = "";
                if (remaining.length > 0) {
                    this.handleInput(remaining);
                }
                return;
            }
            return;
        }
        if (data === "\\") {
            this.insertCharacter("\\");
            return;
        }
        // Ctrl+C - let parent handle (exit/clear)
        if (kb.matches(data, "tui.input.copy")) {
            return;
        }
        if (kb.matches(data, "tui.editor.undo")) {
            this.undo();
            return;
        }
        // Handle autocomplete mode
        if (this.autocompleteState && this.autocompleteList) {
            if (kb.matches(data, "tui.select.cancel")) {
                this.cancelAutocomplete();
                return;
            }
            if (kb.matches(data, "tui.select.up") || kb.matches(data, "tui.select.down")) {
                this.autocompleteList.handleInput(data);
                return;
            }
            if (kb.matches(data, "tui.input.tab")) {
                const selected = this.autocompleteList.getSelectedItem();
                if (selected && this.autocompleteProvider) {
                    this.pushUndoSnapshot();
                    this.lastAction = null;
                    const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
                    this.state.lines = result.lines;
                    this.state.cursorLine = result.cursorLine;
                    this.state.cursorCol = result.cursorCol;
                    const shouldRetrigger = this.autocompletePrefix.startsWith("@") &&
                        this.shouldRetriggerMentionAutocomplete(result.lines, result.cursorLine, result.cursorCol);
                    this.cancelAutocomplete();
                    if (this.onChange)
                        this.onChange(this.getText());
                    if (shouldRetrigger) {
                        this.tryTriggerAutocomplete();
                    }
                }
                return;
            }
            if (kb.matches(data, "tui.select.confirm")) {
                const selected = this.autocompleteList.getSelectedItem();
                if (selected && this.autocompleteProvider) {
                    this.pushUndoSnapshot();
                    this.lastAction = null;
                    const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
                    this.state.lines = result.lines;
                    this.state.cursorLine = result.cursorLine;
                    this.state.cursorCol = result.cursorCol;
                    if (this.autocompletePrefix.startsWith("/")) {
                        this.cancelAutocomplete();
                        // Fall through to submit
                    }
                    else {
                        const shouldRetrigger = this.autocompletePrefix.startsWith("@") &&
                            this.shouldRetriggerMentionAutocomplete(result.lines, result.cursorLine, result.cursorCol);
                        this.cancelAutocomplete();
                        if (this.onChange)
                            this.onChange(this.getText());
                        if (shouldRetrigger) {
                            this.tryTriggerAutocomplete();
                        }
                        return;
                    }
                }
            }
        }
        // Tab - trigger completion
        if (kb.matches(data, "tui.input.tab") && !this.autocompleteState) {
            this.handleTabCompletion();
            return;
        }
        // Deletion actions
        if (kb.matches(data, "tui.editor.deleteToLineEnd")) {
            this.deleteToEndOfLine();
            return;
        }
        if (kb.matches(data, "tui.editor.deleteToLineStart")) {
            this.deleteToStartOfLine();
            return;
        }
        if (kb.matches(data, "tui.editor.deleteWordBackward")) {
            this.deleteWordBackwards();
            return;
        }
        if (kb.matches(data, "tui.editor.deleteCharBackward") || matchesKey(data, "shift+backspace")) {
            this.handleBackspace();
            return;
        }
        if (kb.matches(data, "tui.editor.deleteCharForward") || matchesKey(data, "shift+delete")) {
            this.handleForwardDelete();
            return;
        }
        // Cursor movement actions
        if (kb.matches(data, "tui.editor.cursorLineStart")) {
            this.moveToLineStart();
            return;
        }
        if (kb.matches(data, "tui.editor.cursorLineEnd")) {
            this.moveToLineEnd();
            return;
        }
        if (kb.matches(data, "tui.editor.cursorWordLeft")) {
            this.moveWordBackwards();
            return;
        }
        if (kb.matches(data, "tui.editor.cursorWordRight")) {
            this.moveWordForwards();
            return;
        }
        // New line (Shift+Enter, Alt+Enter, etc.)
        if (kb.matches(data, "tui.input.newLine") ||
            (data.charCodeAt(0) === 10 && data.length > 1) ||
            data === "\x1b\r" ||
            data === "\x1b[13;2~" ||
            (data.length > 1 && data.includes("\x1b") && data.includes("\r")) ||
            (data === "\n" && data.length === 1) ||
            data === "\\\r") {
            this.addNewLine();
            return;
        }
        // Submit (Enter)
        if (kb.matches(data, "tui.input.submit")) {
            if (this.disableSubmit)
                return;
            let result = this.state.lines.join("\n").trim();
            result = this.expandPasteMarkers(result);
            this.state = { lines: [""], cursorLine: 0, cursorCol: 0 };
            this.pastes.clear();
            this.pasteCounter = 0;
            this.historyIndex = -1;
            this.undoStack.clear();
            this.lastAction = null;
            if (this.onChange)
                this.onChange("");
            if (this.onSubmit)
                this.onSubmit(result);
            return;
        }
        // Arrow key navigation (with history support)
        if (kb.matches(data, "tui.editor.cursorUp")) {
            if (this.isEditorEmpty()) {
                this.navigateHistory(-1);
            }
            else if (this.historyIndex > -1 && this.isOnFirstVisualLine()) {
                this.navigateHistory(-1);
            }
            else {
                this.moveCursor(-1, 0);
            }
            return;
        }
        if (kb.matches(data, "tui.editor.cursorDown")) {
            if (this.historyIndex > -1 && this.isOnLastVisualLine()) {
                this.navigateHistory(1);
            }
            else {
                this.moveCursor(1, 0);
            }
            return;
        }
        if (kb.matches(data, "tui.editor.cursorRight")) {
            this.moveCursor(0, 1);
            return;
        }
        if (kb.matches(data, "tui.editor.cursorLeft")) {
            this.moveCursor(0, -1);
            return;
        }
        // Shift+Space - insert regular space
        if (matchesKey(data, "shift+space")) {
            this.insertCharacter(" ");
            return;
        }
        const kittyPrintable = decodeKittyPrintable(data);
        if (kittyPrintable !== undefined) {
            this.insertCharacter(kittyPrintable);
            return;
        }
        // Regular characters
        if (data.charCodeAt(0) >= 32) {
            this.insertCharacter(data);
        }
    }
    layoutText(contentWidth) {
        const layoutLines = [];
        if (this.state.lines.length === 0 ||
            (this.state.lines.length === 1 && this.state.lines[0] === "")) {
            // Empty editor
            layoutLines.push({
                text: "",
                hasCursor: true,
                cursorPos: 0,
            });
            return layoutLines;
        }
        // Process each logical line
        for (let i = 0; i < this.state.lines.length; i++) {
            const line = this.state.lines[i] || "";
            const isCurrentLine = i === this.state.cursorLine;
            const lineVisibleWidth = visibleWidth(line);
            if (lineVisibleWidth <= contentWidth) {
                // Line fits in one layout line
                if (isCurrentLine) {
                    layoutLines.push({
                        text: line,
                        hasCursor: true,
                        cursorPos: this.state.cursorCol,
                    });
                }
                else {
                    layoutLines.push({
                        text: line,
                        hasCursor: false,
                    });
                }
            }
            else {
                // Line needs wrapping - use word-aware wrapping
                const chunks = wordWrapLine(line, contentWidth, [...this.segment(line)]);
                for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
                    const chunk = chunks[chunkIndex];
                    if (!chunk)
                        continue;
                    const cursorPos = this.state.cursorCol;
                    const isLastChunk = chunkIndex === chunks.length - 1;
                    // Determine if cursor is in this chunk
                    // For word-wrapped chunks, we need to handle the case where
                    // cursor might be in trimmed whitespace at end of chunk
                    let hasCursorInChunk = false;
                    let adjustedCursorPos = 0;
                    if (isCurrentLine) {
                        if (isLastChunk) {
                            // Last chunk: cursor belongs here if >= startIndex
                            hasCursorInChunk = cursorPos >= chunk.startIndex;
                            adjustedCursorPos = cursorPos - chunk.startIndex;
                        }
                        else {
                            // Non-last chunk: cursor belongs here if in range [startIndex, endIndex)
                            // But we need to handle the visual position in the trimmed text
                            hasCursorInChunk = cursorPos >= chunk.startIndex && cursorPos < chunk.endIndex;
                            if (hasCursorInChunk) {
                                adjustedCursorPos = cursorPos - chunk.startIndex;
                                // Clamp to text length (in case cursor was in trimmed whitespace)
                                if (adjustedCursorPos > chunk.text.length) {
                                    adjustedCursorPos = chunk.text.length;
                                }
                            }
                        }
                    }
                    if (hasCursorInChunk) {
                        layoutLines.push({
                            text: chunk.text,
                            hasCursor: true,
                            cursorPos: adjustedCursorPos,
                        });
                    }
                    else {
                        layoutLines.push({
                            text: chunk.text,
                            hasCursor: false,
                        });
                    }
                }
            }
        }
        return layoutLines;
    }
    getText() {
        return this.state.lines.join("\n");
    }
    /**
     * Get text with paste markers expanded to their actual content.
     * Use this when you need the full content (e.g., for external editor).
     */
    getExpandedText() {
        return this.expandPasteMarkers(this.state.lines.join("\n"));
    }
    expandPasteMarkers(text) {
        let result = text;
        for (const [pasteId, pasteContent] of this.pastes) {
            const markerRegex = new RegExp(`\\[paste #${pasteId}(?: (?:\\+\\d+ lines|\\d+ chars))?\\]`, "g");
            result = result.replace(markerRegex, () => pasteContent);
        }
        return result;
    }
    getLines() {
        return [...this.state.lines];
    }
    getCursor() {
        return { line: this.state.cursorLine, col: this.state.cursorCol };
    }
    setText(text) {
        this.lastAction = null;
        this.historyIndex = -1; // Exit history browsing mode
        const sanitized = sanitizeInputText(text);
        if (this.getText() !== sanitized) {
            this.pushUndoSnapshot();
        }
        this.setTextInternal(sanitized);
    }
    /**
     * Insert text at the current cursor position.
     * Used for programmatic insertion (e.g., clipboard image markers).
     */
    insertTextAtCursor(text) {
        if (!text)
            return;
        this.pushUndoSnapshot();
        this.lastAction = null;
        this.historyIndex = -1;
        this.insertTextAtCursorInternal(text);
    }
    insertTextAtCursorInternal(text) {
        if (!text)
            return;
        const sanitized = sanitizeInputText(text);
        if (!sanitized)
            return;
        this.historyIndex = -1;
        const normalized = this.normalizeText(sanitized);
        const insertedLines = normalized.split("\n");
        const currentLine = this.state.lines[this.state.cursorLine] || "";
        const beforeCursor = currentLine.slice(0, this.state.cursorCol);
        const afterCursor = currentLine.slice(this.state.cursorCol);
        if (insertedLines.length === 1) {
            this.state.lines[this.state.cursorLine] = beforeCursor + normalized + afterCursor;
            this.state.cursorCol += normalized.length;
        }
        else {
            this.state.lines = [
                ...this.state.lines.slice(0, this.state.cursorLine),
                beforeCursor + (insertedLines[0] || ""),
                ...insertedLines.slice(1, -1),
                (insertedLines[insertedLines.length - 1] || "") + afterCursor,
                ...this.state.lines.slice(this.state.cursorLine + 1),
            ];
            this.state.cursorLine += insertedLines.length - 1;
            this.state.cursorCol = (insertedLines[insertedLines.length - 1] || "").length;
        }
        if (this.onChange) {
            this.onChange(this.getText());
        }
        if (this.autocompleteState) {
            this.updateAutocomplete();
        }
    }
    // All the editor methods from before...
    insertCharacter(char) {
        const sanitized = sanitizeInputText(char);
        if (!sanitized)
            return;
        this.historyIndex = -1; // Exit history browsing mode
        if (isWhitespaceChar(sanitized) || this.lastAction !== "type-word") {
            this.pushUndoSnapshot();
        }
        this.lastAction = "type-word";
        const line = this.state.lines[this.state.cursorLine] || "";
        const before = line.slice(0, this.state.cursorCol);
        const after = line.slice(this.state.cursorCol);
        this.state.lines[this.state.cursorLine] = before + sanitized + after;
        this.state.cursorCol += sanitized.length;
        if (this.onChange) {
            this.onChange(this.getText());
        }
        // Check if we should trigger or update autocomplete
        if (!this.autocompleteState) {
            // Auto-trigger for "/" at the start of a line (slash commands)
            if (sanitized === "/" && this.isAtStartOfMessage()) {
                this.tryTriggerAutocomplete();
            }
            // Auto-trigger for "@" mention tags
            else if (sanitized === "@") {
                const currentLine = this.state.lines[this.state.cursorLine] || "";
                const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
                // Only trigger if @ is after whitespace or at start of line
                const charBeforeAt = textBeforeCursor[textBeforeCursor.length - 2];
                if (textBeforeCursor.length === 1 || charBeforeAt === " " || charBeforeAt === "\t") {
                    this.tryTriggerAutocomplete();
                }
            }
            // Also auto-trigger when typing letters in a slash command context
            else if (/[a-zA-Z0-9.\-_]/.test(sanitized)) {
                const currentLine = this.state.lines[this.state.cursorLine] || "";
                const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
                if (this.isInSlashCommandContext(textBeforeCursor)) {
                    this.tryTriggerAutocomplete();
                }
                else if (this.isMentionAutocompleteContext(textBeforeCursor)) {
                    this.tryTriggerAutocomplete();
                }
            }
        }
        else {
            this.updateAutocomplete();
        }
    }
    handlePaste(pastedText) {
        this.historyIndex = -1; // Exit history browsing mode
        this.lastAction = null;
        this.pushUndoSnapshot();
        // Clean the pasted text
        const sanitizedText = sanitizeInputText(pastedText);
        const cleanText = this.normalizeText(sanitizedText);
        // Filter out non-printable characters except newlines
        let filteredText = cleanText
            .split("")
            .filter((char) => char === "\n" || char.charCodeAt(0) >= 32)
            .join("");
        // If pasting a file path (starts with /, ~, or .) and the character before
        // the cursor is a word character, prepend a space for better readability
        if (/^[/~.]/.test(filteredText)) {
            const currentLine = this.state.lines[this.state.cursorLine] || "";
            const charBeforeCursor = this.state.cursorCol > 0 ? currentLine[this.state.cursorCol - 1] : "";
            if (charBeforeCursor && /\w/.test(charBeforeCursor)) {
                filteredText = ` ${filteredText}`;
            }
        }
        // Split into lines
        const pastedLines = filteredText.split("\n");
        // Check if this is a large paste (> 32 lines or > 2000 characters)
        const totalChars = filteredText.length;
        if (pastedLines.length > 32 || totalChars > 2000) {
            // Store the paste and insert a marker
            this.pasteCounter++;
            const pasteId = this.pasteCounter;
            this.pastes.set(pasteId, filteredText);
            // Insert marker like "[paste #1 +123 lines]" or "[paste #1 1234 chars]"
            const marker = pastedLines.length > 32
                ? `[paste #${pasteId} +${pastedLines.length} lines]`
                : `[paste #${pasteId} ${totalChars} chars]`;
            this.insertTextAtCursorInternal(marker);
            return;
        }
        this.insertTextAtCursorInternal(filteredText);
    }
    addNewLine() {
        this.historyIndex = -1; // Exit history browsing mode
        this.lastAction = null;
        this.pushUndoSnapshot();
        const currentLine = this.state.lines[this.state.cursorLine] || "";
        const before = currentLine.slice(0, this.state.cursorCol);
        const after = currentLine.slice(this.state.cursorCol);
        // Split current line
        this.state.lines[this.state.cursorLine] = before;
        this.state.lines.splice(this.state.cursorLine + 1, 0, after);
        // Move cursor to start of new line
        this.state.cursorLine++;
        this.state.cursorCol = 0;
        if (this.onChange) {
            this.onChange(this.getText());
        }
    }
    handleBackspace() {
        this.historyIndex = -1; // Exit history browsing mode
        this.lastAction = null;
        if (this.state.cursorCol > 0) {
            this.pushUndoSnapshot();
            // Delete grapheme before cursor (handles emojis, combining characters, etc.)
            const line = this.state.lines[this.state.cursorLine] || "";
            const beforeCursor = line.slice(0, this.state.cursorCol);
            // Find the last grapheme in the text before cursor
            const graphemes = [...this.segment(beforeCursor)];
            const lastGrapheme = graphemes[graphemes.length - 1];
            const graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1;
            const before = line.slice(0, this.state.cursorCol - graphemeLength);
            const after = line.slice(this.state.cursorCol);
            this.state.lines[this.state.cursorLine] = before + after;
            this.state.cursorCol -= graphemeLength;
        }
        else if (this.state.cursorLine > 0) {
            this.pushUndoSnapshot();
            // Merge with previous line
            const currentLine = this.state.lines[this.state.cursorLine] || "";
            const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
            this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
            this.state.lines.splice(this.state.cursorLine, 1);
            this.state.cursorLine--;
            this.state.cursorCol = previousLine.length;
        }
        if (this.onChange) {
            this.onChange(this.getText());
        }
        // Update or re-trigger autocomplete after backspace
        if (this.autocompleteState) {
            this.updateAutocomplete();
        }
        else {
            // If autocomplete was cancelled (no matches), re-trigger if we're in a completable context
            const currentLine = this.state.lines[this.state.cursorLine] || "";
            const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
            if (this.isInSlashCommandContext(textBeforeCursor)) {
                this.tryTriggerAutocomplete();
            }
            else if (this.isMentionAutocompleteContext(textBeforeCursor)) {
                this.tryTriggerAutocomplete();
            }
        }
    }
    moveToLineStart() {
        this.lastAction = null;
        this.state.cursorCol = 0;
    }
    moveToLineEnd() {
        this.lastAction = null;
        const currentLine = this.state.lines[this.state.cursorLine] || "";
        this.state.cursorCol = currentLine.length;
    }
    deleteToStartOfLine() {
        this.historyIndex = -1; // Exit history browsing mode
        this.lastAction = null;
        const currentLine = this.state.lines[this.state.cursorLine] || "";
        if (this.state.cursorCol > 0) {
            this.pushUndoSnapshot();
            // Delete from start of line up to cursor
            this.state.lines[this.state.cursorLine] = currentLine.slice(this.state.cursorCol);
            this.state.cursorCol = 0;
        }
        else if (this.state.cursorLine > 0) {
            this.pushUndoSnapshot();
            // At start of line - merge with previous line
            const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
            this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
            this.state.lines.splice(this.state.cursorLine, 1);
            this.state.cursorLine--;
            this.state.cursorCol = previousLine.length;
        }
        if (this.onChange) {
            this.onChange(this.getText());
        }
    }
    deleteToEndOfLine() {
        this.historyIndex = -1; // Exit history browsing mode
        this.lastAction = null;
        const currentLine = this.state.lines[this.state.cursorLine] || "";
        if (this.state.cursorCol < currentLine.length) {
            this.pushUndoSnapshot();
            // Delete from cursor to end of line
            this.state.lines[this.state.cursorLine] = currentLine.slice(0, this.state.cursorCol);
        }
        else if (this.state.cursorLine < this.state.lines.length - 1) {
            this.pushUndoSnapshot();
            // At end of line - merge with next line
            const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
            this.state.lines[this.state.cursorLine] = currentLine + nextLine;
            this.state.lines.splice(this.state.cursorLine + 1, 1);
        }
        if (this.onChange) {
            this.onChange(this.getText());
        }
    }
    deleteWordBackwards() {
        this.historyIndex = -1; // Exit history browsing mode
        this.lastAction = null;
        const currentLine = this.state.lines[this.state.cursorLine] || "";
        // If at start of line, behave like backspace at column 0 (merge with previous line)
        if (this.state.cursorCol === 0) {
            if (this.state.cursorLine > 0) {
                this.pushUndoSnapshot();
                const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
                this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
                this.state.lines.splice(this.state.cursorLine, 1);
                this.state.cursorLine--;
                this.state.cursorCol = previousLine.length;
            }
        }
        else {
            this.pushUndoSnapshot();
            const oldCursorCol = this.state.cursorCol;
            this.moveWordBackwards();
            const deleteFrom = this.state.cursorCol;
            this.state.cursorCol = oldCursorCol;
            this.state.lines[this.state.cursorLine] =
                currentLine.slice(0, deleteFrom) + currentLine.slice(this.state.cursorCol);
            this.state.cursorCol = deleteFrom;
        }
        if (this.onChange) {
            this.onChange(this.getText());
        }
    }
    handleForwardDelete() {
        this.historyIndex = -1; // Exit history browsing mode
        this.lastAction = null;
        const currentLine = this.state.lines[this.state.cursorLine] || "";
        if (this.state.cursorCol < currentLine.length) {
            this.pushUndoSnapshot();
            // Delete grapheme at cursor position (handles emojis, combining characters, etc.)
            const afterCursor = currentLine.slice(this.state.cursorCol);
            // Find the first grapheme at cursor
            const graphemes = [...this.segment(afterCursor)];
            const firstGrapheme = graphemes[0];
            const graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1;
            const before = currentLine.slice(0, this.state.cursorCol);
            const after = currentLine.slice(this.state.cursorCol + graphemeLength);
            this.state.lines[this.state.cursorLine] = before + after;
        }
        else if (this.state.cursorLine < this.state.lines.length - 1) {
            this.pushUndoSnapshot();
            // At end of line - merge with next line
            const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
            this.state.lines[this.state.cursorLine] = currentLine + nextLine;
            this.state.lines.splice(this.state.cursorLine + 1, 1);
        }
        if (this.onChange) {
            this.onChange(this.getText());
        }
        // Update or re-trigger autocomplete after forward delete
        if (this.autocompleteState) {
            this.updateAutocomplete();
        }
        else {
            const currentLine = this.state.lines[this.state.cursorLine] || "";
            const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
            if (this.isInSlashCommandContext(textBeforeCursor)) {
                this.tryTriggerAutocomplete();
            }
            else if (this.isMentionAutocompleteContext(textBeforeCursor)) {
                this.tryTriggerAutocomplete();
            }
        }
    }
    /**
     * Build a mapping from visual lines to logical positions.
     * Returns an array where each element represents a visual line with:
     * - logicalLine: index into this.state.lines
     * - startCol: starting column in the logical line
     * - length: length of this visual line segment
     */
    buildVisualLineMap(width) {
        const visualLines = [];
        for (let i = 0; i < this.state.lines.length; i++) {
            const line = this.state.lines[i] || "";
            const lineVisWidth = visibleWidth(line);
            if (line.length === 0) {
                // Empty line still takes one visual line
                visualLines.push({ logicalLine: i, startCol: 0, length: 0 });
            }
            else if (lineVisWidth <= width) {
                visualLines.push({ logicalLine: i, startCol: 0, length: line.length });
            }
            else {
                // Line needs wrapping - use word-aware wrapping
                const chunks = wordWrapLine(line, width, [...this.segment(line)]);
                for (const chunk of chunks) {
                    visualLines.push({
                        logicalLine: i,
                        startCol: chunk.startIndex,
                        length: chunk.endIndex - chunk.startIndex,
                    });
                }
            }
        }
        return visualLines;
    }
    /**
     * Find the visual line index for the current cursor position.
     */
    findCurrentVisualLine(visualLines) {
        for (let i = 0; i < visualLines.length; i++) {
            const vl = visualLines[i];
            if (!vl)
                continue;
            if (vl.logicalLine === this.state.cursorLine) {
                const colInSegment = this.state.cursorCol - vl.startCol;
                // Cursor is in this segment if it's within range
                // For the last segment of a logical line, cursor can be at length (end position)
                const isLastSegmentOfLine = i === visualLines.length - 1 || visualLines[i + 1]?.logicalLine !== vl.logicalLine;
                if (colInSegment >= 0 &&
                    (colInSegment < vl.length || (isLastSegmentOfLine && colInSegment <= vl.length))) {
                    return i;
                }
            }
        }
        // Fallback: return last visual line
        return visualLines.length - 1;
    }
    snapCursorToSegmentBoundary(logicalLine, movingUp) {
        for (const seg of this.segment(logicalLine)) {
            if (seg.index > this.state.cursorCol)
                break;
            if (seg.segment.length <= 1)
                continue;
            if (this.state.cursorCol < seg.index + seg.segment.length) {
                this.state.cursorCol = movingUp ? seg.index : seg.index + seg.segment.length;
                break;
            }
        }
    }
    moveCursor(deltaLine, deltaCol) {
        this.lastAction = null;
        const width = this.lastWidth;
        if (deltaLine !== 0) {
            // Build visual line map for navigation
            const visualLines = this.buildVisualLineMap(width);
            const currentVisualLine = this.findCurrentVisualLine(visualLines);
            // Calculate column position within current visual line
            const currentVL = visualLines[currentVisualLine];
            const visualCol = currentVL ? this.state.cursorCol - currentVL.startCol : 0;
            // Move to target visual line
            const targetVisualLine = currentVisualLine + deltaLine;
            if (targetVisualLine >= 0 && targetVisualLine < visualLines.length) {
                const targetVL = visualLines[targetVisualLine];
                if (targetVL) {
                    this.state.cursorLine = targetVL.logicalLine;
                    // Try to maintain visual column position, clamped to line length
                    const targetCol = targetVL.startCol + Math.min(visualCol, targetVL.length);
                    const logicalLine = this.state.lines[targetVL.logicalLine] || "";
                    this.state.cursorCol = Math.min(targetCol, logicalLine.length);
                    this.snapCursorToSegmentBoundary(logicalLine, deltaLine < 0);
                }
            }
        }
        if (deltaCol !== 0) {
            const currentLine = this.state.lines[this.state.cursorLine] || "";
            if (deltaCol > 0) {
                // Moving right - move by one grapheme (handles emojis, combining characters, etc.)
                if (this.state.cursorCol < currentLine.length) {
                    const afterCursor = currentLine.slice(this.state.cursorCol);
                    const graphemes = [...this.segment(afterCursor)];
                    const firstGrapheme = graphemes[0];
                    this.state.cursorCol += firstGrapheme ? firstGrapheme.segment.length : 1;
                }
                else if (this.state.cursorLine < this.state.lines.length - 1) {
                    // Wrap to start of next logical line
                    this.state.cursorLine++;
                    this.state.cursorCol = 0;
                }
            }
            else {
                // Moving left - move by one grapheme (handles emojis, combining characters, etc.)
                if (this.state.cursorCol > 0) {
                    const beforeCursor = currentLine.slice(0, this.state.cursorCol);
                    const graphemes = [...this.segment(beforeCursor)];
                    const lastGrapheme = graphemes[graphemes.length - 1];
                    this.state.cursorCol -= lastGrapheme ? lastGrapheme.segment.length : 1;
                }
                else if (this.state.cursorLine > 0) {
                    // Wrap to end of previous logical line
                    this.state.cursorLine--;
                    const prevLine = this.state.lines[this.state.cursorLine] || "";
                    this.state.cursorCol = prevLine.length;
                }
            }
        }
    }
    moveWordBackwards() {
        this.lastAction = null;
        const currentLine = this.state.lines[this.state.cursorLine] || "";
        // If at start of line, move to end of previous line
        if (this.state.cursorCol === 0) {
            if (this.state.cursorLine > 0) {
                this.state.cursorLine--;
                const prevLine = this.state.lines[this.state.cursorLine] || "";
                this.state.cursorCol = prevLine.length;
            }
            return;
        }
        const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
        const graphemes = [...this.segment(textBeforeCursor)];
        let newCol = this.state.cursorCol;
        // Skip trailing whitespace
        while (graphemes.length > 0 &&
            !isPasteMarker(graphemes[graphemes.length - 1]?.segment || "") &&
            isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "")) {
            newCol -= graphemes.pop()?.segment.length || 0;
        }
        if (graphemes.length > 0) {
            const lastGrapheme = graphemes[graphemes.length - 1]?.segment || "";
            if (isPasteMarker(lastGrapheme)) {
                newCol -= graphemes.pop()?.segment.length || 0;
            }
            else if (isPunctuationChar(lastGrapheme)) {
                // Skip punctuation run
                while (graphemes.length > 0 &&
                    isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "") &&
                    !isPasteMarker(graphemes[graphemes.length - 1]?.segment || "")) {
                    newCol -= graphemes.pop()?.segment.length || 0;
                }
            }
            else {
                // Skip word run
                while (graphemes.length > 0 &&
                    !isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "") &&
                    !isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "") &&
                    !isPasteMarker(graphemes[graphemes.length - 1]?.segment || "")) {
                    newCol -= graphemes.pop()?.segment.length || 0;
                }
            }
        }
        this.state.cursorCol = newCol;
    }
    moveWordForwards() {
        this.lastAction = null;
        const currentLine = this.state.lines[this.state.cursorLine] || "";
        // If at end of line, move to start of next line
        if (this.state.cursorCol >= currentLine.length) {
            if (this.state.cursorLine < this.state.lines.length - 1) {
                this.state.cursorLine++;
                this.state.cursorCol = 0;
            }
            return;
        }
        const textAfterCursor = currentLine.slice(this.state.cursorCol);
        const segments = this.segment(textAfterCursor);
        const iterator = segments[Symbol.iterator]();
        let next = iterator.next();
        let newCol = this.state.cursorCol;
        // Skip leading whitespace
        while (!next.done &&
            !isPasteMarker(next.value.segment) &&
            isWhitespaceChar(next.value.segment)) {
            newCol += next.value.segment.length;
            next = iterator.next();
        }
        if (!next.done) {
            const firstGrapheme = next.value.segment;
            if (isPasteMarker(firstGrapheme)) {
                newCol += firstGrapheme.length;
            }
            else if (isPunctuationChar(firstGrapheme)) {
                // Skip punctuation run
                while (!next.done &&
                    isPunctuationChar(next.value.segment) &&
                    !isPasteMarker(next.value.segment)) {
                    newCol += next.value.segment.length;
                    next = iterator.next();
                }
            }
            else {
                // Skip word run
                while (!next.done &&
                    !isWhitespaceChar(next.value.segment) &&
                    !isPunctuationChar(next.value.segment) &&
                    !isPasteMarker(next.value.segment)) {
                    newCol += next.value.segment.length;
                    next = iterator.next();
                }
            }
        }
        this.state.cursorCol = newCol;
    }
    isSlashMenuAllowed() {
        return this.state.lines.length === 1 && this.state.cursorLine === 0;
    }
    // Helper method to check if cursor is at start of message (for slash command detection)
    isAtStartOfMessage() {
        if (!this.isSlashMenuAllowed())
            return false;
        const currentLine = this.state.lines[this.state.cursorLine] || "";
        const beforeCursor = currentLine.slice(0, this.state.cursorCol);
        return beforeCursor.trim() === "" || beforeCursor.trim() === "/";
    }
    isInSlashCommandContext(textBeforeCursor) {
        return this.isSlashMenuAllowed() && textBeforeCursor.trimStart().startsWith("/");
    }
    isMentionAutocompleteContext(textBeforeCursor) {
        return /(?:^|[\s])@[^\s]*$/.test(textBeforeCursor);
    }
    shouldRetriggerMentionAutocomplete(lines, cursorLine, cursorCol) {
        const line = lines[cursorLine] ?? "";
        const beforeCursor = line.slice(0, cursorCol);
        return /(?:^|[\s])@@[a-z-]+:$/.test(beforeCursor);
    }
    getAutocompleteMatchPrefix(prefix) {
        if (prefix.startsWith("@@")) {
            const mentionMatch = prefix.match(/^@@[^:\s]+:(.*)$/);
            return mentionMatch ? (mentionMatch[1] ?? "") : prefix.slice(2);
        }
        if (prefix.startsWith("@") || prefix.startsWith("/")) {
            return prefix.slice(1);
        }
        return prefix;
    }
    getBestAutocompleteMatchIndex(items, prefix) {
        const matchPrefix = this.getAutocompleteMatchPrefix(prefix);
        if (!matchPrefix)
            return -1;
        let firstPrefixIndex = -1;
        for (let i = 0; i < items.length; i++) {
            const value = items[i]?.value;
            if (!value)
                continue;
            if (value === matchPrefix) {
                return i;
            }
            if (firstPrefixIndex === -1 && value.startsWith(matchPrefix)) {
                firstPrefixIndex = i;
            }
        }
        return firstPrefixIndex;
    }
    createAutocompleteList(_prefix, items) {
        return new SelectList(items, 5, this.theme.selectList, SELECT_LIST_LAYOUT);
    }
    tryTriggerAutocomplete(explicitTab = false) {
        this.requestAutocomplete({ force: false, explicitTab });
    }
    handleTabCompletion() {
        if (!this.autocompleteProvider)
            return;
        const currentLine = this.state.lines[this.state.cursorLine] || "";
        const beforeCursor = currentLine.slice(0, this.state.cursorCol);
        if (this.isInSlashCommandContext(beforeCursor) && !beforeCursor.trimStart().includes(" ")) {
            this.handleSlashCommandCompletion();
        }
        else {
            this.forceFileAutocomplete(true);
        }
    }
    handleSlashCommandCompletion() {
        this.requestAutocomplete({ force: false, explicitTab: true });
    }
    forceFileAutocomplete(explicitTab = false) {
        this.requestAutocomplete({ force: true, explicitTab });
    }
    requestAutocomplete(options) {
        if (!this.autocompleteProvider)
            return;
        if (options.force) {
            const provider = this.autocompleteProvider;
            const shouldTrigger = !provider.shouldTriggerFileCompletion ||
                provider.shouldTriggerFileCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol);
            if (!shouldTrigger) {
                return;
            }
        }
        this.cancelAutocompleteRequest();
        const startToken = ++this.autocompleteStartToken;
        const debounceMs = this.getAutocompleteDebounceMs(options);
        if (debounceMs > 0) {
            this.autocompleteDebounceTimer = setTimeout(() => {
                this.autocompleteDebounceTimer = undefined;
                void this.startAutocompleteRequest(startToken, options);
            }, debounceMs);
            return;
        }
        void this.startAutocompleteRequest(startToken, options);
    }
    async startAutocompleteRequest(startToken, options) {
        const previousTask = this.autocompleteRequestTask;
        this.autocompleteRequestTask = (async () => {
            await previousTask;
            if (startToken !== this.autocompleteStartToken || !this.autocompleteProvider) {
                return;
            }
            const controller = new AbortController();
            this.autocompleteAbort = controller;
            const requestId = ++this.autocompleteRequestId;
            const snapshotText = this.getText();
            const snapshotLine = this.state.cursorLine;
            const snapshotCol = this.state.cursorCol;
            await this.runAutocompleteRequest(requestId, controller, snapshotText, snapshotLine, snapshotCol, options);
        })();
        await this.autocompleteRequestTask;
    }
    getAutocompleteDebounceMs(options) {
        if (options.explicitTab || options.force) {
            return 0;
        }
        const currentLine = this.state.lines[this.state.cursorLine] || "";
        const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
        const isAttachmentContext = /(?:^|[ \t])@(?:"[^"]*|[^\s]*)$/.test(textBeforeCursor);
        return isAttachmentContext ? ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS : 0;
    }
    async runAutocompleteRequest(requestId, controller, snapshotText, snapshotLine, snapshotCol, options) {
        if (!this.autocompleteProvider)
            return;
        const suggestions = await this.autocompleteProvider.getSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol, { signal: controller.signal, force: options.force });
        if (!this.isAutocompleteRequestCurrent(requestId, controller, snapshotText, snapshotLine, snapshotCol)) {
            return;
        }
        this.autocompleteAbort = undefined;
        if (!suggestions || !Array.isArray(suggestions.items) || suggestions.items.length === 0) {
            this.cancelAutocomplete();
            return;
        }
        if (options.force && options.explicitTab && suggestions.items.length === 1) {
            const item = suggestions.items[0];
            if (!item) {
                this.cancelAutocomplete();
                return;
            }
            this.pushUndoSnapshot();
            this.lastAction = null;
            const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, item, suggestions.prefix);
            this.state.lines = result.lines;
            this.state.cursorLine = result.cursorLine;
            this.state.cursorCol = result.cursorCol;
            if (this.onChange)
                this.onChange(this.getText());
            this.requestUiRender();
            return;
        }
        this.applyAutocompleteSuggestions(suggestions, options.force ? "force" : "regular");
        this.requestUiRender();
    }
    isAutocompleteRequestCurrent(requestId, controller, snapshotText, snapshotLine, snapshotCol) {
        return (!controller.signal.aborted &&
            requestId === this.autocompleteRequestId &&
            this.getText() === snapshotText &&
            this.state.cursorLine === snapshotLine &&
            this.state.cursorCol === snapshotCol);
    }
    applyAutocompleteSuggestions(suggestions, state) {
        this.autocompletePrefix = suggestions.prefix;
        this.autocompleteList = this.createAutocompleteList(suggestions.prefix, suggestions.items);
        const bestMatchIndex = this.getBestAutocompleteMatchIndex(suggestions.items, suggestions.prefix);
        if (bestMatchIndex >= 0) {
            this.autocompleteList.setSelectedIndex(bestMatchIndex);
        }
        this.autocompleteState = state;
    }
    cancelAutocompleteRequest() {
        this.autocompleteStartToken += 1;
        if (this.autocompleteDebounceTimer) {
            clearTimeout(this.autocompleteDebounceTimer);
            this.autocompleteDebounceTimer = undefined;
        }
        this.autocompleteAbort?.abort();
        this.autocompleteAbort = undefined;
    }
    clearAutocompleteUi() {
        this.autocompleteState = null;
        this.autocompleteList = undefined;
        this.autocompletePrefix = "";
    }
    cancelAutocomplete() {
        this.cancelAutocompleteRequest();
        this.clearAutocompleteUi();
        this.requestUiRender();
    }
    isShowingAutocomplete() {
        return this.autocompleteState !== null;
    }
    updateAutocomplete() {
        if (!this.autocompleteState || !this.autocompleteProvider)
            return;
        this.requestAutocomplete({ force: this.autocompleteState === "force", explicitTab: false });
    }
    pushUndoSnapshot() {
        this.undoStack.push(this.state);
    }
    undo() {
        this.historyIndex = -1;
        const snapshot = this.undoStack.pop();
        if (!snapshot)
            return;
        this.state = snapshot;
        this.lastAction = null;
        if (this.onChange) {
            this.onChange(this.getText());
        }
        if (this.autocompleteState) {
            this.updateAutocomplete();
        }
    }
}
//# sourceMappingURL=editor.js.map