"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.SearchAddon = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ /** * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs class SearchAddon { constructor() { (0, _defineProperty2.default)(this, "_terminal", void 0); /** * translateBufferLineToStringWithWrap is a fairly expensive call. * We memoize the calls into an array that has a time based ttl. * _linesCache is also invalidated when the terminal cursor moves. */ (0, _defineProperty2.default)(this, "_linesCache", void 0); (0, _defineProperty2.default)(this, "_linesCacheTimeoutId", 0); (0, _defineProperty2.default)(this, "_cursorMoveListener", void 0); (0, _defineProperty2.default)(this, "_resizeListener", void 0); } activate(terminal) { this._terminal = terminal; } dispose() {} /** * Find the next instance of the term, then scroll to and select it. If it * doesn't exist, do nothing. * @param term The search term. * @param searchOptions Search options. * @return Whether a result was found. */ findNext(term, searchOptions) { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } if (!term || term.length === 0) { this._terminal.clearSelection(); return false; } let startCol = 0; let startRow = 0; let currentSelection; if (this._terminal.hasSelection()) { const incremental = searchOptions ? searchOptions.incremental : false; // Start from the selection end if there is a selection // For incremental search, use existing row currentSelection = this._terminal.getSelectionPosition(); startRow = incremental ? currentSelection.start.y : currentSelection.end.y; startCol = incremental ? currentSelection.start.x : currentSelection.end.x; } if (searchOptions !== null && searchOptions !== void 0 && searchOptions.lastLineOnly) { startRow = this._terminal.buffer.active.cursorY; startCol = (searchOptions === null || searchOptions === void 0 ? void 0 : searchOptions.startCol) || 0; } this._initLinesCache(); const searchPosition = { startRow, startCol }; // Search startRow let result = this._findInLine(term, searchPosition, searchOptions); // Search from startRow + 1 to end if (!result) { for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) { searchPosition.startRow = y; searchPosition.startCol = 0; // If the current line is wrapped line, increase index of column to ignore the previous scan // Otherwise, reset beginning column index to zero with set new unwrapped line index result = this._findInLine(term, searchPosition, searchOptions); if (result) { break; } } } // 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++) { searchPosition.startRow = y; searchPosition.startCol = 0; result = this._findInLine(term, searchPosition, searchOptions); if (result) { break; } } } // If there is only one result, wrap back and return selection if it exists. if (!result && currentSelection) { searchPosition.startRow = currentSelection.start.y; searchPosition.startCol = 0; result = this._findInLine(term, searchPosition, searchOptions); } // Set selection and scroll if a result was found return this._selectResult(result); } /** * Find the previous instance of the term, then scroll to and select it. If it * doesn't exist, do nothing. * @param term The search term. * @param searchOptions Search options. * @return Whether a result was found. */ findPrevious(term, searchOptions) { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } if (!term || term.length === 0) { this._terminal.clearSelection(); return false; } const isReverseSearch = true; let startRow = this._terminal.buffer.active.baseY + this._terminal.rows; let startCol = this._terminal.cols; let result; const incremental = searchOptions ? searchOptions.incremental : false; let currentSelection; if (this._terminal.hasSelection()) { currentSelection = this._terminal.getSelectionPosition(); // Start from selection start if there is a selection startRow = currentSelection.start.y; startCol = currentSelection.start.x; } else if (searchOptions !== null && searchOptions !== void 0 && searchOptions.lastLineOnly) { startRow = this._terminal.buffer.active.cursorY - 1; startCol = this._terminal.cols; } this._initLinesCache(); const searchPosition = { startRow, startCol }; if (incremental) { // Try to expand selection to right first. result = this._findInLine(term, searchPosition, searchOptions, false); const isOldResultHighlighted = result && result.row === startRow && result.col === startCol; if (!isOldResultHighlighted) { // If selection was not able to be expanded to the right, then try reverse search if (currentSelection) { searchPosition.startRow = currentSelection.end.y; searchPosition.startCol = currentSelection.end.x; } result = this._findInLine(term, searchPosition, searchOptions, true); } } else { result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); } // Search from startRow - 1 to top if (!result) { searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols); for (let y = startRow - 1; y >= 0; y--) { searchPosition.startRow = y; result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); if (result) { break; } } } // If we hit the top and didn't search from the very bottom wrap back down if (!result && startRow !== this._terminal.buffer.active.baseY + this._terminal.rows) { for (let y = this._terminal.buffer.active.baseY + this._terminal.rows; y >= startRow; y--) { searchPosition.startRow = y; result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); if (result) { break; } } } // If there is only one result, return true. if (!result && currentSelection) return true; // Set selection and scroll if a result was found return this._selectResult(result); } /** * Sets up a line cache with a ttl */ _initLinesCache() { const terminal = this._terminal; if (!this._linesCache) { this._linesCache = new Array(terminal.buffer.active.length); this._cursorMoveListener = terminal.onCursorMove(() => this._destroyLinesCache()); this._resizeListener = terminal.onResize(() => this._destroyLinesCache()); } window.clearTimeout(this._linesCacheTimeoutId); this._linesCacheTimeoutId = window.setTimeout(() => this._destroyLinesCache(), LINES_CACHE_TIME_TO_LIVE); } _destroyLinesCache() { this._linesCache = undefined; if (this._cursorMoveListener) { this._cursorMoveListener.dispose(); this._cursorMoveListener = undefined; } if (this._resizeListener) { this._resizeListener.dispose(); this._resizeListener = undefined; } if (this._linesCacheTimeoutId) { window.clearTimeout(this._linesCacheTimeoutId); this._linesCacheTimeoutId = 0; } } /** * 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 is part of a wrapped text line that * started on an earlier line then it is skipped since it will be properly searched when the terminal line that the * text starts on is searched. * @param term The search term. * @param searchPosition The position to start the search. * @param searchOptions Search options. * @param isReverseSearch Whether the search should start from the right side of the terminal and search to the left. * @return The search result if it was found. */ _findInLine(term, searchPosition, searchOptions = {}, isReverseSearch = false) { var _this$_linesCache; const terminal = this._terminal; const row = searchPosition.startRow; const col = searchPosition.startCol; // Ignore wrapped lines, only consider on unwrapped line (first row of command string). const firstLine = terminal.buffer.active.getLine(row); if (firstLine !== null && firstLine !== void 0 && firstLine.isWrapped) { if (isReverseSearch) { searchPosition.startCol += 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 += terminal.cols; return this._findInLine(term, searchPosition, searchOptions); } let cache = (_this$_linesCache = this._linesCache) === null || _this$_linesCache === void 0 ? void 0 : _this$_linesCache[row]; if (!cache) { cache = this._translateBufferLineToStringWithWrap(row, true); if (this._linesCache) { this._linesCache[row] = cache; } } const [stringLine, offsets] = cache; const offset = this._bufferColsToStringOffset(row, col); const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase(); const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase(); let resultIndex = -1; if (searchOptions.regex) { const searchRegex = RegExp(searchTerm, 'g'); let foundTerm; if (isReverseSearch) { // This loop will get the resultIndex of the _last_ regex match in the range 0..offset while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) { resultIndex = searchRegex.lastIndex - foundTerm[0].length; term = foundTerm[0]; searchRegex.lastIndex -= term.length - 1; } } else { foundTerm = searchRegex.exec(searchStringLine.slice(offset)); if (foundTerm && foundTerm[0].length > 0) { resultIndex = offset + (searchRegex.lastIndex - foundTerm[0].length); term = foundTerm[0]; } } } else { if (isReverseSearch) { if (offset - searchTerm.length >= 0) { resultIndex = searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length); } } else { resultIndex = searchStringLine.indexOf(searchTerm, offset); } } if (resultIndex >= 0) { // Adjust the row number and search index if needed since a "line" of text can span multiple rows let startRowOffset = 0; while (startRowOffset < offsets.length - 1 && resultIndex >= offsets[startRowOffset + 1]) { startRowOffset++; } let endRowOffset = startRowOffset; while (endRowOffset < offsets.length - 1 && resultIndex + term.length >= offsets[endRowOffset + 1]) { endRowOffset++; } const startColOffset = resultIndex - offsets[startRowOffset]; const endColOffset = resultIndex + term.length - offsets[endRowOffset]; const startColIndex = this._stringLengthToBufferSize(row + startRowOffset, startColOffset); const endColIndex = this._stringLengthToBufferSize(row + endRowOffset, endColOffset); const size = endColIndex - startColIndex + terminal.cols * (endRowOffset - startRowOffset); return { term, col: startColIndex, row: row + startRowOffset, size }; } } _stringLengthToBufferSize(row, offset) { const line = this._terminal.buffer.active.getLine(row); if (!line) { return 0; } for (let i = 0; i < offset; i++) { const cell = line.getCell(i); if (!cell) { break; } // Adjust the searchIndex to normalize emoji into single chars const char = cell.getChars(); if (char.length > 1) { offset -= char.length - 1; } // Adjust the searchIndex for empty characters following wide unicode // chars (eg. CJK) const nextCell = line.getCell(i + 1); if (nextCell && nextCell.getWidth() === 0) { offset++; } } return offset; } _bufferColsToStringOffset(startRow, cols) { const terminal = this._terminal; let lineIndex = startRow; let offset = 0; let line = terminal.buffer.active.getLine(lineIndex); while (cols > 0 && line) { for (let i = 0; i < cols && i < terminal.cols; i++) { const cell = line.getCell(i); if (!cell) { break; } if (cell.getWidth()) { offset += cell.getChars().length; } } lineIndex++; line = terminal.buffer.active.getLine(lineIndex); if (line && !line.isWrapped) { break; } cols -= terminal.cols; } return offset; } /** * Translates a buffer line to a string, including subsequent lines if they are wraps. * Wide characters will count as two columns in the resulting string. This * function is useful for getting the actual text underneath the raw selection * position. * @param lineIndex The line being translated. * @param trimRight Whether to trim whitespace to the right. */ _translateBufferLineToStringWithWrap(lineIndex, trimRight) { const terminal = this._terminal; const strings = []; const lineOffsets = [0]; let line = terminal.buffer.active.getLine(lineIndex); while (line) { const nextLine = terminal.buffer.active.getLine(lineIndex + 1); const lineWrapsToNext = nextLine ? nextLine.isWrapped : false; let string = line.translateToString(!lineWrapsToNext && trimRight); if (lineWrapsToNext && nextLine) { var _nextLine$getCell; const lastCell = line.getCell(line.length - 1); const lastCellIsNull = lastCell && lastCell.getCode() === 0 && lastCell.getWidth() === 1; // a wide character wrapped to the next line if (lastCellIsNull && ((_nextLine$getCell = nextLine.getCell(0)) === null || _nextLine$getCell === void 0 ? void 0 : _nextLine$getCell.getWidth()) === 2) { string = string.slice(0, -1); } } strings.push(string); if (lineWrapsToNext) { lineOffsets.push(lineOffsets[lineOffsets.length - 1] + string.length); } else { break; } lineIndex++; line = nextLine; } return [strings.join(''), lineOffsets]; } /** * Selects and scrolls to a result. * @param result The result to select. * @return Whethera result was selected. */ _selectResult(result) { const terminal = this._terminal; if (!result) { terminal.clearSelection(); return false; } terminal.select(result.col, result.row, result.size); // If it is not in the viewport then we scroll else it just gets selected if (result.row >= terminal.buffer.active.viewportY + terminal.rows || result.row < terminal.buffer.active.viewportY) { let scroll = result.row - terminal.buffer.active.viewportY; scroll -= Math.floor(terminal.rows / 2); terminal.scrollLines(scroll); } return true; } } exports.SearchAddon = SearchAddon;