import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
import { logException } from '@atlaskit/editor-common/monitoring';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { keydownHandler } from '@atlaskit/editor-prosemirror/keymap';
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
import { isAutocompleteDebugEnabled } from './debug-mode';
import { createGhostTextDecorationSet } from './ghost-text-decoration';
import { createLocalSlowLaneClient } from './local-slow-lane-client';
import { createSlowLaneClient, setDefaultSlowLaneClient, isWordBoundary } from './slow-lane-client';
import { predict, loadDefaultVocabulary, loadVectorsAsync, incrementSessionFreq, ingestDocumentPage } from './text-predictor';
export var autocompletePluginKey = new PluginKey('autocomplete');
var DEBOUNCE_MS = 150;
var NETWORK_SLOW_LANE_DEBOUNCE_MS = 300;
var LOCAL_SLOW_LANE_DEBOUNCE_MS = 100;
var CONTEXT_REFRESH_THROTTLE_MS = 1000;
// Caps the dedup Set so long editing sessions don't retain every distinct
// version of the (potentially hundreds-of-KB) page content for the plugin's
// lifetime. Eviction is FIFO; a re-ingest of an evicted text is harmless.
var MAX_INGESTED_CONTEXT_TEXTS = 50;
// Caps how many times the word-boundary path will retry getContext() while the
// parent comment is still missing. Combined with the 1s throttle this gives a
// ~5s window to cover a still-loading comment thread, then stops permanently so
// non-comment editors (where parentCommentContent never arrives) don't refetch
// on every word boundary for the plugin's lifetime.
var MAX_CONTEXT_REFRESH_ATTEMPTS = 5;

// eslint-disable-next-line require-unicode-regexp
var TRAILING_NON_BOUNDARY_TOKEN_REGEX = /([^\s.,;:!?]*)$/;
// eslint-disable-next-line require-unicode-regexp
var WORD_BOUNDARY_CHARS_REGEX = /[\s.,;:!?]/;
var hasDestroy = function hasDestroy(client) {
  return 'destroy' in client && typeof client.destroy === 'function';
};
var createInitialState = function createInitialState() {
  return {
    ghostText: '',
    ghostPosition: -1,
    decorationSet: DecorationSet.empty
  };
};

/**
 * Extract text content before the cursor from the current document.
 * Returns the last ~200 characters for context.
 */
var getTextBeforeCursor = function getTextBeforeCursor(state) {
  var $from = state.selection.$from;
  var maxChars = 200;
  var blockNode = $from.parent;
  var offsetInBlock = $from.parentOffset;
  var blockText = blockNode.textContent.slice(0, offsetInBlock);
  if (blockText.length >= maxChars) {
    return blockText.slice(-maxChars);
  }
  var fullText = blockText;

  // Walk backwards through previous blocks until we have enough context.
  var depth = $from.depth - 1;
  while (fullText.length < maxChars && depth >= 0) {
    var parentNode = $from.node(depth);
    var indexInParent = $from.index(depth);
    for (var i = indexInParent - 1; i >= 0 && fullText.length < maxChars; i--) {
      var sibling = parentNode.child(i);
      var siblingText = sibling.textContent;
      fullText = siblingText + '\n' + fullText;
    }
    depth--;
  }
  return fullText.slice(-maxChars);
};

/**
 * Set the autocomplete state via a transaction metadata.
 */
var setAutocompleteMeta = function setAutocompleteMeta(tr, meta) {
  return tr.setMeta(autocompletePluginKey, meta);
};

/**
 * Apply a ghost text suggestion to the editor state.
 */
var lastShownGhostText = '';
var showGhostText = function showGhostText(view, text, position) {
  try {
    var state = view.state,
      dispatch = view.dispatch;
    var decorationSet = createGhostTextDecorationSet(state, position, text);
    var tr = setAutocompleteMeta(state.tr, {
      ghostText: text,
      ghostPosition: position,
      decorationSet: decorationSet
    });
    dispatch(tr);
  } catch (error) {
    logException(error, {
      location: 'editor-plugin-autocomplete/showGhostText'
    });
  }
};

/**
 * Clear the current ghost text from the editor.
 */
var clearGhostText = function clearGhostText(state, dispatch) {
  var pluginState = autocompletePluginKey.getState(state);
  if (!pluginState || !pluginState.ghostText) {
    return false;
  }
  if (dispatch) {
    try {
      var tr = setAutocompleteMeta(state.tr, {
        ghostText: '',
        ghostPosition: -1,
        decorationSet: DecorationSet.empty
      });
      dispatch(tr);
    } catch (error) {
      logException(error, {
        location: 'editor-plugin-autocomplete/clearGhostText'
      });
      return false;
    }
  }
  return true;
};

/**
 * Accept the current ghost text suggestion and insert it into the document.
 */
var acceptGhostText = function acceptGhostText(state, dispatch) {
  var pluginState = autocompletePluginKey.getState(state);
  if (!pluginState || !pluginState.ghostText) {
    return false;
  }
  if (dispatch) {
    try {
      var ghostText = pluginState.ghostText,
        ghostPosition = pluginState.ghostPosition;
      var tr = state.tr.insertText(ghostText, ghostPosition);
      tr = setAutocompleteMeta(tr, {
        ghostText: '',
        ghostPosition: -1,
        decorationSet: DecorationSet.empty
      });
      dispatch(tr);
    } catch (error) {
      logException(error, {
        location: 'editor-plugin-autocomplete/acceptGhostText'
      });
      return false;
    }
  }
  return true;
};

/**
 * Context provided to the autocomplete plugin on first editor focus.
 * Text fields are selectively ingested to boost word-frequency scoring for
 * predictions, giving words already present in the document/thread an L1
 * priority boost.
 */

/**
 * Build the text payload for the slow-lane request by prepending any available
 * comment context ahead of the live document text. This gives the backend
 * model richer context about the thread the user is writing in.
 */
var buildSlowLaneText = function buildSlowLaneText(docText, context) {
  var _context$siblingComme, _context$siblingComme2, _context$siblingComme3;
  var lines = [];
  if (context !== null && context !== void 0 && context.parentCommentContent) {
    lines.push("comment: ".concat(context.parentCommentContent));
  }
  context === null || context === void 0 || (_context$siblingComme = context.siblingCommentsContents) === null || _context$siblingComme === void 0 || _context$siblingComme.forEach(function (sibling, index) {
    lines.push("reply ".concat(index + 1, ": ").concat(sibling));
  });
  var nextReplyNumber = ((_context$siblingComme2 = context === null || context === void 0 || (_context$siblingComme3 = context.siblingCommentsContents) === null || _context$siblingComme3 === void 0 ? void 0 : _context$siblingComme3.length) !== null && _context$siblingComme2 !== void 0 ? _context$siblingComme2 : 0) + 1;
  lines.push("reply ".concat(nextReplyNumber, ": ").concat(docText));
  return lines.join('\n');
};
var createMidWordCharacterRegex = function createMidWordCharacterRegex() {
  try {
    // string-built regex avoids TS1501 under the package's ES5 build target
    // Ignored via go/ees019
    // eslint-disable-next-line e18e/prefer-static-regex
    return new RegExp('[\\p{L}\\p{N}_]', 'u');
  } catch (_unused) {
    return null;
  }
};
var midWordCharacterRegex = createMidWordCharacterRegex();
var isAsciiWordCharacter = function isAsciiWordCharacter(char) {
  if (char === '_') {
    return true;
  }
  var charCode = char.charCodeAt(0);
  return charCode >= 48 && charCode <= 57 || charCode >= 65 && charCode <= 90 || charCode >= 97 && charCode <= 122;
};
var isMidWordCharacter = function isMidWordCharacter(char) {
  if (midWordCharacterRegex) {
    return midWordCharacterRegex.test(char);
  }
  if (isAsciiWordCharacter(char)) {
    return true;
  }
  return char.toLocaleLowerCase() !== char.toLocaleUpperCase();
};
var getLeadingTextCharacter = function getLeadingTextCharacter(text) {
  var firstCodePoint = text === null || text === void 0 ? void 0 : text.codePointAt(0);
  if (firstCodePoint === undefined) {
    return undefined;
  }
  return String.fromCodePoint(firstCodePoint);
};
var getTypedLengthForPrediction = function getTypedLengthForPrediction(textBefore) {
  var _textBefore$match$, _textBefore$match;
  if (isWordBoundary(textBefore)) {
    return 0;
  }
  var trailingToken = (_textBefore$match$ = (_textBefore$match = textBefore.match(TRAILING_NON_BOUNDARY_TOKEN_REGEX)) === null || _textBefore$match === void 0 ? void 0 : _textBefore$match[1]) !== null && _textBefore$match$ !== void 0 ? _textBefore$match$ : '';
  return trailingToken.length;
};
var isEnglishLocale = function isEnglishLocale(locale) {
  if (!locale) {
    return false;
  }
  return locale.toLowerCase().startsWith('en');
};
export var createAutocompletePlugin = function createAutocompletePlugin(options, api) {
  var _options$locale;
  var locale = (_options$locale = options === null || options === void 0 ? void 0 : options.locale) !== null && _options$locale !== void 0 ? _options$locale : typeof navigator !== 'undefined' ? navigator.language : undefined;
  var isAutocompleteEnabled = isEnglishLocale(locale);
  var debounceTimer = null;
  var hasIngestedPage = false;
  var resolvedContext;
  /**
   * Kept in sync with the live EditorView so the async getContext() promise
   * can re-trigger a slow-lane update the moment context arrives, even if the
   * user has already typed several words before the promise resolved.
   */
  var currentView = null;
  var unsubscribeFromContextUpdates;
  /**
   * Set after accepting a suggestion so the next doc-change update
   * skips scheduling a new prediction for the just-inserted text.
   * Scoped to the factory so multiple editor instances don't share state.
   */
  var justAccepted = false;

  /**
   * Stores the text-before-cursor snapshot at the moment the user dismissed
   * a suggestion via Escape. While the context remains identical, we suppress
   * re-showing the same suggestion. Resets to null as soon as the text changes.
   */
  var dismissedContext = null;
  var lastSuggestionTypedLength = 0;
  var lastSuggestionLength = 0;

  /**
   * cold  → no slow-lane vector received yet; frequency-only trie scoring
   * server → server slow-lane API returned the context vector
   * localLlm → on-device WebGPU/MLC model returned the context vector
   */
  var getCompletionSource = function getCompletionSource() {
    if (!slowLaneClient.getContextVector()) {
      return 'cold';
    }
    return options !== null && options !== void 0 && options.useLocalModel ? 'localLlm' : 'server';
  };
  var fireSuggestionDismissedAnalytics = function fireSuggestionDismissedAnalytics(reason) {
    var _api$analytics;
    api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || _api$analytics.actions.fireAnalyticsEvent({
      action: ACTION.SUGGESTION_DISMISSED,
      actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
      eventType: EVENT_TYPE.TRACK,
      attributes: {
        completionSource: getCompletionSource(),
        reason: reason
      }
    });
  };
  var fireLocalModelLoadedAnalytics = function fireLocalModelLoadedAnalytics(info) {
    var _api$analytics2;
    api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 || _api$analytics2.actions.fireAnalyticsEvent({
      action: ACTION.LOCAL_MODEL_LOADED,
      actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
      eventType: EVENT_TYPE.TRACK,
      attributes: {
        modelId: info.modelId,
        embeddingModelId: info.embeddingModelId,
        loadDurationMs: info.loadDurationMs,
        gpuVendor: info.capabilities.vendor,
        gpuArchitecture: info.capabilities.architecture
      }
    });
  };
  var fireLocalModelLoadFailedAnalytics = function fireLocalModelLoadFailedAnalytics(error) {
    var _api$analytics3;
    var capabilities = error.capabilities;
    api === null || api === void 0 || (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 || _api$analytics3.actions.fireAnalyticsEvent({
      action: ACTION.LOCAL_MODEL_LOAD_FAILED,
      actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
      eventType: EVENT_TYPE.TRACK,
      attributes: {
        reason: error.reason,
        message: error.message,
        modelId: error.modelId,
        embeddingModelId: error.embeddingModelId,
        webgpuAvailable: capabilities.available,
        adapterAvailable: capabilities.adapterAvailable,
        shaderF16Supported: capabilities.shaderF16Supported,
        maxBufferSizeMB: capabilities.maxBufferSizeMB,
        maxStorageBufferBindingSizeMB: capabilities.maxStorageBufferBindingSizeMB,
        gpuVendor: capabilities.vendor,
        gpuArchitecture: capabilities.architecture
      }
    });
  };
  var fireSuggestionInsertedAnalytics = function fireSuggestionInsertedAnalytics(ghostText) {
    var _api$analytics4;
    var typedLength = lastSuggestionTypedLength;
    var suggestionLength = lastSuggestionLength || typedLength + ghostText.length;
    var kssDelta = suggestionLength - typedLength;
    api === null || api === void 0 || (_api$analytics4 = api.analytics) === null || _api$analytics4 === void 0 || _api$analytics4.actions.fireAnalyticsEvent({
      action: ACTION.SUGGESTION_INSERTED,
      actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
      eventType: EVENT_TYPE.TRACK,
      attributes: {
        completionSource: getCompletionSource(),
        suggestionLength: suggestionLength,
        typedLength: typedLength,
        kssDelta: kssDelta
      }
    });
  };
  var slowLaneClient = options !== null && options !== void 0 && options.useLocalModel ? createLocalSlowLaneClient({
    debounceMs: LOCAL_SLOW_LANE_DEBOUNCE_MS,
    onLoadSuccess: fireLocalModelLoadedAnalytics,
    onLoadError: fireLocalModelLoadFailedAnalytics
  }) : createSlowLaneClient({
    baseUrl: '',
    debounceMs: NETWORK_SLOW_LANE_DEBOUNCE_MS
  });
  setDefaultSlowLaneClient(slowLaneClient);
  var contextRequestInFlight = false;
  var lastContextRefreshAt = 0;
  // Bounds the word-boundary retry loop so it terminates even when the editor is
  // not in a comment thread (parentCommentContent never resolves).
  var wordBoundaryRefreshAttempts = 0;
  // Set when the plugin is torn down so in-flight getContext() resolutions don't
  // mutate the global text-predictor state after destruction.
  var destroyed = false;
  var ingestedContextTexts = new Set();
  var logContextResolved = function logContextResolved(source, context) {
    var _context$parentCommen, _context$siblingComme4, _context$siblingComme5;
    if (!isAutocompleteDebugEnabled()) {
      return;
    }

    // eslint-disable-next-line no-console
    console.log('%c[Autocomplete] %cgetContext resolved', 'color: #00b8d9; font-weight: bold;', 'color: inherit;', {
      source: source,
      hasParentComment: !!(context !== null && context !== void 0 && context.parentCommentContent),
      parentCommentPreview: context === null || context === void 0 || (_context$parentCommen = context.parentCommentContent) === null || _context$parentCommen === void 0 ? void 0 : _context$parentCommen.slice(0, 80),
      siblingCount: (_context$siblingComme4 = context === null || context === void 0 || (_context$siblingComme5 = context.siblingCommentsContents) === null || _context$siblingComme5 === void 0 ? void 0 : _context$siblingComme5.length) !== null && _context$siblingComme4 !== void 0 ? _context$siblingComme4 : 0,
      hasFullPage: !!(context !== null && context !== void 0 && context.fullPageContent)
    });
  };
  var applyContext = function applyContext(context) {
    var _context$siblingComme6;
    // Merge rather than replace: the word-boundary retry may resolve only a
    // late-arriving field (e.g. parentCommentContent) without re-sending
    // fullPageContent, so replacing would drop previously resolved context.
    // Strip undefined values first so a field explicitly set to undefined by
    // getContext doesn't overwrite a previously resolved value.
    var definedContext = Object.fromEntries(Object.entries(context).filter(function (_ref) {
      var _ref2 = _slicedToArray(_ref, 2),
        value = _ref2[1];
      return value !== undefined;
    }));
    resolvedContext = _objectSpread(_objectSpread({}, resolvedContext), definedContext);
    var ingestContextText = function ingestContextText(text) {
      if (!text || ingestedContextTexts.has(text)) {
        return;
      }
      ingestedContextTexts.add(text);
      // Evict oldest entries (Set preserves insertion order) to bound memory.
      while (ingestedContextTexts.size > MAX_INGESTED_CONTEXT_TEXTS) {
        var oldest = ingestedContextTexts.values().next().value;
        if (oldest === undefined) {
          break;
        }
        ingestedContextTexts.delete(oldest);
      }
      ingestDocumentPage(text);
    };
    ingestContextText(context.fullPageContent);
    ingestContextText(context.parentCommentContent);
    var _iterator = _createForOfIteratorHelper((_context$siblingComme6 = context.siblingCommentsContents) !== null && _context$siblingComme6 !== void 0 ? _context$siblingComme6 : []),
      _step;
    try {
      for (_iterator.s(); !(_step = _iterator.n()).done;) {
        var siblingCommentContent = _step.value;
        ingestContextText(siblingCommentContent);
      }

      // Context arrived after word boundaries may already have fired. Re-send
      // slow-lane context immediately so the next inference includes the thread.
    } catch (err) {
      _iterator.e(err);
    } finally {
      _iterator.f();
    }
    if (currentView) {
      slowLaneClient.updateContext(buildSlowLaneText(currentView.state.doc.textContent, resolvedContext));
    }
  };

  /**
   * Returns true when a fetch was actually started, false when it was skipped
   * (no getContext, a request already in flight, or throttled). Callers that
   * track a retry budget should only count attempts where this returns true.
   */
  var refreshContext = function refreshContext(_ref3) {
    var source = _ref3.source,
      _ref3$allowThrottle = _ref3.allowThrottle,
      allowThrottle = _ref3$allowThrottle === void 0 ? true : _ref3$allowThrottle;
    if (!(options !== null && options !== void 0 && options.getContext) || contextRequestInFlight) {
      return false;
    }
    var now = Date.now();
    if (allowThrottle && now - lastContextRefreshAt < CONTEXT_REFRESH_THROTTLE_MS) {
      return false;
    }
    contextRequestInFlight = true;
    lastContextRefreshAt = now;
    options.getContext().then(function (context) {
      // Bail if the plugin was destroyed while the fetch was in flight —
      // applyContext mutates global text-predictor state we must not touch
      // after teardown.
      if (destroyed) {
        return;
      }
      logContextResolved(source, context);
      if (!context) {
        return;
      }
      applyContext(context);
    }).catch(function (error) {
      logException(error, {
        location: 'editor-plugin-autocomplete/getContext'
      });
    }).finally(function () {
      contextRequestInFlight = false;
    });
    return true;
  };

  /**
   * Schedule a prediction after a short debounce.
   * Tier 1 predictions are synchronous (<0.1ms) but we still debounce
   * to avoid unnecessary work on rapid keystrokes.
   */
  var schedulePrediction = function schedulePrediction(view) {
    if (debounceTimer) {
      clearTimeout(debounceTimer);
    }
    debounceTimer = setTimeout(function () {
      try {
        var state = view.state;
        var selection = state.selection;
        if (!selection.empty) {
          return;
        }

        // Suppress suggestions when the cursor is mid-word — only offer
        // completions when the cursor is at the trailing edge of a token.
        // nodeAfter correctly handles inline atoms (mentions, emojis) where
        // parentOffset and textContent indices diverge.
        var nodeAfter = selection.$from.nodeAfter;
        var charAfterCursor = nodeAfter !== null && nodeAfter !== void 0 && nodeAfter.isText ? getLeadingTextCharacter(nodeAfter.text) : undefined;
        // Only suppress for Unicode letters, digits, and underscore — hyphens,
        // apostrophes, and similar punctuation are valid left-edge boundaries
        // and should not block suggestions (e.g. cursor before '-' in compound-word).
        if (charAfterCursor && isMidWordCharacter(charAfterCursor)) {
          return;
        }
        var textBefore = getTextBeforeCursor(state);

        // Suppress re-showing the same suggestion the user just dismissed.
        // Once the text context changes (user types or deletes), this clears automatically.
        if (textBefore === dismissedContext) {
          return;
        }
        dismissedContext = null;
        if (textBefore.trim().length < 3) {
          return;
        }
        var prediction = predict(textBefore);
        if (prediction && prediction.length > 0) {
          var typedLength = getTypedLengthForPrediction(textBefore);
          lastSuggestionTypedLength = typedLength;
          lastSuggestionLength = typedLength + prediction.length;
          showGhostText(view, prediction, selection.from);
          if (prediction !== lastShownGhostText) {
            var _api$analytics5;
            lastShownGhostText = prediction;
            api === null || api === void 0 || (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 || _api$analytics5.actions.fireAnalyticsEvent({
              action: ACTION.SUGGESTION_VIEWED,
              actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
              eventType: EVENT_TYPE.TRACK,
              attributes: {
                completionSource: getCompletionSource()
              }
            });
          }
        }
      } catch (error) {
        logException(error, {
          location: 'editor-plugin-autocomplete/schedulePrediction'
        });
      }
    }, DEBOUNCE_MS);
  };
  var maybeUpdateSessionFrequency = function maybeUpdateSessionFrequency(view, prevState) {
    var newText = getTextBeforeCursor(view.state);
    var prevText = getTextBeforeCursor(prevState);
    if (newText.length <= prevText.length) {
      return;
    }
    var lastChar = newText[newText.length - 1];
    if (!WORD_BOUNDARY_CHARS_REGEX.test(lastChar)) {
      return;
    }

    // Only fire if the previous state did not already end on a boundary,
    // so we don't double-count when multiple boundary chars are inserted.
    var prevLastChar = prevText[prevText.length - 1];
    if (prevLastChar && WORD_BOUNDARY_CHARS_REGEX.test(prevLastChar)) {
      return;
    }
    var beforeBoundary = newText.slice(0, -1).trimEnd();
    var lastSpaceIdx = beforeBoundary.lastIndexOf(' ');
    var completedWord = beforeBoundary.slice(lastSpaceIdx + 1).toLowerCase();
    if (completedWord.length >= 2) {
      incrementSessionFreq(completedWord);
    }
  };
  return new SafePlugin({
    key: autocompletePluginKey,
    state: {
      init: function init() {
        return createInitialState();
      },
      apply: function apply(tr, pluginState) {
        var meta = tr.getMeta(autocompletePluginKey);
        if (meta) {
          return _objectSpread(_objectSpread({}, pluginState), meta);
        }

        // A new prediction is scheduled from view.update.
        if (tr.docChanged) {
          return _objectSpread(_objectSpread({}, pluginState), {}, {
            ghostText: '',
            ghostPosition: -1,
            decorationSet: DecorationSet.empty
          });
        }
        if (tr.selectionSet && pluginState.ghostText) {
          return _objectSpread(_objectSpread({}, pluginState), {}, {
            ghostText: '',
            ghostPosition: -1,
            decorationSet: DecorationSet.empty
          });
        }
        return pluginState;
      }
    },
    props: {
      decorations: function decorations(state) {
        var _pluginState$decorati;
        var pluginState = autocompletePluginKey.getState(state);
        return (_pluginState$decorati = pluginState === null || pluginState === void 0 ? void 0 : pluginState.decorationSet) !== null && _pluginState$decorati !== void 0 ? _pluginState$decorati : DecorationSet.empty;
      },
      handleKeyDown: keydownHandler({
        Tab: function Tab(state, dispatch) {
          var _pluginState$ghostTex;
          var pluginState = autocompletePluginKey.getState(state);
          var ghostText = (_pluginState$ghostTex = pluginState === null || pluginState === void 0 ? void 0 : pluginState.ghostText) !== null && _pluginState$ghostTex !== void 0 ? _pluginState$ghostTex : '';
          var accepted = acceptGhostText(state, dispatch);
          if (accepted) {
            justAccepted = true;
            lastShownGhostText = '';
            fireSuggestionInsertedAnalytics(ghostText);
          }
          return accepted;
        },
        ArrowRight: function ArrowRight(state, dispatch) {
          var _pluginState$ghostTex2;
          var pluginState = autocompletePluginKey.getState(state);
          var ghostText = (_pluginState$ghostTex2 = pluginState === null || pluginState === void 0 ? void 0 : pluginState.ghostText) !== null && _pluginState$ghostTex2 !== void 0 ? _pluginState$ghostTex2 : '';
          var accepted = acceptGhostText(state, dispatch);
          if (accepted) {
            justAccepted = true;
            lastShownGhostText = '';
            fireSuggestionInsertedAnalytics(ghostText);
          }
          return accepted;
        },
        Escape: function Escape(state, dispatch) {
          var didClear = clearGhostText(state, dispatch);
          if (didClear) {
            dismissedContext = getTextBeforeCursor(state);
            fireSuggestionDismissedAnalytics('escape');
          }
          return didClear;
        }
      }),
      handleDOMEvents: {
        blur: function blur(view) {
          if (!isAutocompleteEnabled) {
            return false;
          }
          var pluginState = autocompletePluginKey.getState(view.state);
          if (pluginState !== null && pluginState !== void 0 && pluginState.ghostText) {
            clearGhostText(view.state, view.dispatch);
            fireSuggestionDismissedAnalytics('blur');
          }
          return false;
        },
        focus: function focus() {
          var _options$useLocalMode, _options$useLocalMode2;
          if (!isAutocompleteEnabled) {
            return false;
          }
          loadDefaultVocabulary({
            isLocalLLM: (_options$useLocalMode = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode !== void 0 ? _options$useLocalMode : false
          }).catch(function (error) {
            logException(error, {
              location: 'editor-plugin-autocomplete/loadDefaultVocabulary'
            });
          });
          loadVectorsAsync({
            getBinaryUrl: options === null || options === void 0 ? void 0 : options.getVectorsBinaryUrl,
            isLocalLLM: (_options$useLocalMode2 = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode2 !== void 0 ? _options$useLocalMode2 : false
          }).catch(function (error) {
            logException(error, {
              location: 'editor-plugin-autocomplete/loadVectorsAsync'
            });
          });
          if (!hasIngestedPage) {
            hasIngestedPage = true;
            refreshContext({
              source: 'focus',
              allowThrottle: false
            });
          }
          return false;
        }
      }
    },
    view: function view(editorView) {
      // Capture up front so a subscription notification before the first PM
      // transaction can still drive slowLaneClient.updateContext (gated on currentView).
      currentView = editorView;

      // Push channel for hosts that keep producing context after mount (e.g. Rovo chat).
      if (isAutocompleteEnabled && options !== null && options !== void 0 && options.subscribeToContextUpdates) {
        unsubscribeFromContextUpdates = options.subscribeToContextUpdates(function () {
          // Bypass throttle for freshness; the in-flight guard prevents overlap.
          refreshContext({
            source: 'subscription',
            allowThrottle: false
          });
        });
      }
      return {
        update: function update(view, prevState) {
          if (!isAutocompleteEnabled) {
            return;
          }
          currentView = view;
          if (!prevState.doc.eq(view.state.doc)) {
            if (justAccepted) {
              justAccepted = false;

              // Snapshot the post-acceptance text so follow-up transactions hit
              // the dismissedContext guard and abort until the user types again.
              dismissedContext = getTextBeforeCursor(view.state);
              if (debounceTimer) {
                clearTimeout(debounceTimer);
              }
              return;
            }
            maybeUpdateSessionFrequency(view, prevState);
            var textBefore = getTextBeforeCursor(view.state);
            if (isWordBoundary(textBefore)) {
              var _resolvedContext;
              slowLaneClient.updateContext(buildSlowLaneText(view.state.doc.textContent, resolvedContext));

              // Context may not have resolved on first focus (e.g. comment
              // thread still loading). Retry on word boundaries until we have
              // the parent comment, throttled so we don't refetch constantly
              // and capped so non-comment editors stop retrying entirely.
              // Skipped for push-channel hosts (subscribeToContextUpdates): they
              // never grow parentCommentContent, so polling only burns the budget.
              if (!(options !== null && options !== void 0 && options.subscribeToContextUpdates) && !((_resolvedContext = resolvedContext) !== null && _resolvedContext !== void 0 && _resolvedContext.parentCommentContent) && wordBoundaryRefreshAttempts < MAX_CONTEXT_REFRESH_ATTEMPTS) {
                // Only count the attempt when a fetch actually started, so an
                // in-flight or throttled no-op doesn't burn the retry budget.
                if (refreshContext({
                  source: 'word-boundary'
                })) {
                  wordBoundaryRefreshAttempts++;
                }
              }
            }
            schedulePrediction(view);
          }
        },
        destroy: function destroy() {
          var _unsubscribeFromConte;
          destroyed = true;
          currentView = null;
          (_unsubscribeFromConte = unsubscribeFromContextUpdates) === null || _unsubscribeFromConte === void 0 || _unsubscribeFromConte();
          unsubscribeFromContextUpdates = undefined;
          if (debounceTimer) {
            clearTimeout(debounceTimer);
          }
          if (hasDestroy(slowLaneClient)) {
            slowLaneClient.destroy();
          }
          ingestedContextTexts.clear();
          setDefaultSlowLaneClient(null);
        }
      };
    }
  });
};