import { getEditorKeybindings } from '../keybindings.js';
import { matchesKey } from '../keys.js';
import { KillRing } from '../kill-ring.js';
import { extractInlineSkillAutocompleteToken } from '../skill-autocomplete.js';
import { CURSOR_MARKER } from '../tui.js';
import { UndoStack } from '../undo-stack.js';
import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from '../utils.js';
import { SelectList } from './select-list.js';
const segmenter = getSegmenter();
/**
 * 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
 */
export function wordWrapLine(line, maxWidth) {
  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 = [...segmenter.segment(line)];
  let currentWidth = 0;
  let chunkStart = 0;
  // Wrap opportunity: the position after the last whitespace before a non-whitespace
  // grapheme, i.e. where a line break is allowed.
  let wrapOppIndex = -1;
  let wrapOppWidth = 0;
  for (let i = 0; i < segments.length; i++) {
    const seg = segments[i];
    const grapheme = seg.segment;
    const gWidth = visibleWidth(grapheme);
    const charIndex = seg.index;
    const isWs = isWhitespaceChar(grapheme);
    // Overflow check before advancing.
    if (currentWidth + gWidth > maxWidth) {
      if (wrapOppIndex >= 0) {
        // Backtrack to last wrap opportunity.
        chunks.push({
          text: line.slice(chunkStart, wrapOppIndex),
          startIndex: chunkStart,
          endIndex: wrapOppIndex
        });
        chunkStart = wrapOppIndex;
        currentWidth -= wrapOppWidth;
      } else if (chunkStart < charIndex) {
        // No wrap opportunity: force-break at current position.
        chunks.push({
          text: line.slice(chunkStart, charIndex),
          startIndex: chunkStart,
          endIndex: charIndex
        });
        chunkStart = charIndex;
        currentWidth = 0;
      }
      wrapOppIndex = -1;
    }
    // Advance.
    currentWidth += gWidth;
    // Record wrap opportunity: whitespace followed by non-whitespace.
    // Multiple spaces join (no break between them); the break point is
    // after the last space before the next word.
    const next = segments[i + 1];
    if (isWs && next && !isWhitespaceChar(next.segment)) {
      wrapOppIndex = next.index;
      wrapOppWidth = currentWidth;
    }
  }
  // Push final chunk.
  chunks.push({ text: line.slice(chunkStart), startIndex: chunkStart, endIndex: line.length });
  return chunks;
}
// Kitty CSI-u sequences for printable keys, including optional shifted/base codepoints.
const KITTY_CSI_U_REGEX = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?u$/;
const KITTY_MOD_SHIFT = 1;
const KITTY_MOD_ALT = 2;
const KITTY_MOD_CTRL = 4;
// Decode a printable CSI-u sequence, preferring the shifted key when present.
function decodeKittyPrintable(data) {
  const match = data.match(KITTY_CSI_U_REGEX);
  if (!match) return undefined;
  // CSI-u groups: <codepoint>[:<shifted>[:<base>]];<mod>u
  const codepoint = Number.parseInt(match[1] ?? '', 10);
  if (!Number.isFinite(codepoint)) return undefined;
  const shiftedKey = match[2] && match[2].length > 0 ? Number.parseInt(match[2], 10) : undefined;
  const modValue = match[4] ? Number.parseInt(match[4], 10) : 1;
  // Modifiers are 1-indexed in CSI-u; normalize to our bitmask.
  const modifier = Number.isFinite(modValue) ? modValue - 1 : 0;
  // Ignore CSI-u sequences used for Alt/Ctrl shortcuts.
  if (modifier & (KITTY_MOD_ALT | KITTY_MOD_CTRL)) return undefined;
  // Prefer the shifted keycode when Shift is held.
  let effectiveCodepoint = codepoint;
  if (modifier & KITTY_MOD_SHIFT && typeof shiftedKey === 'number') {
    effectiveCodepoint = shiftedKey;
  }
  // Drop control characters or invalid codepoints.
  if (!Number.isFinite(effectiveCodepoint) || effectiveCodepoint < 32) return undefined;
  try {
    return String.fromCodePoint(effectiveCodepoint);
  } catch {
    return undefined;
  }
}
export class Editor {
  state = {
    lines: [''],
    cursorLine: 0,
    cursorCol: 0
  };
  /** Focusable interface - set by TUI when focus changes */
  focused = false;
  tui;
  theme;
  paddingX = 0;
  // Store last render width for cursor navigation
  lastWidth = 80;
  // Vertical scrolling support
  scrollOffset = 0;
  // Border color (can be changed dynamically)
  borderColor;
  // Autocomplete support
  autocompleteProvider;
  autocompleteList;
  autocompleteState = null;
  autocompletePrefix = '';
  autocompleteMaxVisible = 5;
  // 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.
  // Kill ring for Emacs-style kill/yank operations
  killRing = new KillRing();
  lastAction = null;
  // Character jump mode
  jumpMode = null;
  // Preferred visual column for vertical cursor movement (sticky column)
  preferredVisualCol = null;
  // Undo support
  undoStack = new UndoStack();
  onSubmit;
  onChange;
  disableSubmit = false;
  constructor(tui, theme, options = {}) {
    this.tui = tui;
    this.theme = theme;
    this.borderColor = theme.borderColor;
    const paddingX = options.paddingX ?? 0;
    this.paddingX = Number.isFinite(paddingX) ? Math.max(0, Math.floor(paddingX)) : 0;
    const maxVisible = options.autocompleteMaxVisible ?? 5;
    this.autocompleteMaxVisible = Number.isFinite(maxVisible)
      ? Math.max(3, Math.min(20, Math.floor(maxVisible)))
      : 5;
  }
  getPaddingX() {
    return this.paddingX;
  }
  setPaddingX(padding) {
    const newPadding = Number.isFinite(padding) ? Math.max(0, Math.floor(padding)) : 0;
    if (this.paddingX !== newPadding) {
      this.paddingX = newPadding;
      this.tui.requestRender();
    }
  }
  getAutocompleteMaxVisible() {
    return this.autocompleteMaxVisible;
  }
  setAutocompleteMaxVisible(maxVisible) {
    const newMaxVisible = Number.isFinite(maxVisible)
      ? Math.max(3, Math.min(20, Math.floor(maxVisible)))
      : 5;
    if (this.autocompleteMaxVisible !== newMaxVisible) {
      this.autocompleteMaxVisible = newMaxVisible;
      this.tui.requestRender();
    }
  }
  setAutocompleteProvider(provider) {
    this.autocompleteProvider = provider;
  }
  /**
   * 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;
    // Capture state when first entering history browsing mode
    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.setCursorCol(this.state.lines[this.state.cursorLine]?.length || 0);
    // Reset scroll - render() will adjust to show cursor
    this.scrollOffset = 0;
    if (this.onChange) {
      this.onChange(this.getText());
    }
  }
  invalidate() {
    // No cached state to invalidate currently
  }
  render(width) {
    const maxPadding = Math.max(0, Math.floor((width - 1) / 2));
    const paddingX = Math.min(this.paddingX, maxPadding);
    const contentWidth = Math.max(1, width - paddingX * 2);
    // Layout width: with padding the cursor can overflow into it,
    // without padding we reserve 1 column for the cursor.
    const layoutWidth = Math.max(1, contentWidth - (paddingX ? 0 : 1));
    // Store for cursor navigation (must match wrapping width)
    this.lastWidth = layoutWidth;
    const horizontal = this.borderColor('─');
    // Layout the text
    const layoutLines = this.layoutText(layoutWidth);
    // Calculate max visible lines: 30% of terminal height, minimum 5 lines
    const terminalRows = this.tui.terminal.rows;
    const maxVisibleLines = Math.max(5, Math.floor(terminalRows * 0.3));
    // Find the cursor line index in layoutLines
    let cursorLineIndex = layoutLines.findIndex((line) => line.hasCursor);
    if (cursorLineIndex === -1) cursorLineIndex = 0;
    // Adjust scroll offset to keep cursor visible
    if (cursorLineIndex < this.scrollOffset) {
      this.scrollOffset = cursorLineIndex;
    } else if (cursorLineIndex >= this.scrollOffset + maxVisibleLines) {
      this.scrollOffset = cursorLineIndex - maxVisibleLines + 1;
    }
    // Clamp scroll offset to valid range
    const maxScrollOffset = Math.max(0, layoutLines.length - maxVisibleLines);
    this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScrollOffset));
    // Get visible lines slice
    const visibleLines = layoutLines.slice(this.scrollOffset, this.scrollOffset + maxVisibleLines);
    const result = [];
    const leftPadding = ' '.repeat(paddingX);
    const rightPadding = leftPadding;
    // Render top border (with scroll indicator if scrolled down)
    if (this.scrollOffset > 0) {
      const indicator = `─── ↑ ${this.scrollOffset} more `;
      const remaining = width - visibleWidth(indicator);
      result.push(this.borderColor(indicator + '─'.repeat(Math.max(0, remaining))));
    } else {
      result.push(horizontal.repeat(width));
    }
    // Render each visible layout line
    // Emit hardware cursor marker only when focused and not showing autocomplete
    const emitCursorMarker = this.focused && !this.autocompleteState;
    for (const layoutLine of visibleLines) {
      let displayText = layoutLine.text;
      let lineVisibleWidth = visibleWidth(layoutLine.text);
      let cursorInPadding = false;
      // 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);
        // Hardware cursor marker (zero-width, emitted before fake cursor for IME positioning)
        const marker = emitCursorMarker ? CURSOR_MARKER : '';
        if (after.length > 0) {
          // Cursor is on a character (grapheme) - replace it with highlighted version
          // Get the first grapheme from 'after'
          const afterGraphemes = [...segmenter.segment(after)];
          const firstGrapheme = afterGraphemes[0]?.segment || '';
          const restAfter = after.slice(firstGrapheme.length);
          const cursor = `\x1b[7m${firstGrapheme}\x1b[0m`;
          displayText = before + marker + cursor + restAfter;
          // lineVisibleWidth stays the same - we're replacing, not adding
        } else {
          // Cursor is at the end - add highlighted space
          const cursor = '\x1b[7m \x1b[0m';
          displayText = before + marker + cursor;
          lineVisibleWidth = lineVisibleWidth + 1;
          // If cursor overflows content width into the padding, flag it
          if (lineVisibleWidth > contentWidth && paddingX > 0) {
            cursorInPadding = true;
          }
        }
      }
      // Calculate padding based on actual visible width
      const padding = ' '.repeat(Math.max(0, contentWidth - lineVisibleWidth));
      const lineRightPadding = cursorInPadding ? rightPadding.slice(1) : rightPadding;
      // Render the line (no side borders, just horizontal lines above and below)
      result.push(`${leftPadding}${displayText}${padding}${lineRightPadding}`);
    }
    // Render bottom border (with scroll indicator if more content below)
    const linesBelow = layoutLines.length - (this.scrollOffset + visibleLines.length);
    if (linesBelow > 0) {
      const indicator = `─── ↓ ${linesBelow} more `;
      const remaining = width - visibleWidth(indicator);
      result.push(this.borderColor(indicator + '─'.repeat(Math.max(0, remaining))));
    } else {
      result.push(horizontal.repeat(width));
    }
    // Add autocomplete list if active
    if (this.autocompleteState && this.autocompleteList) {
      const autocompleteResult = this.autocompleteList.render(contentWidth);
      for (const line of autocompleteResult) {
        const lineWidth = visibleWidth(line);
        const linePadding = ' '.repeat(Math.max(0, contentWidth - lineWidth));
        result.push(`${leftPadding}${line}${linePadding}${rightPadding}`);
      }
    }
    return result;
  }
  handleInput(data) {
    const kb = getEditorKeybindings();
    // Handle character jump mode (awaiting next character to jump to)
    if (this.jumpMode !== null) {
      // Cancel if the hotkey is pressed again
      if (kb.matches(data, 'jumpForward') || kb.matches(data, 'jumpBackward')) {
        this.jumpMode = null;
        return;
      }
      if (data.charCodeAt(0) >= 32) {
        // Printable character - perform the jump
        const direction = this.jumpMode;
        this.jumpMode = null;
        this.jumpToChar(data, direction);
        return;
      }
      // Control character - cancel and fall through to normal handling
      this.jumpMode = null;
    }
    // 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;
    }
    // Ctrl+C - let parent handle (exit/clear)
    if (kb.matches(data, 'copy')) {
      return;
    }
    // Undo
    if (kb.matches(data, 'undo')) {
      this.undo();
      return;
    }
    // Handle autocomplete mode
    if (this.autocompleteState && this.autocompleteList) {
      if (kb.matches(data, 'selectCancel')) {
        this.cancelAutocomplete();
        return;
      }
      if (kb.matches(data, 'selectUp') || kb.matches(data, 'selectDown')) {
        this.autocompleteList.handleInput(data);
        return;
      }
      if (kb.matches(data, '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.setCursorCol(result.cursorCol);
          this.cancelAutocomplete();
          if (this.onChange) this.onChange(this.getText());
        }
        return;
      }
      if (kb.matches(data, 'selectConfirm')) {
        const selected = this.autocompleteList.getSelectedItem();
        if (selected && this.autocompleteProvider) {
          const currentLine = this.state.lines[this.state.cursorLine] || '';
          const shouldSubmitAfterConfirm =
            this.autocompletePrefix.startsWith('/') &&
            this.shouldSubmitAfterSlashAutocomplete(
              currentLine,
              this.state.cursorCol,
              this.autocompletePrefix
            );
          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.setCursorCol(result.cursorCol);
          if (shouldSubmitAfterConfirm) {
            this.cancelAutocomplete();
            // Fall through to submit
          } else {
            this.cancelAutocomplete();
            if (this.onChange) this.onChange(this.getText());
            return;
          }
        }
      }
    }
    // Tab - trigger completion
    if (kb.matches(data, 'tab') && !this.autocompleteState) {
      this.handleTabCompletion();
      return;
    }
    // Deletion actions
    if (kb.matches(data, 'deleteToLineEnd')) {
      this.deleteToEndOfLine();
      return;
    }
    if (kb.matches(data, 'deleteToLineStart')) {
      this.deleteToStartOfLine();
      return;
    }
    if (kb.matches(data, 'deleteWordBackward')) {
      this.deleteWordBackwards();
      return;
    }
    if (kb.matches(data, 'deleteWordForward')) {
      this.deleteWordForward();
      return;
    }
    if (kb.matches(data, 'deleteCharBackward') || matchesKey(data, 'shift+backspace')) {
      this.handleBackspace();
      return;
    }
    if (kb.matches(data, 'deleteCharForward') || matchesKey(data, 'shift+delete')) {
      this.handleForwardDelete();
      return;
    }
    // Kill ring actions
    if (kb.matches(data, 'yank')) {
      this.yank();
      return;
    }
    if (kb.matches(data, 'yankPop')) {
      this.yankPop();
      return;
    }
    // Cursor movement actions
    if (kb.matches(data, 'cursorLineStart')) {
      this.moveToLineStart();
      return;
    }
    if (kb.matches(data, 'cursorLineEnd')) {
      this.moveToLineEnd();
      return;
    }
    if (kb.matches(data, 'cursorWordLeft')) {
      this.moveWordBackwards();
      return;
    }
    if (kb.matches(data, 'cursorWordRight')) {
      this.moveWordForwards();
      return;
    }
    // New line
    if (
      kb.matches(data, '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)
    ) {
      if (this.shouldSubmitOnBackslashEnter(data, kb)) {
        this.handleBackspace();
        this.submitValue();
        return;
      }
      this.addNewLine();
      return;
    }
    // Submit (Enter)
    if (kb.matches(data, 'submit')) {
      if (this.disableSubmit) return;
      // Workaround for terminals without Shift+Enter support:
      // If char before cursor is \, delete it and insert newline instead of submitting.
      const currentLine = this.state.lines[this.state.cursorLine] || '';
      if (this.state.cursorCol > 0 && currentLine[this.state.cursorCol - 1] === '\\') {
        this.handleBackspace();
        this.addNewLine();
        return;
      }
      this.submitValue();
      return;
    }
    // Arrow key navigation (with history support)
    if (kb.matches(data, 'cursorUp')) {
      if (this.isEditorEmpty()) {
        this.navigateHistory(-1);
      } else if (this.historyIndex > -1 && this.isOnFirstVisualLine()) {
        this.navigateHistory(-1);
      } else if (this.isOnFirstVisualLine()) {
        // Already at top - jump to start of line
        this.moveToLineStart();
      } else {
        this.moveCursor(-1, 0);
      }
      return;
    }
    if (kb.matches(data, 'cursorDown')) {
      if (this.historyIndex > -1 && this.isOnLastVisualLine()) {
        this.navigateHistory(1);
      } else if (this.isOnLastVisualLine()) {
        // Already at bottom - jump to end of line
        this.moveToLineEnd();
      } else {
        this.moveCursor(1, 0);
      }
      return;
    }
    if (kb.matches(data, 'cursorRight')) {
      this.moveCursor(0, 1);
      return;
    }
    if (kb.matches(data, 'cursorLeft')) {
      this.moveCursor(0, -1);
      return;
    }
    // Page up/down - scroll by page and move cursor
    if (kb.matches(data, 'pageUp')) {
      this.pageScroll(-1);
      return;
    }
    if (kb.matches(data, 'pageDown')) {
      this.pageScroll(1);
      return;
    }
    // Character jump mode triggers
    if (kb.matches(data, 'jumpForward')) {
      this.jumpMode = 'forward';
      return;
    }
    if (kb.matches(data, 'jumpBackward')) {
      this.jumpMode = 'backward';
      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);
        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() {
    let result = this.state.lines.join('\n');
    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
    // Push undo snapshot if content differs (makes programmatic changes undoable)
    if (this.getText() !== text) {
      this.pushUndoSnapshot();
    }
    this.setTextInternal(text);
  }
  /**
   * Insert text at the current cursor position.
   * Used for programmatic insertion (e.g., clipboard image markers).
   * This is atomic for undo - single undo restores entire pre-insert state.
   */
  insertTextAtCursor(text) {
    if (!text) return;
    this.pushUndoSnapshot();
    this.lastAction = null;
    this.historyIndex = -1;
    this.insertTextAtCursorInternal(text);
  }
  /**
   * Internal text insertion at cursor. Handles single and multi-line text.
   * Does not push undo snapshots or trigger autocomplete - caller is responsible.
   * Normalizes line endings and calls onChange once at the end.
   */
  insertTextAtCursorInternal(text) {
    if (!text) return;
    // Normalize line endings
    const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
    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) {
      // Single line - insert at cursor position
      this.state.lines[this.state.cursorLine] = beforeCursor + normalized + afterCursor;
      this.setCursorCol(this.state.cursorCol + normalized.length);
    } else {
      // Multi-line insertion
      this.state.lines = [
        // All lines before current line
        ...this.state.lines.slice(0, this.state.cursorLine),
        // The first inserted line merged with text before cursor
        beforeCursor + insertedLines[0],
        // All middle inserted lines
        ...insertedLines.slice(1, -1),
        // The last inserted line with text after cursor
        insertedLines[insertedLines.length - 1] + afterCursor,
        // All lines after current line
        ...this.state.lines.slice(this.state.cursorLine + 1)
      ];
      this.state.cursorLine += insertedLines.length - 1;
      this.setCursorCol((insertedLines[insertedLines.length - 1] || '').length);
    }
    if (this.onChange) {
      this.onChange(this.getText());
    }
  }
  // All the editor methods from before...
  insertCharacter(char, skipUndoCoalescing) {
    this.historyIndex = -1; // Exit history browsing mode
    // Undo coalescing (fish-style):
    // - Consecutive word chars coalesce into one undo unit
    // - Space captures state before itself (so undo removes space+following word together)
    // - Each space is separately undoable
    // Skip coalescing when called from atomic operations (e.g., handlePaste)
    if (!skipUndoCoalescing) {
      if (isWhitespaceChar(char) || 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 + char + after;
    this.setCursorCol(this.state.cursorCol + char.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) and inline skill selection.
      if (char === '/') {
        this.tryTriggerAutocomplete();
      }
      // Auto-trigger for "@" file reference (fuzzy search)
      else if (char === '@') {
        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(char)) {
        const currentLine = this.state.lines[this.state.cursorLine] || '';
        const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
        // Check if we're in a slash command (with or without space for arguments)
        if (this.isInSlashCommandContext(textBeforeCursor)) {
          this.tryTriggerAutocomplete();
        }
        // Check if we're in an @ file reference context
        else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
          this.tryTriggerAutocomplete();
        }
      }
    } else {
      this.updateAutocomplete();
    }
  }
  handlePaste(pastedText) {
    this.historyIndex = -1; // Exit history browsing mode
    this.lastAction = null;
    this.pushUndoSnapshot();
    // Clean the pasted text
    const cleanText = pastedText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
    // Convert tabs to spaces (4 spaces per tab)
    const tabExpandedText = cleanText.replace(/\t/g, '    ');
    // Filter out non-printable characters except newlines
    let filteredText = tabExpandedText
      .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 to check for large paste
    const pastedLines = filteredText.split('\n');
    // Check if this is a large paste (> 10 lines or > 1000 characters)
    const totalChars = filteredText.length;
    if (pastedLines.length > 10 || totalChars > 1000) {
      // 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 > 10
          ? `[paste #${pasteId} +${pastedLines.length} lines]`
          : `[paste #${pasteId} ${totalChars} chars]`;
      this.insertTextAtCursorInternal(marker);
      return;
    }
    if (pastedLines.length === 1) {
      // Single line - insert character by character to trigger autocomplete
      for (const char of filteredText) {
        this.insertCharacter(char, true);
      }
      return;
    }
    // Multi-line paste - use direct state manipulation
    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.setCursorCol(0);
    if (this.onChange) {
      this.onChange(this.getText());
    }
  }
  shouldSubmitOnBackslashEnter(data, kb) {
    if (this.disableSubmit) return false;
    if (!matchesKey(data, 'enter')) return false;
    const submitKeys = kb.getKeys('submit');
    const hasShiftEnter = submitKeys.includes('shift+enter') || submitKeys.includes('shift+return');
    if (!hasShiftEnter) return false;
    const currentLine = this.state.lines[this.state.cursorLine] || '';
    return this.state.cursorCol > 0 && currentLine[this.state.cursorCol - 1] === '\\';
  }
  submitValue() {
    let result = this.state.lines.join('\n').trim();
    for (const [pasteId, pasteContent] of this.pastes) {
      const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, 'g');
      result = result.replace(markerRegex, pasteContent);
    }
    this.state = { lines: [''], cursorLine: 0, cursorCol: 0 };
    this.pastes.clear();
    this.pasteCounter = 0;
    this.historyIndex = -1;
    this.scrollOffset = 0;
    this.undoStack.clear();
    this.lastAction = null;
    if (this.onChange) this.onChange('');
    if (this.onSubmit) this.onSubmit(result);
  }
  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 = [...segmenter.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.setCursorCol(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.setCursorCol(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);
      // Slash command context
      if (this.isInSlashCommandContext(textBeforeCursor)) {
        this.tryTriggerAutocomplete();
      }
      // @ file reference context
      else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
        this.tryTriggerAutocomplete();
      }
    }
  }
  /**
   * Set cursor column and clear preferredVisualCol.
   * Use this for all non-vertical cursor movements to reset sticky column behavior.
   */
  setCursorCol(col) {
    this.state.cursorCol = col;
    this.preferredVisualCol = null;
  }
  /**
   * Move cursor to a target visual line, applying sticky column logic.
   * Shared by moveCursor() and pageScroll().
   */
  moveToVisualLine(visualLines, currentVisualLine, targetVisualLine) {
    const currentVL = visualLines[currentVisualLine];
    const targetVL = visualLines[targetVisualLine];
    if (currentVL && targetVL) {
      const currentVisualCol = this.state.cursorCol - currentVL.startCol;
      // For non-last segments, clamp to length-1 to stay within the segment
      const isLastSourceSegment =
        currentVisualLine === visualLines.length - 1 ||
        visualLines[currentVisualLine + 1]?.logicalLine !== currentVL.logicalLine;
      const sourceMaxVisualCol = isLastSourceSegment
        ? currentVL.length
        : Math.max(0, currentVL.length - 1);
      const isLastTargetSegment =
        targetVisualLine === visualLines.length - 1 ||
        visualLines[targetVisualLine + 1]?.logicalLine !== targetVL.logicalLine;
      const targetMaxVisualCol = isLastTargetSegment
        ? targetVL.length
        : Math.max(0, targetVL.length - 1);
      const moveToVisualCol = this.computeVerticalMoveColumn(
        currentVisualCol,
        sourceMaxVisualCol,
        targetMaxVisualCol
      );
      // Set cursor position
      this.state.cursorLine = targetVL.logicalLine;
      const targetCol = targetVL.startCol + moveToVisualCol;
      const logicalLine = this.state.lines[targetVL.logicalLine] || '';
      this.state.cursorCol = Math.min(targetCol, logicalLine.length);
    }
  }
  /**
   * Compute the target visual column for vertical cursor movement.
   * Implements the sticky column decision table:
   *
   * | P | S | T | U | Scenario                                             | Set Preferred | Move To     |
   * |---|---|---|---| ---------------------------------------------------- |---------------|-------------|
   * | 0 | * | 0 | - | Start nav, target fits                               | null          | current     |
   * | 0 | * | 1 | - | Start nav, target shorter                            | current       | target end  |
   * | 1 | 0 | 0 | 0 | Clamped, target fits preferred                       | null          | preferred   |
   * | 1 | 0 | 0 | 1 | Clamped, target longer but still can't fit preferred | keep          | target end  |
   * | 1 | 0 | 1 | - | Clamped, target even shorter                         | keep          | target end  |
   * | 1 | 1 | 0 | - | Rewrapped, target fits current                       | null          | current     |
   * | 1 | 1 | 1 | - | Rewrapped, target shorter than current               | current       | target end  |
   *
   * Where:
   * - P = preferred col is set
   * - S = cursor in middle of source line (not clamped to end)
   * - T = target line shorter than current visual col
   * - U = target line shorter than preferred col
   */
  computeVerticalMoveColumn(currentVisualCol, sourceMaxVisualCol, targetMaxVisualCol) {
    const hasPreferred = this.preferredVisualCol !== null; // P
    const cursorInMiddle = currentVisualCol < sourceMaxVisualCol; // S
    const targetTooShort = targetMaxVisualCol < currentVisualCol; // T
    if (!hasPreferred || cursorInMiddle) {
      if (targetTooShort) {
        // Cases 2 and 7
        this.preferredVisualCol = currentVisualCol;
        return targetMaxVisualCol;
      }
      // Cases 1 and 6
      this.preferredVisualCol = null;
      return currentVisualCol;
    }
    const targetCantFitPreferred = targetMaxVisualCol < this.preferredVisualCol; // U
    if (targetTooShort || targetCantFitPreferred) {
      // Cases 4 and 5
      return targetMaxVisualCol;
    }
    // Case 3
    const result = this.preferredVisualCol;
    this.preferredVisualCol = null;
    return result;
  }
  moveToLineStart() {
    this.lastAction = null;
    this.setCursorCol(0);
  }
  moveToLineEnd() {
    this.lastAction = null;
    const currentLine = this.state.lines[this.state.cursorLine] || '';
    this.setCursorCol(currentLine.length);
  }
  deleteToStartOfLine() {
    this.historyIndex = -1; // Exit history browsing mode
    const currentLine = this.state.lines[this.state.cursorLine] || '';
    if (this.state.cursorCol > 0) {
      this.pushUndoSnapshot();
      // Calculate text to be deleted and save to kill ring (backward deletion = prepend)
      const deletedText = currentLine.slice(0, this.state.cursorCol);
      this.killRing.push(deletedText, { prepend: true, accumulate: this.lastAction === 'kill' });
      this.lastAction = 'kill';
      // Delete from start of line up to cursor
      this.state.lines[this.state.cursorLine] = currentLine.slice(this.state.cursorCol);
      this.setCursorCol(0);
    } else if (this.state.cursorLine > 0) {
      this.pushUndoSnapshot();
      // At start of line - merge with previous line, treating newline as deleted text
      this.killRing.push('\n', { prepend: true, accumulate: this.lastAction === 'kill' });
      this.lastAction = 'kill';
      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.setCursorCol(previousLine.length);
    }
    if (this.onChange) {
      this.onChange(this.getText());
    }
  }
  deleteToEndOfLine() {
    this.historyIndex = -1; // Exit history browsing mode
    const currentLine = this.state.lines[this.state.cursorLine] || '';
    if (this.state.cursorCol < currentLine.length) {
      this.pushUndoSnapshot();
      // Calculate text to be deleted and save to kill ring (forward deletion = append)
      const deletedText = currentLine.slice(this.state.cursorCol);
      this.killRing.push(deletedText, { prepend: false, accumulate: this.lastAction === 'kill' });
      this.lastAction = 'kill';
      // 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, treating newline as deleted text
      this.killRing.push('\n', { prepend: false, accumulate: this.lastAction === 'kill' });
      this.lastAction = 'kill';
      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
    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();
        // Treat newline as deleted text (backward deletion = prepend)
        this.killRing.push('\n', { prepend: true, accumulate: this.lastAction === 'kill' });
        this.lastAction = 'kill';
        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.setCursorCol(previousLine.length);
      }
    } else {
      this.pushUndoSnapshot();
      // Save lastAction before cursor movement (moveWordBackwards resets it)
      const wasKill = this.lastAction === 'kill';
      const oldCursorCol = this.state.cursorCol;
      this.moveWordBackwards();
      const deleteFrom = this.state.cursorCol;
      this.setCursorCol(oldCursorCol);
      const deletedText = currentLine.slice(deleteFrom, this.state.cursorCol);
      this.killRing.push(deletedText, { prepend: true, accumulate: wasKill });
      this.lastAction = 'kill';
      this.state.lines[this.state.cursorLine] =
        currentLine.slice(0, deleteFrom) + currentLine.slice(this.state.cursorCol);
      this.setCursorCol(deleteFrom);
    }
    if (this.onChange) {
      this.onChange(this.getText());
    }
  }
  deleteWordForward() {
    this.historyIndex = -1; // Exit history browsing mode
    const currentLine = this.state.lines[this.state.cursorLine] || '';
    // If at end of line, merge with next line (delete the newline)
    if (this.state.cursorCol >= currentLine.length) {
      if (this.state.cursorLine < this.state.lines.length - 1) {
        this.pushUndoSnapshot();
        // Treat newline as deleted text (forward deletion = append)
        this.killRing.push('\n', { prepend: false, accumulate: this.lastAction === 'kill' });
        this.lastAction = 'kill';
        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);
      }
    } else {
      this.pushUndoSnapshot();
      // Save lastAction before cursor movement (moveWordForwards resets it)
      const wasKill = this.lastAction === 'kill';
      const oldCursorCol = this.state.cursorCol;
      this.moveWordForwards();
      const deleteTo = this.state.cursorCol;
      this.setCursorCol(oldCursorCol);
      const deletedText = currentLine.slice(this.state.cursorCol, deleteTo);
      this.killRing.push(deletedText, { prepend: false, accumulate: wasKill });
      this.lastAction = 'kill';
      this.state.lines[this.state.cursorLine] =
        currentLine.slice(0, this.state.cursorCol) + currentLine.slice(deleteTo);
    }
    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 = [...segmenter.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);
      // Slash command context
      if (this.isInSlashCommandContext(textBeforeCursor)) {
        this.tryTriggerAutocomplete();
      }
      // @ file reference context
      else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
        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);
        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;
  }
  moveCursor(deltaLine, deltaCol) {
    this.lastAction = null;
    const visualLines = this.buildVisualLineMap(this.lastWidth);
    const currentVisualLine = this.findCurrentVisualLine(visualLines);
    if (deltaLine !== 0) {
      const targetVisualLine = currentVisualLine + deltaLine;
      if (targetVisualLine >= 0 && targetVisualLine < visualLines.length) {
        this.moveToVisualLine(visualLines, currentVisualLine, targetVisualLine);
      }
    }
    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 = [...segmenter.segment(afterCursor)];
          const firstGrapheme = graphemes[0];
          this.setCursorCol(
            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.setCursorCol(0);
        } else {
          // At end of last line - can't move, but set preferredVisualCol for up/down navigation
          const currentVL = visualLines[currentVisualLine];
          if (currentVL) {
            this.preferredVisualCol = this.state.cursorCol - currentVL.startCol;
          }
        }
      } 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 = [...segmenter.segment(beforeCursor)];
          const lastGrapheme = graphemes[graphemes.length - 1];
          this.setCursorCol(
            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.setCursorCol(prevLine.length);
        }
      }
    }
  }
  /**
   * Scroll by a page (direction: -1 for up, 1 for down).
   * Moves cursor by the page size while keeping it in bounds.
   */
  pageScroll(direction) {
    this.lastAction = null;
    const terminalRows = this.tui.terminal.rows;
    const pageSize = Math.max(5, Math.floor(terminalRows * 0.3));
    const visualLines = this.buildVisualLineMap(this.lastWidth);
    const currentVisualLine = this.findCurrentVisualLine(visualLines);
    const targetVisualLine = Math.max(
      0,
      Math.min(visualLines.length - 1, currentVisualLine + direction * pageSize)
    );
    this.moveToVisualLine(visualLines, currentVisualLine, targetVisualLine);
  }
  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.setCursorCol(prevLine.length);
      }
      return;
    }
    const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
    const graphemes = [...segmenter.segment(textBeforeCursor)];
    let newCol = this.state.cursorCol;
    // Skip trailing whitespace
    while (
      graphemes.length > 0 &&
      isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || '')
    ) {
      newCol -= graphemes.pop()?.segment.length || 0;
    }
    if (graphemes.length > 0) {
      const lastGrapheme = graphemes[graphemes.length - 1]?.segment || '';
      if (isPunctuationChar(lastGrapheme)) {
        // Skip punctuation run
        while (
          graphemes.length > 0 &&
          isPunctuationChar(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 || '')
        ) {
          newCol -= graphemes.pop()?.segment.length || 0;
        }
      }
    }
    this.setCursorCol(newCol);
  }
  /**
   * Yank (paste) the most recent kill ring entry at cursor position.
   */
  yank() {
    if (this.killRing.length === 0) return;
    this.pushUndoSnapshot();
    const text = this.killRing.peek();
    this.insertYankedText(text);
    this.lastAction = 'yank';
  }
  /**
   * Cycle through kill ring (only works immediately after yank or yank-pop).
   * Replaces the last yanked text with the previous entry in the ring.
   */
  yankPop() {
    // Only works if we just yanked and have more than one entry
    if (this.lastAction !== 'yank' || this.killRing.length <= 1) return;
    this.pushUndoSnapshot();
    // Delete the previously yanked text (still at end of ring before rotation)
    this.deleteYankedText();
    // Rotate the ring: move end to front
    this.killRing.rotate();
    // Insert the new most recent entry (now at end after rotation)
    const text = this.killRing.peek();
    this.insertYankedText(text);
    this.lastAction = 'yank';
  }
  /**
   * Insert text at cursor position (used by yank operations).
   */
  insertYankedText(text) {
    this.historyIndex = -1; // Exit history browsing mode
    const lines = text.split('\n');
    if (lines.length === 1) {
      // Single line - insert at cursor
      const currentLine = this.state.lines[this.state.cursorLine] || '';
      const before = currentLine.slice(0, this.state.cursorCol);
      const after = currentLine.slice(this.state.cursorCol);
      this.state.lines[this.state.cursorLine] = before + text + after;
      this.setCursorCol(this.state.cursorCol + text.length);
    } else {
      // Multi-line insert
      const currentLine = this.state.lines[this.state.cursorLine] || '';
      const before = currentLine.slice(0, this.state.cursorCol);
      const after = currentLine.slice(this.state.cursorCol);
      // First line merges with text before cursor
      this.state.lines[this.state.cursorLine] = before + (lines[0] || '');
      // Insert middle lines
      for (let i = 1; i < lines.length - 1; i++) {
        this.state.lines.splice(this.state.cursorLine + i, 0, lines[i] || '');
      }
      // Last line merges with text after cursor
      const lastLineIndex = this.state.cursorLine + lines.length - 1;
      this.state.lines.splice(lastLineIndex, 0, (lines[lines.length - 1] || '') + after);
      // Update cursor position
      this.state.cursorLine = lastLineIndex;
      this.setCursorCol((lines[lines.length - 1] || '').length);
    }
    if (this.onChange) {
      this.onChange(this.getText());
    }
  }
  /**
   * Delete the previously yanked text (used by yank-pop).
   * The yanked text is derived from killRing[end] since it hasn't been rotated yet.
   */
  deleteYankedText() {
    const yankedText = this.killRing.peek();
    if (!yankedText) return;
    const yankLines = yankedText.split('\n');
    if (yankLines.length === 1) {
      // Single line - delete backward from cursor
      const currentLine = this.state.lines[this.state.cursorLine] || '';
      const deleteLen = yankedText.length;
      const before = currentLine.slice(0, this.state.cursorCol - deleteLen);
      const after = currentLine.slice(this.state.cursorCol);
      this.state.lines[this.state.cursorLine] = before + after;
      this.setCursorCol(this.state.cursorCol - deleteLen);
    } else {
      // Multi-line delete - cursor is at end of last yanked line
      const startLine = this.state.cursorLine - (yankLines.length - 1);
      const startCol = (this.state.lines[startLine] || '').length - (yankLines[0] || '').length;
      // Get text after cursor on current line
      const afterCursor = (this.state.lines[this.state.cursorLine] || '').slice(
        this.state.cursorCol
      );
      // Get text before yank start position
      const beforeYank = (this.state.lines[startLine] || '').slice(0, startCol);
      // Remove all lines from startLine to cursorLine and replace with merged line
      this.state.lines.splice(startLine, yankLines.length, beforeYank + afterCursor);
      // Update cursor
      this.state.cursorLine = startLine;
      this.setCursorCol(startCol);
    }
    if (this.onChange) {
      this.onChange(this.getText());
    }
  }
  pushUndoSnapshot() {
    this.undoStack.push(this.state);
  }
  undo() {
    this.historyIndex = -1; // Exit history browsing mode
    const snapshot = this.undoStack.pop();
    if (!snapshot) return;
    Object.assign(this.state, snapshot);
    this.lastAction = null;
    this.preferredVisualCol = null;
    if (this.onChange) {
      this.onChange(this.getText());
    }
  }
  /**
   * Jump to the first occurrence of a character in the specified direction.
   * Multi-line search. Case-sensitive. Skips the current cursor position.
   */
  jumpToChar(char, direction) {
    this.lastAction = null;
    const isForward = direction === 'forward';
    const lines = this.state.lines;
    const end = isForward ? lines.length : -1;
    const step = isForward ? 1 : -1;
    for (let lineIdx = this.state.cursorLine; lineIdx !== end; lineIdx += step) {
      const line = lines[lineIdx] || '';
      const isCurrentLine = lineIdx === this.state.cursorLine;
      // Current line: start after/before cursor; other lines: search full line
      const searchFrom = isCurrentLine
        ? isForward
          ? this.state.cursorCol + 1
          : this.state.cursorCol - 1
        : undefined;
      const idx = isForward ? line.indexOf(char, searchFrom) : line.lastIndexOf(char, searchFrom);
      if (idx !== -1) {
        this.state.cursorLine = lineIdx;
        this.setCursorCol(idx);
        return;
      }
    }
    // No match found - cursor stays in place
  }
  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.setCursorCol(0);
      }
      return;
    }
    const textAfterCursor = currentLine.slice(this.state.cursorCol);
    const segments = segmenter.segment(textAfterCursor);
    const iterator = segments[Symbol.iterator]();
    let next = iterator.next();
    let newCol = this.state.cursorCol;
    // Skip leading whitespace
    while (!next.done && isWhitespaceChar(next.value.segment)) {
      newCol += next.value.segment.length;
      next = iterator.next();
    }
    if (!next.done) {
      const firstGrapheme = next.value.segment;
      if (isPunctuationChar(firstGrapheme)) {
        // Skip punctuation run
        while (!next.done && isPunctuationChar(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)
        ) {
          newCol += next.value.segment.length;
          next = iterator.next();
        }
      }
    }
    this.setCursorCol(newCol);
  }
  // Slash menu only allowed on the first line of the editor
  isSlashMenuAllowed() {
    return 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() === '/';
  }
  getCurrentSlashToken(textBeforeCursor) {
    if (!this.isSlashMenuAllowed()) return null;
    const inlineToken = extractInlineSkillAutocompleteToken(textBeforeCursor);
    if (inlineToken) {
      return inlineToken.token;
    }
    const trimmedStart = textBeforeCursor.trimStart();
    if (!trimmedStart.startsWith('/')) return null;
    if (trimmedStart.includes(' ')) return null;
    return trimmedStart;
  }
  isInSlashCommandContext(textBeforeCursor) {
    return this.getCurrentSlashToken(textBeforeCursor) !== null;
  }
  shouldSubmitAfterSlashAutocomplete(currentLine, cursorCol, prefix) {
    const beforePrefix = currentLine.slice(0, cursorCol - prefix.length);
    return beforePrefix.trim() === '';
  }
  // Autocomplete methods
  tryTriggerAutocomplete(explicitTab = false) {
    if (!this.autocompleteProvider) return;
    // Check if we should trigger file completion on Tab
    if (explicitTab) {
      const provider = this.autocompleteProvider;
      const shouldTrigger =
        !provider.shouldTriggerFileCompletion ||
        provider.shouldTriggerFileCompletion(
          this.state.lines,
          this.state.cursorLine,
          this.state.cursorCol
        );
      if (!shouldTrigger) {
        return;
      }
    }
    const suggestions = this.autocompleteProvider.getSuggestions(
      this.state.lines,
      this.state.cursorLine,
      this.state.cursorCol
    );
    if (suggestions && suggestions.items.length > 0) {
      this.autocompletePrefix = suggestions.prefix;
      this.autocompleteList = new SelectList(
        suggestions.items,
        this.autocompleteMaxVisible,
        this.theme.selectList
      );
      this.autocompleteState = 'regular';
    } else {
      this.cancelAutocomplete();
    }
  }
  handleTabCompletion() {
    if (!this.autocompleteProvider) return;
    const currentLine = this.state.lines[this.state.cursorLine] || '';
    const beforeCursor = currentLine.slice(0, this.state.cursorCol);
    const slashToken = this.getCurrentSlashToken(beforeCursor);
    // Check if we're in a slash command context
    if (slashToken && !slashToken.includes(' ')) {
      this.handleSlashCommandCompletion();
    } else {
      this.forceFileAutocomplete(true);
    }
  }
  handleSlashCommandCompletion() {
    this.tryTriggerAutocomplete(true);
  }
  /*
    https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/559322883
    17 this job fails with https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19
    536643416/job/55932288317 havea  look at .gi
     */
  forceFileAutocomplete(explicitTab = false) {
    if (!this.autocompleteProvider) return;
    // Check if provider supports force file suggestions via runtime check
    const provider = this.autocompleteProvider;
    if (typeof provider.getForceFileSuggestions !== 'function') {
      this.tryTriggerAutocomplete(true);
      return;
    }
    const suggestions = provider.getForceFileSuggestions(
      this.state.lines,
      this.state.cursorLine,
      this.state.cursorCol
    );
    if (suggestions && suggestions.items.length > 0) {
      // If there's exactly one suggestion, apply it immediately
      if (explicitTab && suggestions.items.length === 1) {
        const item = suggestions.items[0];
        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.setCursorCol(result.cursorCol);
        if (this.onChange) this.onChange(this.getText());
        return;
      }
      this.autocompletePrefix = suggestions.prefix;
      this.autocompleteList = new SelectList(
        suggestions.items,
        this.autocompleteMaxVisible,
        this.theme.selectList
      );
      this.autocompleteState = 'force';
    } else {
      this.cancelAutocomplete();
    }
  }
  cancelAutocomplete() {
    this.autocompleteState = null;
    this.autocompleteList = undefined;
    this.autocompletePrefix = '';
  }
  isShowingAutocomplete() {
    return this.autocompleteState !== null;
  }
  updateAutocomplete() {
    if (!this.autocompleteState || !this.autocompleteProvider) return;
    if (this.autocompleteState === 'force') {
      this.forceFileAutocomplete();
      return;
    }
    const suggestions = this.autocompleteProvider.getSuggestions(
      this.state.lines,
      this.state.cursorLine,
      this.state.cursorCol
    );
    if (suggestions && suggestions.items.length > 0) {
      this.autocompletePrefix = suggestions.prefix;
      // Always create new SelectList to ensure update
      this.autocompleteList = new SelectList(
        suggestions.items,
        this.autocompleteMaxVisible,
        this.theme.selectList
      );
    } else {
      this.cancelAutocomplete();
    }
  }
}
//# sourceMappingURL=editor.js.map
