mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 00:03:15 +00:00
SearchEngine's forward scans walk every buffer row and _findInLine rewinds each wrapped row back to the start of its line, so a single long un-newlined line costs O(rows^2) on the renderer's main thread. The find bar re-runs that scan on every keystroke and again 200ms after every onWriteParsed, so a pane holding one long line freezes repeatedly rather than searching. A wrapped row's text was already searched when the first row of its line was, so skip it. Only sound for a plain search: wholeWord and regex can reject at one offset and match at a later one in the same line, so both keep the old walk. Measured, one un-newlined line at 80 cols filling the default 5000-row scrollback, no match (real Terminal + SearchAddon, happy-dom): upstream 17046 ms with the recursion fix 18500 ms with this commit 6 ms
86 lines
4.0 KiB
Diff
86 lines
4.0 KiB
Diff
diff --git a/src/SearchEngine.ts b/src/SearchEngine.ts
|
|
index 1760bc2bd1fd274d23e2032fde631b39c739f0d9..4ce664a7b913c5c19d6bbf7feba92cbdaef38c6f 100644
|
|
--- a/src/SearchEngine.ts
|
|
+++ b/src/SearchEngine.ts
|
|
@@ -76,6 +76,9 @@ export class SearchEngine {
|
|
// Search from startRow + 1 to end
|
|
if (!result) {
|
|
for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
|
|
+ if (this._isRowCoveredByLineStart(y, searchOptions)) {
|
|
+ continue;
|
|
+ }
|
|
searchPosition.startRow = y;
|
|
searchPosition.startCol = 0;
|
|
result = this._findInLine(term, searchPosition, searchOptions);
|
|
@@ -127,6 +130,9 @@ export class SearchEngine {
|
|
// Search from startRow + 1 to end
|
|
if (!result) {
|
|
for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
|
|
+ if (this._isRowCoveredByLineStart(y, searchOptions)) {
|
|
+ continue;
|
|
+ }
|
|
searchPosition.startRow = y;
|
|
searchPosition.startCol = 0;
|
|
result = this._findInLine(term, searchPosition, searchOptions);
|
|
@@ -138,6 +144,9 @@ export class SearchEngine {
|
|
// If we hit the bottom and didn't search from the very top wrap back up
|
|
if (!result && startRow !== 0) {
|
|
for (let y = 0; y < startRow; y++) {
|
|
+ if (this._isRowCoveredByLineStart(y, searchOptions)) {
|
|
+ continue;
|
|
+ }
|
|
searchPosition.startRow = y;
|
|
searchPosition.startCol = 0;
|
|
result = this._findInLine(term, searchPosition, searchOptions);
|
|
@@ -237,6 +246,19 @@ export class SearchEngine {
|
|
(((searchIndex + term.length) === line.length) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));
|
|
}
|
|
|
|
+ /**
|
|
+ * A wrapped row's text was already searched in full when the first row of its line was, so a
|
|
+ * full-buffer scan that visits every row does O(rows^2) work over one long line. Skipping is
|
|
+ * only sound for a plain search: wholeWord and regex can both reject at one offset and match at
|
|
+ * a later one within the same line.
|
|
+ */
|
|
+ private _isRowCoveredByLineStart(row: number, searchOptions?: ISearchOptions): boolean {
|
|
+ if (searchOptions?.regex || searchOptions?.wholeWord) {
|
|
+ return false;
|
|
+ }
|
|
+ return this._terminal.buffer.active.getLine(row)?.isWrapped === true;
|
|
+ }
|
|
+
|
|
/**
|
|
* Searches a line for a search term. Takes the provided terminal line and searches the text line,
|
|
* which may contain subsequent terminal lines if the text is wrapped. If the provided line number
|
|
@@ -250,23 +272,19 @@ export class SearchEngine {
|
|
* @returns The search result if it was found.
|
|
*/
|
|
private _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
|
|
- const row = searchPosition.startRow;
|
|
- const col = searchPosition.startCol;
|
|
-
|
|
// Ignore wrapped lines, only consider on unwrapped line (first row of command string).
|
|
- const firstLine = this._terminal.buffer.active.getLine(row);
|
|
- if (firstLine?.isWrapped) {
|
|
+ // This walks back to the line start rather than recursing into it: one frame per wrapped
|
|
+ // row overflows the stack on a line long enough to fill the scrollback.
|
|
+ while (this._terminal.buffer.active.getLine(searchPosition.startRow)?.isWrapped) {
|
|
+ searchPosition.startCol += this._terminal.cols;
|
|
if (isReverseSearch) {
|
|
- searchPosition.startCol += this._terminal.cols;
|
|
return;
|
|
}
|
|
-
|
|
- // This will iterate until we find the line start.
|
|
- // When we find it, we will search using the calculated start column.
|
|
searchPosition.startRow--;
|
|
- searchPosition.startCol += this._terminal.cols;
|
|
- return this._findInLine(term, searchPosition, searchOptions);
|
|
}
|
|
+ const row = searchPosition.startRow;
|
|
+ const col = searchPosition.startCol;
|
|
+
|
|
let cache = this._lineCache.getLineFromCache(row);
|
|
if (!cache) {
|
|
cache = this._lineCache.translateBufferLineToStringWithWrap(row, true);
|