Merge pull request #704 from Tyriar/553_find_api

Implement very basic find API
This commit is contained in:
Daniel Imms
2017-07-13 11:12:45 -07:00
committed by GitHub
10 changed files with 357 additions and 60 deletions
+8
View File
@@ -11,10 +11,18 @@
<script src="/build/addons/attach/attach.js" ></script>
<script src="/build/addons/fit/fit.js" ></script>
<script src="/build/addons/fullscreen/fullscreen.js" ></script>
<script src="/build/addons/search/search.js" ></script>
</head>
<body>
<h1>xterm.js: xterm, in the browser</h1>
<div id="terminal-container"></div>
<div>
<h2>Actions</h2>
<p>
<label>Find next <input id="find-next"/></label>
<label>Find previous <input id="find-previous"/></label>
</p>
</div>
<div>
<h2>Options</h2>
<p>
+17
View File
@@ -7,6 +7,10 @@ var term,
charHeight;
var terminalContainer = document.getElementById('terminal-container'),
actionElements = {
findNext: document.querySelector('#find-next'),
findPrevious: document.querySelector('#find-previous')
},
optionElements = {
cursorBlink: document.querySelector('#option-cursor-blink'),
cursorStyle: document.querySelector('#option-cursor-style'),
@@ -30,6 +34,19 @@ function setTerminalSize () {
colsElement.addEventListener('change', setTerminalSize);
rowsElement.addEventListener('change', setTerminalSize);
actionElements.findNext.addEventListener('keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findNext(actionElements.findNext.value);
}
});
actionElements.findPrevious.addEventListener('keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findPrevious(actionElements.findPrevious.value);
}
});
optionElements.cursorBlink.addEventListener('change', function () {
term.setOption('cursorBlink', optionElements.cursorBlink.checked);
});
+47 -7
View File
@@ -1,3 +1,7 @@
/**
* @license MIT
*/
const browserify = require('browserify');
const buffer = require('vinyl-buffer');
const coveralls = require('gulp-coveralls');
@@ -14,6 +18,7 @@ const ts = require('gulp-typescript');
let buildDir = process.env.BUILD_DIR || 'build';
let tsProject = ts.createProject('tsconfig.json');
let tsProjectSearchAddon = ts.createProject('./src/addons/search/tsconfig.json');
let srcDir = tsProject.config.compilerOptions.rootDir;
let outDir = tsProject.config.compilerOptions.outDir;
@@ -30,13 +35,17 @@ gulp.task('tsc', function () {
let tsResult = tsProject.src().pipe(sourcemaps.init()).pipe(tsProject());
let tsc = tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir));
fs.emptyDirSync(`${outDir}/addons/search`);
let tsResultSearchAddon = tsProjectSearchAddon.src().pipe(sourcemaps.init()).pipe(tsProjectSearchAddon());
let tscSearchAddon = tsResultSearchAddon.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(`${outDir}/addons/search`));
// Copy all addons from ${srcDir}/ to ${outDir}/
let copyAddons = gulp.src(`${srcDir}/addons/**/*`).pipe(gulp.dest(`${outDir}/addons`));
let copyAddons = gulp.src([`${srcDir}/addons/**/*`, `!${srcDir}/addons/search`, `!${srcDir}/addons/search/**`]).pipe(gulp.dest(`${outDir}/addons`));
// Copy stylesheets from ${srcDir}/ to ${outDir}/
let copyStylesheets = gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir));
return merge(tsc, copyAddons, copyStylesheets);
return merge(tsc, tscSearchAddon, copyAddons, copyStylesheets);
});
/**
@@ -63,13 +72,38 @@ gulp.task('browserify', ['tsc'], function() {
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(buildDir));
// Copy all add-ons from ${outDir}/ to buildDir
let copyAddons = gulp.src(`${outDir}/addons/**/*`).pipe(gulp.dest(`${buildDir}/addons`));
// Copy stylesheets from ${outDir}/ to ${buildDir}/
let copyStylesheets = gulp.src(`${outDir}/**/*.css`).pipe(gulp.dest(buildDir));
return merge(bundleStream, copyAddons, copyStylesheets);
return merge(bundleStream, copyStylesheets);
});
gulp.task('browserify-addons', ['tsc'], function() {
let searchOptions = {
basedir: `${buildDir}/addons/search`,
debug: true,
entries: [`${outDir}/addons/search/search.js`],
cache: {},
packageCache: {}
};
let searchBundle = browserify(searchOptions)
.bundle()
.pipe(source('./addons/search/search.js'))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: ''}))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(buildDir));
// Copy all add-ons from outDir to buildDir
let copyAddons = gulp.src([
// Copy JS addons
`${outDir}/addons/**/*`,
// Exclude TS addons from copy as they are being built via browserify
`!${outDir}/addons/search`,
`!${outDir}/addons/search/**`
]).pipe(gulp.dest(`${buildDir}/addons`));
return merge(searchBundle, copyAddons);
});
gulp.task('instrument-test', function () {
@@ -98,6 +132,12 @@ gulp.task('sorcery', ['browserify'], function () {
chain.writeSync();
});
gulp.task('sorcery-addons', ['browserify-addons'], function () {
var chain = sorcery.loadSync(`${buildDir}/addons/search/search.js`);
chain.apply();
chain.writeSync();
});
/**
* Submit coverage results to coveralls.io
*/
@@ -106,6 +146,6 @@ gulp.task('coveralls', function () {
.pipe(coveralls());
});
gulp.task('build', ['sorcery']);
gulp.task('build', ['sorcery', 'sorcery-addons']);
gulp.task('test', ['mocha']);
gulp.task('default', ['build']);
+5
View File
@@ -21,6 +21,7 @@ export interface ITerminal {
element: HTMLElement;
rowContainer: HTMLElement;
selectionContainer: HTMLElement;
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
textarea: HTMLTextAreaElement;
ybase: number;
@@ -51,6 +52,10 @@ export interface ITerminal {
export interface ISelectionManager {
selectionText: string;
selectionStart: [number, number];
selectionEnd: [number, number];
setSelection(row: number, col: number, length: number);
}
export interface ICharMeasure {
+16 -53
View File
@@ -9,6 +9,7 @@ import { CircularList } from './utils/CircularList';
import { EventEmitter } from './EventEmitter';
import { ITerminal } from './Interfaces';
import { SelectionModel } from './SelectionModel';
import { translateBufferLineToString } from './utils/BufferLine';
/**
* The number of pixels the mouse needs to be above or below the viewport in
@@ -163,6 +164,9 @@ export class SelectionManager extends EventEmitter {
this.clearSelection();
}
public get selectionStart(): [number, number] { return this._model.finalSelectionStart; }
public get selectionEnd(): [number, number] { return this._model.finalSelectionEnd; }
/**
* Gets whether there is an active text selection.
*/
@@ -188,12 +192,12 @@ export class SelectionManager extends EventEmitter {
// Get first row
const startRowEndCol = start[1] === end[1] ? end[0] : null;
let result: string[] = [];
result.push(this._translateBufferLineToString(this._buffer.get(start[1]), true, start[0], startRowEndCol));
result.push(translateBufferLineToString(this._buffer.get(start[1]), true, start[0], startRowEndCol));
// Get middle rows
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
const bufferLine = this._buffer.get(i);
const lineText = this._translateBufferLineToString(bufferLine, true);
const lineText = translateBufferLineToString(bufferLine, true);
if (bufferLine.isWrapped) {
result[result.length - 1] += lineText;
} else {
@@ -204,7 +208,7 @@ export class SelectionManager extends EventEmitter {
// Get final row
if (start[1] !== end[1]) {
const bufferLine = this._buffer.get(end[1]);
const lineText = this._translateBufferLineToString(bufferLine, true, 0, end[0]);
const lineText = translateBufferLineToString(bufferLine, true, 0, end[0]);
if (bufferLine.isWrapped) {
result[result.length - 1] += lineText;
} else {
@@ -230,55 +234,6 @@ export class SelectionManager extends EventEmitter {
this.refresh();
}
/**
* Translates a buffer line to a string, with optional start and end columns.
* 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 line The line being translated.
* @param trimRight Whether to trim whitespace to the right.
* @param startCol The column to start at.
* @param endCol The column to end at.
*/
private _translateBufferLineToString(line: any, trimRight: boolean, startCol: number = 0, endCol: number = null): string {
// TODO: This function should live in a buffer or buffer line class
// Get full line
let lineString = '';
let widthAdjustedStartCol = startCol;
let widthAdjustedEndCol = endCol;
for (let i = 0; i < line.length; i++) {
const char = line[i];
lineString += char[LINE_DATA_CHAR_INDEX];
// Adjust start and end cols for wide characters if they affect their
// column indexes
if (char[LINE_DATA_WIDTH_INDEX] === 0) {
if (startCol >= i) {
widthAdjustedStartCol--;
}
if (endCol >= i) {
widthAdjustedEndCol--;
}
}
}
// Calculate the final end col by trimming whitespace on the right of the
// line if needed.
let finalEndCol = widthAdjustedEndCol || line.length;
if (trimRight) {
const rightWhitespaceIndex = lineString.search(/\s+$/);
if (rightWhitespaceIndex !== -1) {
finalEndCol = Math.min(finalEndCol, rightWhitespaceIndex);
}
// Return the empty string if only trimmed whitespace is selected
if (finalEndCol <= widthAdjustedStartCol) {
return '';
}
}
return lineString.substring(widthAdjustedStartCol, finalEndCol);
}
/**
* Queues a refresh, redrawing the selection on the next opportunity.
* @param isNewSelection Whether the selection should be registered as a new
@@ -565,13 +520,21 @@ export class SelectionManager extends EventEmitter {
return charIndex;
}
public setSelection(col: number, row: number, length: number): void {
this._model.clearSelection();
this._removeMouseDownListeners();
this._model.selectionStart = [col, row];
this._model.selectionStartLength = length;
this.refresh();
}
/**
* Gets positional information for the word at the coordinated specified.
* @param coords The coordinates to get the word at.
*/
private _getWordAt(coords: [number, number]): IWordPosition {
const bufferLine = this._buffer.get(coords[1]);
const line = this._translateBufferLineToString(bufferLine, false);
const line = translateBufferLineToString(bufferLine, false);
// Get actual index, taking into consideration wide characters
let endIndex = this._convertViewportColToCharacterIndex(bufferLine, coords);
+140
View File
@@ -0,0 +1,140 @@
/**
* @license MIT
*/
// import { ITerminal } from '../../Interfaces';
// import { translateBufferLineToString } from '../../utils/BufferLine';
interface ISearchResult {
term: string;
col: number;
row: number;
}
/**
* A class that knows how to search the terminal and how to display the results.
*/
export class SearchHelper {
constructor(private _terminal: any, private _translateBufferLineToString: any) {
// TODO: Search for multiple instances on 1 line
// TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted
// TODO: Highlight other instances in the viewport
// TODO: Support regex, case sensitivity, etc.
}
/**
* Find the next instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @return Whether a result was found.
*/
public findNext(term: string): boolean {
if (!term || term.length === 0) {
return false;
}
let result: ISearchResult;
let startRow = this._terminal.ydisp;
if (this._terminal.selectionManager.selectionEnd) {
// Start from the selection end if there is a selection
startRow = this._terminal.selectionManager.selectionEnd[1];
}
// Search from ydisp + 1 to end
for (let y = startRow + 1; y < this._terminal.ybase + this._terminal.rows; y++) {
result = this._findInLine(term, y);
if (result) {
break;
}
}
// Search from the top to the current ydisp
if (!result) {
for (let y = 0; y < startRow; y++) {
result = this._findInLine(term, y);
if (result) {
break;
}
}
}
// 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 Tne search term.
* @return Whether a result was found.
*/
public findPrevious(term: string): boolean {
if (!term || term.length === 0) {
return false;
}
let result: ISearchResult;
let startRow = this._terminal.ydisp;
if (this._terminal.selectionManager.selectionStart) {
// Start from the selection end if there is a selection
startRow = this._terminal.selectionManager.selectionStart[1];
}
// Search from ydisp + 1 to end
for (let y = startRow - 1; y >= 0; y--) {
result = this._findInLine(term, y);
if (result) {
break;
}
}
// Search from the top to the current ydisp
if (!result) {
for (let y = this._terminal.ybase + this._terminal.rows - 1; y > startRow; y--) {
result = this._findInLine(term, y);
if (result) {
break;
}
}
}
// Set selection and scroll if a result was found
return this._selectResult(result);
}
/**
* Searches a line for a search term.
* @param term Tne search term.
* @param y The line to search.
* @return The search result if it was found.
*/
private _findInLine(term: string, y: number): ISearchResult {
const bufferLine = this._terminal.lines.get(y);
const lowerStringLine = this._translateBufferLineToString(bufferLine, true).toLowerCase();
const lowerTerm = term.toLowerCase();
const searchIndex = lowerStringLine.indexOf(lowerTerm);
if (searchIndex >= 0) {
return {
term,
col: searchIndex,
row: y
};
}
}
/**
* Selects and scrolls to a result.
* @param result The result to select.
* @return Whethera result was selected.
*/
private _selectResult(result: ISearchResult): boolean {
if (!result) {
return false;
}
this._terminal.selectionManager.setSelection(result.col, result.row, result.term.length);
this._terminal.scrollDisp(result.row - this._terminal.ydisp, false);
return true;
}
}
+57
View File
@@ -0,0 +1,57 @@
/**
* @license MIT
*/
import { SearchHelper } from './SearchHelper';
declare var exports: any;
declare var module: any;
declare var define: any;
declare var require: any;
declare var window: any;
(function (addon) {
if ('Terminal' in window) {
/**
* Plain browser environment
*/
addon(window.Terminal);
} else if (typeof exports === 'object' && typeof module === 'object') {
/**
* CommonJS environment
*/
const xterm = '../../xterm';
module.exports = addon(require(xterm));
} else if (typeof define == 'function') {
/**
* Require.js is available
*/
define(['../../xterm'], addon);
}
})((Terminal: any) => {
/**
* Find the next instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @return Whether a result was found.
*/
Terminal.prototype.findNext = function(term: string): boolean {
if (!this._searchHelper) {
this.searchHelper = new SearchHelper(this, Terminal.translateBufferLineToString);
}
return (<SearchHelper>this.searchHelper).findNext(term);
};
/**
* Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @return Whether a result was found.
*/
Terminal.prototype.findPrevious = function(term: string): boolean {
if (!this._searchHelper) {
this.searchHelper = new SearchHelper(this, Terminal.translateBufferLineToString);
}
return (<SearchHelper>this.searchHelper).findPrevious(term);
};
});
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"rootDir": ".",
"outDir": "../../../lib/addons/search/",
"sourceMap": true,
"removeComments": true
}
}
+55
View File
@@ -0,0 +1,55 @@
/**
* @license MIT
*/
// TODO: This module should be merged into a buffer or buffer line class
const LINE_DATA_CHAR_INDEX = 1;
const LINE_DATA_WIDTH_INDEX = 2;
/**
* Translates a buffer line to a string, with optional start and end columns.
* 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 line The line being translated.
* @param trimRight Whether to trim whitespace to the right.
* @param startCol The column to start at.
* @param endCol The column to end at.
*/
export function translateBufferLineToString(line: any, trimRight: boolean, startCol: number = 0, endCol: number = null): string {
// Get full line
let lineString = '';
let widthAdjustedStartCol = startCol;
let widthAdjustedEndCol = endCol;
for (let i = 0; i < line.length; i++) {
const char = line[i];
lineString += char[LINE_DATA_CHAR_INDEX];
// Adjust start and end cols for wide characters if they affect their
// column indexes
if (char[LINE_DATA_WIDTH_INDEX] === 0) {
if (startCol >= i) {
widthAdjustedStartCol--;
}
if (endCol >= i) {
widthAdjustedEndCol--;
}
}
}
// Calculate the final end col by trimming whitespace on the right of the
// line if needed.
let finalEndCol = widthAdjustedEndCol || line.length;
if (trimRight) {
const rightWhitespaceIndex = lineString.search(/\s+$/);
if (rightWhitespaceIndex !== -1) {
finalEndCol = Math.min(finalEndCol, rightWhitespaceIndex);
}
// Return the empty string if only trimmed whitespace is selected
if (finalEndCol <= widthAdjustedStartCol) {
return '';
}
}
return lineString.substring(widthAdjustedStartCol, finalEndCol);
}
+2
View File
@@ -26,6 +26,7 @@ import * as Browser from './utils/Browser';
import * as Mouse from './utils/Mouse';
import { CHARSETS } from './Charsets';
import { getRawByteCoords } from './utils/Mouse';
import { translateBufferLineToString } from './utils/BufferLine';
/**
* Terminal Emulation References:
@@ -2453,6 +2454,7 @@ function keys(obj) {
* Expose
*/
Terminal.translateBufferLineToString = translateBufferLineToString;
Terminal.EventEmitter = EventEmitter;
Terminal.inherits = inherits;