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. +
} @@ -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