diff --git a/gearbox/internal/framework/templates/components/console.templ b/gearbox/internal/framework/templates/components/console.templ
index 97fba39..536c735 100644
--- a/gearbox/internal/framework/templates/components/console.templ
+++ b/gearbox/internal/framework/templates/components/console.templ
@@ -116,12 +116,65 @@ templ ConsoleDrawer() {
+ // Search overlay — hidden until Ctrl-F/Cmd-F triggers it.
+ // Powered by xterm-addon-search; input wired in console.js.
+
+
+
+
+
+
+
// xterm host. Sessions attach their own xterm into this.
// `console-xterm-pad` adds breathing room so cursor/text isn't
// flush against the panel border.
+ // Large-paste confirmation modal. Triggered by ConsoleManager
+ // when clipboard content exceeds the paste-confirm threshold
+ // (default 20 newlines). Buttons wired in console.js.
+
+
+
Paste ? lines?
+
Large pastes run many commands in sequence. Review the preview before confirming.
+
+
+
+
+
+
+
}
@@ -133,5 +186,6 @@ templ ConsoleAssets() {
+
}
diff --git a/gearbox/static/css/components/console.css b/gearbox/static/css/components/console.css
index 9960bc8..55b0c81 100644
--- a/gearbox/static/css/components/console.css
+++ b/gearbox/static/css/components/console.css
@@ -65,7 +65,9 @@ body.console-dock-open {
.console-tool-btn.hidden,
.console-tab-dot.hidden,
.console-tab-new.hidden,
-.console-tab-bar.hidden {
+.console-tab-bar.hidden,
+.console-search-bar.hidden,
+.console-paste-modal.hidden {
display: none;
}
@@ -203,6 +205,20 @@ body.console-dock-open {
cursor: pointer;
padding: 0;
}
+
+/* Inline rename input — sized to match the original label so the tab
+ * doesn't jump width on edit. */
+.console-tab-rename {
+ background: rgb(2, 6, 23);
+ color: rgb(226, 232, 240);
+ border: 1px solid rgb(52, 211, 153);
+ border-radius: 2px;
+ font: inherit;
+ font-size: 12px;
+ padding: 0 4px;
+ min-width: 80px;
+ max-width: 200px;
+}
.console-tab-close {
background: none;
border: none;
@@ -231,6 +247,116 @@ body.console-dock-open {
display: none;
}
+/* ---------- search overlay ---------- */
+
+/* Floats below the header in drawer/dock; absolute so it doesn't push
+ * the xterm host. Width matches the panel content area. */
+.console-search-bar {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 8px;
+ background: rgb(15, 23, 42);
+ border-bottom: 1px solid rgb(51, 65, 85);
+}
+.console-search-input {
+ flex: 1 1 auto;
+ min-width: 0;
+ background: rgb(2, 6, 23);
+ color: rgb(226, 232, 240);
+ border: 1px solid rgb(51, 65, 85);
+ border-radius: 4px;
+ padding: 4px 8px;
+ font-size: 12px;
+ font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
+}
+.console-search-input:focus {
+ outline: none;
+ border-color: rgb(52, 211, 153);
+}
+.console-search-count {
+ font-size: 11px;
+ color: rgb(148, 163, 184);
+ min-width: 50px;
+ text-align: right;
+ font-variant-numeric: tabular-nums;
+}
+
+/* ---------- paste-confirm modal ---------- */
+
+.console-paste-modal {
+ position: absolute;
+ inset: 0;
+ z-index: 5;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 24px;
+}
+.console-paste-modal-panel {
+ background: rgb(15, 23, 42);
+ border: 1px solid rgb(51, 65, 85);
+ border-radius: 6px;
+ max-width: 560px;
+ width: 100%;
+ padding: 16px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
+}
+.console-paste-modal-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: rgb(226, 232, 240);
+ margin: 0 0 4px 0;
+}
+.console-paste-modal-hint {
+ font-size: 12px;
+ color: rgb(148, 163, 184);
+ margin: 0 0 8px 0;
+}
+.console-paste-modal-preview {
+ background: rgb(2, 6, 23);
+ color: rgb(203, 213, 225);
+ border: 1px solid rgb(51, 65, 85);
+ border-radius: 4px;
+ padding: 8px;
+ font-size: 11px;
+ font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
+ max-height: 200px;
+ overflow: auto;
+ white-space: pre-wrap;
+ word-break: break-all;
+ margin: 0 0 12px 0;
+}
+.console-paste-modal-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+}
+.console-paste-modal-btn {
+ padding: 6px 12px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+ border: 1px solid rgb(51, 65, 85);
+ cursor: pointer;
+}
+.console-paste-modal-cancel {
+ background: rgb(30, 41, 59);
+ color: rgb(226, 232, 240);
+}
+.console-paste-modal-cancel:hover {
+ background: rgb(51, 65, 85);
+}
+.console-paste-modal-confirm {
+ background: rgb(52, 211, 153);
+ color: rgb(2, 6, 23);
+ border-color: rgb(52, 211, 153);
+}
+.console-paste-modal-confirm:hover {
+ background: rgb(110, 231, 183);
+}
+
/* ---------- xterm padding ---------- */
/* xterm.js paints right up to its container's edges by default. The
diff --git a/gearbox/static/js/console/console.js b/gearbox/static/js/console/console.js
index b1dca9c..5737007 100644
--- a/gearbox/static/js/console/console.js
+++ b/gearbox/static/js/console/console.js
@@ -63,6 +63,19 @@
while (node.firstChild) node.removeChild(node.firstChild);
}
+ // Cookie reader used by the global shortcut to pull the active box_id
+ // out of the switchBox cookie. Returns '' when not found.
+ function readCookie(name) {
+ const parts = (document.cookie || '').split(';');
+ for (let i = 0; i < parts.length; i++) {
+ const p = parts[i].trim();
+ if (p.indexOf(name + '=') === 0) {
+ return decodeURIComponent(p.substring(name.length + 1));
+ }
+ }
+ return '';
+ }
+
function clampInt(n, lo, hi) {
n = parseInt(n, 10);
if (!isFinite(n)) n = lo;
@@ -138,17 +151,25 @@
function ConsoleSession(descriptor) {
this.id = nextSessionID();
this.descriptor = descriptor;
- this.label = descriptor.label || descriptor.boxID || this.id;
+ // baseLabel is the descriptor-derived name; label may be overridden by
+ // user rename (double-click on tab). Both are tracked so the rename
+ // sticks across status updates but the underlying box name stays
+ // available for tooltips.
+ this.baseLabel = descriptor.label || descriptor.boxID || this.id;
+ this.label = this.baseLabel;
this.status = 'idle';
this.mode = '';
this.uid = null;
this.errorMsg = '';
this.term = null;
this.fitAddon = null;
+ this.searchAddon = null; // lazy-loaded on first search
this.ws = null;
this.host = null; // wrapper that gets attached/detached
this.onStatusChange = null; // ConsoleManager wires this
+ this.onSearchResults = null; // ConsoleManager wires this for count updates
this.fontSize = FONT_DEFAULT;
+ this.onKeyIntercept = null; // ConsoleManager wires this for Ctrl-F, etc.
}
ConsoleSession.prototype._setStatus = function (status) {
@@ -183,6 +204,66 @@
const enc = new TextEncoder();
self._sendData(enc.encode(data));
});
+
+ // Key interception: Ctrl-F / Cmd-F opens the search bar instead
+ // of bubbling to the shell. Returning false from this handler
+ // tells xterm NOT to send the keystroke onward. Any other shortcut
+ // we want to capture at the manager level rides through here too.
+ this.term.attachCustomKeyEventHandler(function (e) {
+ if (e.type !== 'keydown') return true;
+ // Ctrl-F (Linux/Win) or Cmd-F (mac) → open search
+ if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && (e.key === 'f' || e.key === 'F')) {
+ if (typeof self.onKeyIntercept === 'function') {
+ if (self.onKeyIntercept('search-open')) return false;
+ }
+ }
+ return true;
+ });
+ };
+
+ // Lazy-load the search addon on first use. Only sessions whose user
+ // opens the find bar pay the cost of constructing it.
+ ConsoleSession.prototype._ensureSearch = function () {
+ if (this.searchAddon || !this.term) return this.searchAddon;
+ if (typeof SearchAddon === 'undefined' || !SearchAddon.SearchAddon) {
+ console.error('[console] xterm-addon-search not loaded');
+ return null;
+ }
+ this.searchAddon = new SearchAddon.SearchAddon();
+ this.term.loadAddon(this.searchAddon);
+ const self = this;
+ if (this.searchAddon.onDidChangeResults) {
+ this.searchAddon.onDidChangeResults(function (res) {
+ if (typeof self.onSearchResults === 'function') self.onSearchResults(res);
+ });
+ }
+ return this.searchAddon;
+ };
+
+ // Find next / previous match. `term` is the active session; manager
+ // routes the search input to whichever tab is active.
+ ConsoleSession.prototype.searchNext = function (q, opts) {
+ const addon = this._ensureSearch();
+ if (!addon || !q) return false;
+ return addon.findNext(q, opts || { decorations: { activeMatchBackground: '#facc15', matchBackground: '#facc1540' } });
+ };
+ ConsoleSession.prototype.searchPrev = function (q, opts) {
+ const addon = this._ensureSearch();
+ if (!addon || !q) return false;
+ return addon.findPrevious(q, opts || { decorations: { activeMatchBackground: '#facc15', matchBackground: '#facc1540' } });
+ };
+ ConsoleSession.prototype.searchClear = function () {
+ if (this.searchAddon && this.searchAddon.clearDecorations) {
+ this.searchAddon.clearDecorations();
+ }
+ };
+
+ // Write a buffer of bytes to the PTY. Used by the paste-confirm flow
+ // once the user OKs a large paste — bypasses the term.onData path.
+ ConsoleSession.prototype.sendText = function (text) {
+ if (!text) return;
+ const enc = new TextEncoder();
+ this._sendData(enc.encode(text));
};
ConsoleSession.prototype.attach = function (parent) {
@@ -403,9 +484,189 @@
});
this._wireDockResize();
+ this._wireSearch();
+ this._wirePasteConfirm();
+ this._wireGlobalShortcut();
this._wired = true;
};
+ /* ---------- search ---------- */
+
+ ConsoleManager.prototype._wireSearch = function () {
+ const bar = this._$('console-search-bar');
+ const input = this._$('console-search-input');
+ const closeBtn = this._$('console-search-close');
+ const nextBtn = this._$('console-search-next');
+ const prevBtn = this._$('console-search-prev');
+ if (!bar || !input) return;
+ const self = this;
+
+ const runSearch = function (dir) {
+ const s = self._active();
+ if (!s) return;
+ const q = input.value;
+ // Wire result-count callback once per session.
+ s.onSearchResults = self._renderSearchCount.bind(self);
+ if (dir === 'prev') s.searchPrev(q);
+ else s.searchNext(q);
+ };
+
+ input.addEventListener('input', function () { runSearch('next'); });
+ input.addEventListener('keydown', function (e) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ runSearch(e.shiftKey ? 'prev' : 'next');
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ self.closeSearch();
+ }
+ });
+ if (nextBtn) nextBtn.addEventListener('click', function () { runSearch('next'); });
+ if (prevBtn) prevBtn.addEventListener('click', function () { runSearch('prev'); });
+ if (closeBtn) closeBtn.addEventListener('click', self.closeSearch.bind(self));
+ };
+
+ ConsoleManager.prototype.openSearch = function () {
+ const bar = this._$('console-search-bar');
+ const input = this._$('console-search-input');
+ if (!bar || !input) return;
+ bar.classList.remove('hidden');
+ input.focus();
+ input.select();
+ };
+
+ ConsoleManager.prototype.closeSearch = function () {
+ const bar = this._$('console-search-bar');
+ const input = this._$('console-search-input');
+ if (bar) bar.classList.add('hidden');
+ if (input) input.value = '';
+ const s = this._active();
+ if (s) s.searchClear();
+ const count = this._$('console-search-count');
+ if (count) count.textContent = '';
+ if (s) s.focus();
+ };
+
+ ConsoleManager.prototype._renderSearchCount = function (res) {
+ const count = this._$('console-search-count');
+ if (!count) return;
+ if (!res || res.resultCount === 0) {
+ count.textContent = 'no matches';
+ return;
+ }
+ // resultIndex is 0-based; show 1-based to the user.
+ const idx = (res.resultIndex === undefined || res.resultIndex < 0) ? 0 : res.resultIndex + 1;
+ count.textContent = idx + ' / ' + res.resultCount;
+ };
+
+ /* ---------- paste confirm ---------- */
+
+ // Per #142 spec: ≥ 20 newline characters in clipboard text triggers
+ // an inline confirm modal so a multi-command paste can't accidentally
+ // run the entire clipboard. We count actual '\n' occurrences (not
+ // lines) so a trailing newline doesn't shift the threshold off by one.
+ // Bracketed paste mode (DECSET 2004) is the remote shell's job;
+ // xterm wraps pasted content with ESC[200~ … ESC[201~ automatically
+ // when the shell asked for it. Confirmed pastes route through
+ // term.paste() so that wrapping still happens.
+ const PASTE_CONFIRM_NEWLINES = 20;
+
+ ConsoleManager.prototype._wirePasteConfirm = function () {
+ const modal = this._$('console-paste-modal');
+ const cancelBtn = this._$('console-paste-modal-cancel');
+ const confirmBtn = this._$('console-paste-modal-confirm');
+ if (!modal || !cancelBtn || !confirmBtn) return;
+
+ const self = this;
+ this._pendingPaste = null;
+
+ const closeModal = function () {
+ modal.classList.add('hidden');
+ self._pendingPaste = null;
+ const s = self._active();
+ if (s) s.focus();
+ };
+
+ cancelBtn.addEventListener('click', closeModal);
+ confirmBtn.addEventListener('click', function () {
+ const text = self._pendingPaste;
+ closeModal();
+ const s = self._active();
+ // Route through term.paste() so xterm applies its normal
+ // paste pipeline (bracketed-paste wrapping, normalization).
+ // sendText would bypass DECSET 2004 framing.
+ if (text && s && s.term && typeof s.term.paste === 'function') {
+ s.term.paste(text);
+ } else if (text && s) {
+ s.sendText(text);
+ }
+ });
+
+ // Listen at document level so paste lands here before xterm's
+ // own paste handling runs. We only intercept when the panel is
+ // visible and the active session is focused (or in the panel
+ // bounds — defensive).
+ document.addEventListener('paste', function (e) {
+ const drawer = self._$('console-drawer');
+ if (!drawer || drawer.classList.contains('hidden')) return;
+ const active = document.activeElement;
+ const xterm = self._$('console-xterm');
+ if (!xterm || !active || !xterm.contains(active)) return;
+ if (!e.clipboardData) return;
+ const text = e.clipboardData.getData('text');
+ if (!text) return;
+ // Count actual newline characters — robust against trailing-newline
+ // quirks vs. counting lines via split().length.
+ let newlines = 0;
+ for (let i = 0; i < text.length; i++) {
+ if (text.charCodeAt(i) === 10) newlines++;
+ }
+ if (newlines < PASTE_CONFIRM_NEWLINES) return; // let xterm handle it normally
+ e.preventDefault();
+ e.stopPropagation();
+ self._pendingPaste = text;
+ const linesEl = self._$('console-paste-modal-lines');
+ const previewEl = self._$('console-paste-modal-preview');
+ // Visible "N lines" count = newlines + 1 (the line after the
+ // final newline). Matches how the user thinks about it.
+ if (linesEl) linesEl.textContent = String(newlines + 1);
+ if (previewEl) {
+ // Cap preview at ~1200 chars + first ~30 lines so a huge
+ // paste doesn't blow up the modal. textContent is safe.
+ let preview = text.split('\n').slice(0, 30).join('\n');
+ if (preview.length > 1200) preview = preview.slice(0, 1200) + '…';
+ previewEl.textContent = preview;
+ }
+ modal.classList.remove('hidden');
+ confirmBtn.focus();
+ });
+ };
+
+ /* ---------- global shortcut ---------- */
+
+ // Ctrl-Shift-` opens the console for the active box. Reads box_id from
+ // the gearbox_active_box cookie set by the box-resolver middleware;
+ // no cookie → no-op (palette is the fallback path for users who
+ // haven't pinned a box yet).
+ //
+ // Registered eagerly on DOMContentLoaded — must not depend on the
+ // user having opened the console at least once.
+ ConsoleManager.prototype._wireGlobalShortcut = function () {
+ if (this._globalShortcutWired) return;
+ this._globalShortcutWired = true;
+ const self = this;
+ document.addEventListener('keydown', function (e) {
+ if (!e.ctrlKey || !e.shiftKey || e.altKey || e.metaKey) return;
+ // Match both literal backtick and "Backquote" code so Dvorak
+ // and other non-QWERTY layouts work too.
+ if (e.key !== '`' && e.code !== 'Backquote') return;
+ e.preventDefault();
+ const boxID = readCookie('gearbox_active_box');
+ if (!boxID) return;
+ self.open({ kind: 'box', boxID: boxID, label: boxID });
+ });
+ };
+
ConsoleManager.prototype._wireDockResize = function () {
const handle = this._$('console-dock-handle');
if (!handle) return;
@@ -505,6 +766,16 @@
const sess = new ConsoleSession(descriptor);
sess.fontSize = this.fontSize;
sess.onStatusChange = this._renderStatus.bind(this);
+ // Route per-session keyboard shortcuts (currently just Ctrl-F for
+ // search) up to the manager so the chrome can react.
+ const self = this;
+ sess.onKeyIntercept = function (action) {
+ if (action === 'search-open') {
+ self.openSearch();
+ return true;
+ }
+ return false;
+ };
this.sessions.push(sess);
this._setActive(sess.id);
this._show();
@@ -615,6 +886,46 @@
// we don't want to rip a live shell or stack a second connect on
// top of one already in flight. Also makes the clicked tab active
// so the reconnect happens where the user can see it.
+ // Inline rename: replace the label button with a text input scoped
+ // to this tab, commit on blur / Enter, revert on Escape. Re-renders
+ // the tab strip after commit so the # disambiguation suffixes
+ // update if needed.
+ ConsoleManager.prototype._beginRename = function (sessionID, labelEl) {
+ const idx = this._indexByID(sessionID);
+ if (idx < 0 || !labelEl) return;
+ const s = this.sessions[idx];
+ const original = s.label;
+ const input = document.createElement('input');
+ input.type = 'text';
+ input.className = 'console-tab-rename';
+ input.value = original;
+ input.setAttribute('aria-label', 'Rename tab');
+ labelEl.replaceWith(input);
+ input.focus();
+ input.select();
+
+ const self = this;
+ let committed = false;
+ const commit = function (next) {
+ if (committed) return;
+ committed = true;
+ const v = (next || '').trim();
+ s.label = v || s.baseLabel;
+ self._renderTabs();
+ };
+
+ input.addEventListener('blur', function () { commit(input.value); });
+ input.addEventListener('keydown', function (e) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ commit(input.value);
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ commit(original);
+ }
+ });
+ };
+
ConsoleManager.prototype._tabDotClick = function (sessionID) {
const idx = this._indexByID(sessionID);
if (idx < 0) return;
@@ -739,7 +1050,7 @@
label.type = 'button';
label.className = 'console-tab-label';
label.textContent = display;
- label.title = display + ' (' + s.status + ')';
+ label.title = display + ' (' + s.status + ') — double-click to rename';
// Label button carries the role="tab" semantics so screen
// readers expose the tab list correctly (one role="tab" per
// tab, focusable, with aria-selected reflecting active).
@@ -747,6 +1058,17 @@
label.setAttribute('aria-selected', s.id === self.activeID ? 'true' : 'false');
label.addEventListener('click', function () { self._setActive(s.id); });
+ // Double-click swaps the label into an inline text input.
+ // Blur or Enter commits; Escape reverts. The rename is
+ // per-session (lives on s.label) and persists for as long
+ // as the tab is open — no localStorage, since the box's
+ // canonical name still lives in s.baseLabel.
+ label.addEventListener('dblclick', function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ self._beginRename(s.id, label);
+ });
+
const close = document.createElement('button');
close.type = 'button';
close.className = 'console-tab-close';
@@ -795,6 +1117,19 @@
manager: manager,
};
+ // Eagerly wire the global Ctrl-Shift-` shortcut so it works even
+ // before the user has opened the console once. Other wiring (dock
+ // resize, search bar, paste modal) needs the drawer markup present
+ // and stays inside the lazy _wire() path.
+ function eagerWire() {
+ try { manager._wireGlobalShortcut(); } catch (_) {}
+ }
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', eagerWire);
+ } else {
+ eagerWire();
+ }
+
window.gearbox.console.markPopout = function () {
manager.popoutMode = true;
manager.layout = 'drawer';
diff --git a/gearbox/static/js/vendor/xterm-addon-search.min.js b/gearbox/static/js/vendor/xterm-addon-search.min.js
new file mode 100644
index 0000000..6890bab
--- /dev/null
+++ b/gearbox/static/js/vendor/xterm-addon-search.min.js
@@ -0,0 +1,8 @@
+/**
+ * Skipped minification because the original files appears to be already minified.
+ * Original file: /npm/@xterm/addon-search@0.15.0/lib/addon-search.js
+ *
+ * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
+ */
+!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SearchAddon=t():e.SearchAddon=t()}(self,(()=>(()=>{"use strict";var e={345:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.runAndSubscribe=t.forwardEvent=t.EventEmitter=void 0,t.EventEmitter=class{constructor(){this._listeners=[],this._disposed=!1}get event(){return this._event||(this._event=e=>(this._listeners.push(e),{dispose:()=>{if(!this._disposed)for(let t=0;tt.fire(e)))},t.runAndSubscribe=function(e,t){return t(void 0),e((e=>t(e)))}},859:(e,t)=>{function i(e){for(const t of e)t.dispose();e.length=0}Object.defineProperty(t,"__esModule",{value:!0}),t.getDisposeArrayDisposable=t.disposeArray=t.toDisposable=t.MutableDisposable=t.Disposable=void 0,t.Disposable=class{constructor(){this._disposables=[],this._isDisposed=!1}dispose(){this._isDisposed=!0;for(const e of this._disposables)e.dispose();this._disposables.length=0}register(e){return this._disposables.push(e),e}unregister(e){const t=this._disposables.indexOf(e);-1!==t&&this._disposables.splice(t,1)}},t.MutableDisposable=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(e){this._isDisposed||e===this._value||(this._value?.dispose(),this._value=e)}clear(){this.value=void 0}dispose(){this._isDisposed=!0,this._value?.dispose(),this._value=void 0}},t.toDisposable=function(e){return{dispose:e}},t.disposeArray=i,t.getDisposeArrayDisposable=function(e){return{dispose:()=>i(e)}}}},t={};function i(s){var r=t[s];if(void 0!==r)return r.exports;var o=t[s]={exports:{}};return e[s](o,o.exports,i),o.exports}var s={};return(()=>{var e=s;Object.defineProperty(e,"__esModule",{value:!0}),e.SearchAddon=void 0;const t=i(345),r=i(859),o=" ~!@#$%^&*()+`-=[]{}|\\;:\"',./<>?";class n extends r.Disposable{constructor(e){super(),this._highlightedLines=new Set,this._highlightDecorations=[],this._selectedDecoration=this.register(new r.MutableDisposable),this._linesCacheTimeoutId=0,this._linesCacheDisposables=new r.MutableDisposable,this._onDidChangeResults=this.register(new t.EventEmitter),this.onDidChangeResults=this._onDidChangeResults.event,this._highlightLimit=e?.highlightLimit??1e3}activate(e){this._terminal=e,this.register(this._terminal.onWriteParsed((()=>this._updateMatches()))),this.register(this._terminal.onResize((()=>this._updateMatches()))),this.register((0,r.toDisposable)((()=>this.clearDecorations())))}_updateMatches(){this._highlightTimeout&&window.clearTimeout(this._highlightTimeout),this._cachedSearchTerm&&this._lastSearchOptions?.decorations&&(this._highlightTimeout=setTimeout((()=>{const e=this._cachedSearchTerm;this._cachedSearchTerm=void 0,this.findPrevious(e,{...this._lastSearchOptions,incremental:!0,noScroll:!0})}),200))}clearDecorations(e){this._selectedDecoration.clear(),(0,r.disposeArray)(this._highlightDecorations),this._highlightDecorations=[],this._highlightedLines.clear(),e||(this._cachedSearchTerm=void 0)}clearActiveDecoration(){this._selectedDecoration.clear()}findNext(e,t){if(!this._terminal)throw new Error("Cannot use addon until it has been loaded");const i=!this._lastSearchOptions||this._didOptionsChange(this._lastSearchOptions,t);this._lastSearchOptions=t,t?.decorations&&(void 0===this._cachedSearchTerm||e!==this._cachedSearchTerm||i)&&this._highlightAllMatches(e,t);const s=this._findNextAndSelect(e,t);return this._fireResults(t),this._cachedSearchTerm=e,s}_highlightAllMatches(e,t){if(!this._terminal)throw new Error("Cannot use addon until it has been loaded");if(!e||0===e.length)return void this.clearDecorations();t=t||{},this.clearDecorations(!0);const i=[];let s,r=this._find(e,0,0,t);for(;r&&(s?.row!==r.row||s?.col!==r.col)&&!(i.length>=this._highlightLimit);)s=r,i.push(s),r=this._find(e,s.col+s.term.length>=this._terminal.cols?s.row+1:s.row,s.col+s.term.length>=this._terminal.cols?0:s.col+1,t);for(const e of i){const i=this._createResultDecoration(e,t.decorations);i&&(this._highlightedLines.add(i.marker.line),this._highlightDecorations.push({decoration:i,match:e,dispose(){i.dispose()}}))}}_find(e,t,i,s){if(!this._terminal||!e||0===e.length)return this._terminal?.clearSelection(),void this.clearDecorations();if(i>this._terminal.cols)throw new Error(`Invalid col: ${i} to search in terminal of ${this._terminal.cols} cols`);let r;this._initLinesCache();const o={startRow:t,startCol:i};if(r=this._findInLine(e,o,s),!r)for(let i=t+1;i=0&&(n.startRow=i,h=this._findInLine(e,n,t,o),!h);i--);}if(!h&&s!==this._terminal.buffer.active.baseY+this._terminal.rows-1)for(let i=this._terminal.buffer.active.baseY+this._terminal.rows-1;i>=s&&(n.startRow=i,h=this._findInLine(e,n,t,o),!h);i--);return this._selectResult(h,t?.decorations,t?.noScroll)}_initLinesCache(){const e=this._terminal;this._linesCache||(this._linesCache=new Array(e.buffer.active.length),this._linesCacheDisposables.value=(0,r.getDisposeArrayDisposable)([e.onLineFeed((()=>this._destroyLinesCache())),e.onCursorMove((()=>this._destroyLinesCache())),e.onResize((()=>this._destroyLinesCache()))])),window.clearTimeout(this._linesCacheTimeoutId),this._linesCacheTimeoutId=window.setTimeout((()=>this._destroyLinesCache()),15e3)}_destroyLinesCache(){this._linesCache=void 0,this._linesCacheDisposables.clear(),this._linesCacheTimeoutId&&(window.clearTimeout(this._linesCacheTimeoutId),this._linesCacheTimeoutId=0)}_isWholeWord(e,t,i){return(0===e||o.includes(t[e-1]))&&(e+i.length===t.length||o.includes(t[e+i.length]))}_findInLine(e,t,i={},s=!1){const r=this._terminal,o=t.startRow,n=t.startCol,h=r.buffer.active.getLine(o);if(h?.isWrapped)return s?void(t.startCol+=r.cols):(t.startRow--,t.startCol+=r.cols,this._findInLine(e,t,i));let a=this._linesCache?.[o];a||(a=this._translateBufferLineToStringWithWrap(o,!0),this._linesCache&&(this._linesCache[o]=a));const[l,c]=a,d=this._bufferColsToStringOffset(o,n),_=i.caseSensitive?e:e.toLowerCase(),u=i.caseSensitive?l:l.toLowerCase();let f=-1;if(i.regex){const t=RegExp(_,"g");let i;if(s)for(;i=t.exec(u.slice(0,d));)f=t.lastIndex-i[0].length,e=i[0],t.lastIndex-=e.length-1;else i=t.exec(u.slice(d)),i&&i[0].length>0&&(f=d+(t.lastIndex-i[0].length),e=i[0])}else s?d-_.length>=0&&(f=u.lastIndexOf(_,d-_.length)):f=u.indexOf(_,d);if(f>=0){if(i.wholeWord&&!this._isWholeWord(f,u,e))return;let t=0;for(;t=c[t+1];)t++;let s=t;for(;s=c[s+1];)s++;const n=f-c[t],h=f+e.length-c[s],a=this._stringLengthToBufferSize(o+t,n);return{term:e,col:a,row:o+t,size:this._stringLengthToBufferSize(o+s,h)-a+r.cols*(s-t)}}}_stringLengthToBufferSize(e,t){const i=this._terminal.buffer.active.getLine(e);if(!i)return 0;for(let e=0;e1&&(t-=r.length-1);const o=i.getCell(e+1);o&&0===o.getWidth()&&t++}return t}_bufferColsToStringOffset(e,t){const i=this._terminal;let s=e,r=0,o=i.buffer.active.getLine(s);for(;t>0&&o;){for(let e=0;ethis._applyStyles(e,t.activeMatchBorder,!0)))),s.push(o.onDispose((()=>(0,r.disposeArray)(s)))),this._selectedDecoration.value={decoration:o,match:e,dispose(){o.dispose()}}}}}if(!i&&(e.row>=s.buffer.active.viewportY+s.rows||e.rowthis._applyStyles(e,t.matchBorder,!1)))),e.push(o.onDispose((()=>(0,r.disposeArray)(e))))}return o}}e.SearchAddon=n})(),s})()));
+//# sourceMappingURL=addon-search.js.map
\ No newline at end of file