diff --git a/addons/xterm-addon-search/fixtures/issue-2444 b/addons/xterm-addon-search/fixtures/issue-2444 new file mode 100644 index 000000000..cb7f78783 --- /dev/null +++ b/addons/xterm-addon-search/fixtures/issue-2444 @@ -0,0 +1,70 @@ +/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles +make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release' +make[3]: Nothing to be done for `libsrc/wrap_curl/CMakeFiles/wrap_curl.dir/build'. +make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release' +[ 0%] /usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles +[ 0%] [ 0%] /usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles +Built target config_utils +[ 0%] make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release' +/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles +Built target omega +make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release' +cd /home/yan/ssd/searchtrunk/tmp/release && /usr/bin/cmake -E cmake_depends "Unix Makefiles" /home/yan/ssd/searchtrunk /home/yan/ssd/searchtrunk/projects/protogen/cpp /home/yan/ssd/searchtrunk/tmp/release /home/yan/ssd/searchtrunk/tmp/release/projects/protogen/cpp /home/yan/ssd/searchtrunk/tmp/release/projects/protogen/cpp/CMakeFiles/protogen.dir/DependInfo.cmake --color= +/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles +Built target morpheus +make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release' +make -f libsrc/base/CMakeFiles/base.dir/build.make libsrc/base/CMakeFiles/base.dir/build +make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release' +make[3]: Nothing to be done for `libsrc/coroutine/CMakeFiles/coroutine.dir/build'. +Built target wrap_curl +make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release' +/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles +/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles +[ 0%] [ 0%] [ 0%] [ 0%] Building CXX object libsrc/image_crop/CMakeFiles/image_crop.dir/src/crop_context.cpp.o +cd /home/yan/ssd/searchtrunk/tmp/release/libsrc/image_crop && /opt/rh/devtoolset-7/root/usr/bin/c++ -DGOGO_USE_LIBEVENT2=1 -DGOGO_USE_OPENSSL_SNI=1 -DHAVE_BOOST -DHAVE_GLIBC -DHAVE_MALLOC_TRIM -DHAVE_PREAD -DHAVE_READAHEAD -D_FILE_OFFSET_BITS=64 -D_STAT_LEMM -pipe -Wall -Wextra -Werror=multichar -Wno-deprecated -Wno-unused-parameter -pthread -fPIC -Woverloaded-virtual -Wnon-virtual-dtor -Werror -std=gnu++11 -Wno-unknown-pragmas -DBOOST_FILESYSTEM_VERSION=3 -DBOOST_NO_CXX11_HDR_CODECVT -DMAGICKCORE_QUANTUM_DEPTH=8 -DMAGICKCORE_HDRI_ENABLE=0 -Wno-unused-local-typedefs -D_GLIBCXX_USE_CXX11_ABI=0 -std=gnu++14 -DCXXHASH128_EXIST -static-libstdc++ -Wno-implicit-fallthrough -Wno-stringop-overflow -O3 -DNDEBUG -DBOOST_UBLAS_NDEBUG -g0 -march=core2 -mssse3 -msse4.1 -I/home/yan/ssd/searchtrunk/tmp/release/__inc -I/home/yan/ssd/searchtrunk/libsrc/image_crop/src -I/home/yan/ssd/searchtrunk/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/boost_1_69_0/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/icu-49.1.2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/protobuf-3.4.0/include -I/home/yan/ssd/searchtrunk/tmp/release -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/opencv-3.0.0m2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/dlib-19.7p1/include -Wno-array-bounds -o CMakeFiles/image_crop.dir/src/crop_context.cpp.o -c /home/yan/ssd/searchtrunk/libsrc/image_crop/src/crop_context.cpp +Building CXX object libsrc/image_crop/CMakeFiles/image_crop.dir/src/detector.cpp.o +Building CXX object libsrc/image_crop/CMakeFiles/image_crop.dir/src/image_crop.cpp.o +cd /home/yan/ssd/searchtrunk/tmp/release/libsrc/image_crop && /opt/rh/devtoolset-7/root/usr/bin/c++ -DGOGO_USE_LIBEVENT2=1 -DGOGO_USE_OPENSSL_SNI=1 -DHAVE_BOOST -DHAVE_GLIBC -DHAVE_MALLOC_TRIM -DHAVE_PREAD -DHAVE_READAHEAD -D_FILE_OFFSET_BITS=64 -D_STAT_LEMM -pipe -Wall -Wextra -Werror=multichar -Wno-deprecated -Wno-unused-parameter -pthread -fPIC -Woverloaded-virtual -Wnon-virtual-dtor -Werror -std=gnu++11 -Wno-unknown-pragmas -DBOOST_FILESYSTEM_VERSION=3 -DBOOST_NO_CXX11_HDR_CODECVT -DMAGICKCORE_QUANTUM_DEPTH=8 -DMAGICKCORE_HDRI_ENABLE=0 -Wno-unused-local-typedefs -D_GLIBCXX_USE_CXX11_ABI=0 -std=gnu++14 -DCXXHASH128_EXIST -static-libstdc++ -Wno-implicit-fallthrough -Wno-stringop-overflow -O3 -DNDEBUG -DBOOST_UBLAS_NDEBUG -g0 -march=core2 -mssse3 -msse4.1 -I/home/yan/ssd/searchtrunk/tmp/release/__inc -I/home/yan/ssd/searchtrunk/libsrc/image_crop/src -I/home/yan/ssd/searchtrunk/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/boost_1_69_0/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/icu-49.1.2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/protobuf-3.4.0/include -I/home/yan/ssd/searchtrunk/tmp/release -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/opencv-3.0.0m2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/dlib-19.7p1/include -Wno-array-bounds -o CMakeFiles/image_crop.dir/src/detector.cpp.o -c /home/yan/ssd/searchtrunk/libsrc/image_crop/src/detector.cpp +Built target coroutine +cd /home/yan/ssd/searchtrunk/tmp/release/libsrc/image_crop && /opt/rh/devtoolset-7/root/usr/bin/c++ -DGOGO_USE_LIBEVENT2=1 -DGOGO_USE_OPENSSL_SNI=1 -DHAVE_BOOST -DHAVE_GLIBC -DHAVE_MALLOC_TRIM -DHAVE_PREAD -DHAVE_READAHEAD -D_FILE_OFFSET_BITS=64 -D_STAT_LEMM -pipe -Wall -Wextra -Werror=multichar -Wno-deprecated -Wno-unused-parameter -pthread -fPIC -Woverloaded-virtual -Wnon-virtual-dtor -Werror -std=gnu++11 -Wno-unknown-pragmas -DBOOST_FILESYSTEM_VERSION=3 -DBOOST_NO_CXX11_HDR_CODECVT -DMAGICKCORE_QUANTUM_DEPTH=8 -DMAGICKCORE_HDRI_ENABLE=0 -Wno-unused-local-typedefs -D_GLIBCXX_USE_CXX11_ABI=0 -std=gnu++14 -DCXXHASH128_EXIST -static-libstdc++ -Wno-implicit-fallthrough -Wno-stringop-overflow -O3 -DNDEBUG -DBOOST_UBLAS_NDEBUG -g0 -march=core2 -mssse3 -msse4.1 -I/home/yan/ssd/searchtrunk/tmp/release/__inc -I/home/yan/ssd/searchtrunk/libsrc/image_crop/src -I/home/yan/ssd/searchtrunk/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/boost_1_69_0/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/icu-49.1.2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/protobuf-3.4.0/include -I/home/yan/ssd/searchtrunk/tmp/release -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/opencv-3.0.0m2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/dlib-19.7p1/include -Wno-array-bounds -o CMakeFiles/image_crop.dir/src/image_crop.cpp.o -c /home/yan/ssd/searchtrunk/libsrc/image_crop/src/image_crop.cpp +make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release' +make[3]: Nothing to be done for `libsrc/base/CMakeFiles/base.dir/build'. +make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release' +/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles +make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release' +make -f projects/protogen/cpp/CMakeFiles/protogen.dir/build.make projects/protogen/cpp/CMakeFiles/protogen.dir/build +[ 0%] Built target base +In file included from /home/yan/ssd/searchtrunk/include/image_crop/detector.hpp:3:0, + from /home/yan/ssd/searchtrunk/libsrc/image_crop/src/detector.cpp:2: +/home/yan/ssd/searchtrunk/include/image_crop/crop_context.hpp:6:10: fatal error: opencv2/dnn.hpp: No such file or directory + #include + ^~~~~~~~~~~~~~~~~ +compilation terminated. +make[3]: *** [libsrc/image_crop/CMakeFiles/image_crop.dir/src/detector.cpp.o] Error 1 +make[3]: *** Waiting for unfinished jobs.... +In file included from /home/yan/ssd/searchtrunk/include/image_crop/image_crop.hpp:3:0, + from /home/yan/ssd/searchtrunk/libsrc/image_crop/src/image_crop.cpp:1: +/home/yan/ssd/searchtrunk/include/image_crop/crop_context.hpp:6:10: fatal error: opencv2/dnn.hpp: No such file or directory + #include + ^~~~~~~~~~~~~~~~~ +compilation terminated. +make[3]: *** [libsrc/image_crop/CMakeFiles/image_crop.dir/src/image_crop.cpp.o] Error 1 +In file included from /home/yan/ssd/searchtrunk/libsrc/image_crop/src/crop_context.cpp:1:0: +/home/yan/ssd/searchtrunk/include/image_crop/crop_context.hpp:6:10: fatal error: opencv2/dnn.hpp: No such file or directory + #include + ^~~~~~~~~~~~~~~~~ +compilation terminated. +make[3]: *** [libsrc/image_crop/CMakeFiles/image_crop.dir/src/crop_context.cpp.o] Error 1 +make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release' +make[2]: *** [libsrc/image_crop/CMakeFiles/image_crop.dir/all] Error 2 +make[2]: *** Waiting for unfinished jobs.... +make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release' +make[3]: Nothing to be done for `projects/protogen/cpp/CMakeFiles/protogen.dir/build'. +make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release' +/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles 63 64 65 66 +[ 25%] Built target protogen +make[2]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release' +make[1]: *** [all] Error 2 +make[1]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release/projects/projectX' +make: *** [install] Error 2 +yan@yPC:~/ssd/searchtrunk/projects/projectX$ diff --git a/addons/xterm-addon-search/src/SearchAddon.api.ts b/addons/xterm-addon-search/src/SearchAddon.api.ts index e3520116c..ff195f5e9 100644 --- a/addons/xterm-addon-search/src/SearchAddon.api.ts +++ b/addons/xterm-addon-search/src/SearchAddon.api.ts @@ -6,6 +6,8 @@ import * as puppeteer from 'puppeteer'; import { assert } from 'chai'; import { ITerminalOptions } from 'xterm'; +import { readFile } from 'fs'; +import { resolve } from 'path'; const APP = 'http://127.0.0.1:3000/test'; @@ -91,7 +93,7 @@ describe('Search Tests', function (): void { selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc'); }); - it ('Simple Regex', async () => { + it('Simple Regex', async () => { await writeSync('abc123defABCD'); await page.evaluate(`window.search.findNext('[a-z]+', {regex: true})`); assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc'); @@ -106,6 +108,86 @@ describe('Search Tests', function (): void { assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true); assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc'); }); + + describe('Regression tests', () => { + describe('#2444 wrapped line content not being found', () => { + let fixture: string; + before(async () => { + const rawFixture = await new Promise(r => readFile(resolve(__dirname, '../fixtures/issue-2444'), (err, data) => r(data))); + fixture = rawFixture.toString() + .replace(/\n/g, '\\n\\r') + .replace(/'/g, '\\\''); + }); + it('should find all occurrences using findNext', async () => { + await writeSync(fixture); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 }); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 }); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 }); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 }); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 }); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 }); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 }); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 }); + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 }); + // Wrap around to first result + assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 }); + }); + it('should find all occurrences using findPrevious', async () => { + await writeSync(fixture); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 }); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 }); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 }); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 }); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 }); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 }); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 }); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 }); + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 }); + // Wrap around to first result + assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); + selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); + assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 }); + }); + }); + }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 91fd39772..db2968e69 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -12,6 +12,11 @@ export interface ISearchOptions { incremental?: boolean; } +export interface ISearchPosition { + startCol: number; + startRow: number; +} + export interface ISearchResult { term: string; col: number; @@ -71,17 +76,23 @@ export class SearchAddon implements ITerminalAddon { this._initLinesCache(); + const searchPosition: ISearchPosition = { + startRow, + startCol + }; + // Search startRow - let result = this._findInLine(term, startRow, startCol, searchOptions); + let result = this._findInLine(term, searchPosition, searchOptions); // Search from startRow + 1 to end if (!result) { for (let y = startRow + 1; y < this._terminal.buffer.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, y, 0, searchOptions); + result = this._findInLine(term, searchPosition, searchOptions); if (result) { break; } @@ -90,7 +101,9 @@ export class SearchAddon implements ITerminalAddon { // 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++) { - result = this._findInLine(term, y, 0, searchOptions); + searchPosition.startRow = y; + searchPosition.startCol = 0; + result = this._findInLine(term, searchPosition, searchOptions); if (result) { break; } @@ -135,21 +148,26 @@ export class SearchAddon implements ITerminalAddon { } this._initLinesCache(); + const searchPosition: ISearchPosition = { + startRow, + startCol + }; if (incremental) { - result = this._findInLine(term, startRow, startCol, searchOptions, false); + result = this._findInLine(term, searchPosition, searchOptions, false); if (!(result && result.row === startRow && result.col === startCol)) { - result = this._findInLine(term, startRow, startCol, searchOptions, true); + result = this._findInLine(term, searchPosition, searchOptions, true); } } else { - result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch); + result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); } // Search from startRow - 1 to top if (!result) { - startCol = this._terminal.cols; + searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols); for (let y = startRow - 1; y >= 0; y--) { - result = this._findInLine(term, y, startCol, searchOptions, isReverseSearch); + searchPosition.startRow = y; + result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); if (result) { break; } @@ -158,7 +176,8 @@ export class SearchAddon implements ITerminalAddon { // If we hit the top and didn't search from the very bottom wrap back down if (!result && startRow !== (this._terminal.buffer.baseY + this._terminal.rows)) { for (let y = (this._terminal.buffer.baseY + this._terminal.rows); y > startRow; y--) { - result = this._findInLine(term, y, startCol, searchOptions, isReverseSearch); + searchPosition.startRow = y; + result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch); if (result) { break; } @@ -220,18 +239,28 @@ export class SearchAddon implements ITerminalAddon { * 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 row The line to start the search from. - * @param col The column to start the search from. + * @param position The position to start the search. * @param searchOptions Search options. * @return The search result if it was found. */ - protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined { + protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined { const terminal = this._terminal!; + let row = searchPosition.startRow; + const col = searchPosition.startCol; // Ignore wrapped lines, only consider on unwrapped line (first row of command string). const firstLine = terminal.buffer.getLine(row); if (firstLine && firstLine.isWrapped) { - return; + 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 stringLine = this._linesCache ? this._linesCache[row] : void 0; if (stringLine === void 0) { diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 3f8163ba0..1f28d5893 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -15,13 +15,14 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { RenderModel, COMBINED_CHAR_BIT_MASK } from './RenderModel'; import { Disposable } from 'common/Lifecycle'; import { DEFAULT_COLOR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX, NULL_CELL_CODE } from 'common/buffer/Constants'; -import { Terminal } from 'xterm'; +import { Terminal, IEvent } from 'xterm'; import { getLuminance } from './ColorUtils'; import { IRenderLayer } from './renderLayer/Types'; -import { IRenderDimensions, IRenderer } from 'browser/renderer/Types'; +import { IRenderDimensions, IRenderer, IRequestRefreshRowsEvent } from 'browser/renderer/Types'; import { IColorSet } from 'browser/Types'; import { FLAGS } from './Constants'; import { getCompatAttr } from './CharDataCompat'; +import { EventEmitter } from 'common/EventEmitter'; export const INDICIES_PER_CELL = 4; @@ -41,6 +42,9 @@ export class WebglRenderer extends Disposable implements IRenderer { private _core: ITerminal; + private _onRequestRefreshRows = new EventEmitter(); + public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } + constructor( private _terminal: Terminal, private _colors: IColorSet, @@ -54,7 +58,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._renderLayers = [ new LinkRenderLayer(this._core.screenElement, 2, this._colors, this._core), - new CursorRenderLayer(this._core.screenElement, 3, this._colors) + new CursorRenderLayer(this._core.screenElement, 3, this._colors, this._onRequestRefreshRows) ]; this.dimensions = { scaledCharWidth: 0, @@ -186,7 +190,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._glyphRenderer.updateSelection(this._model, columnSelectMode); // TODO: #2102 Should this move to RenderCoordinator? - this._core.refresh(0, this._terminal.rows - 1); + this._onRequestRefreshRows.fire({ start: 0, end: this._terminal.rows - 1 }); } public onCursorMove(): void { diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index 8a67526e7..413f4fdd4 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -8,7 +8,8 @@ import { BaseRenderLayer } from './BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { IColorSet } from 'browser/Types'; -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions, IRequestRefreshRowsEvent } from 'browser/renderer/Types'; +import { IEventEmitter } from 'common/EventEmitter'; interface ICursorState { x: number; @@ -29,7 +30,12 @@ export class CursorRenderLayer extends BaseRenderLayer { private _cursorBlinkStateManager: CursorBlinkStateManager | undefined; private _cell: ICellData = new CellData(); - constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { + constructor( + container: HTMLElement, + zIndex: number, + colors: IColorSet, + private _onRequestRefreshRowsEvent: IEventEmitter + ) { super(container, 'cursor', zIndex, true, colors); this._state = { x: 0, @@ -70,14 +76,14 @@ export class CursorRenderLayer extends BaseRenderLayer { if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager.pause(); } - terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY); + this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.cursorY, end: terminal.buffer.cursorY }); } public onFocus(terminal: Terminal): void { if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager.resume(terminal); } else { - terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY); + this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.cursorY, end: terminal.buffer.cursorY }); } } @@ -89,13 +95,12 @@ export class CursorRenderLayer extends BaseRenderLayer { }); } } else { - if (this._cursorBlinkStateManager) { - this._cursorBlinkStateManager.dispose(); - } + this._cursorBlinkStateManager?.dispose(); + this._cursorBlinkStateManager = undefined; } // Request a refresh from the terminal as management of rendering is being // moved back to the terminal - terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY); + this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.cursorY, end: terminal.buffer.cursorY }); } public onCursorMove(terminal: Terminal): void { @@ -115,7 +120,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _render(terminal: Terminal, triggeredByAnimationFrame: boolean): void { // Don't draw the cursor if it's hidden // TODO: Need to expose API for this - if (!(terminal as any)._core.cursorState || (terminal as any)._core.cursorHidden) { + if (!(terminal as any)._core._coreService.isCursorInitialized || (terminal as any)._core._coreService.isCursorHidden) { this._clearCursor(); return; } diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 177f581f4..4daa09618 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -7,6 +7,8 @@ jobs: - job: Linux pool: vmImage: 'ubuntu-16.04' + variables: + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1 steps: - task: NodeTool@0 inputs: @@ -16,38 +18,39 @@ jobs: inputs: versionSpec: '1.x' displayName: 'Install Yarn' - - script: yarn + - task: CacheBeta@1 + inputs: + key: yarn2 | $(Agent.OS) | yarn.lock + path: node_modules + displayName: Cache node modules + - script: yarn --frozen-lockfile displayName: 'Install dependencies and build' - - script: yarn test-unit --forbid-only + - script: yarn test-unit-coverage --forbid-only displayName: 'Unit tests' - script: yarn lint displayName: 'Lint' - - script: | - NODE_PATH=$(pwd)/out ./node_modules/.bin/nyc ./node_modules/.bin/mocha './out/*test.js' './out/**/*test.js' - ./node_modules/.bin/nyc report --reporter=cobertura - displayName: 'Coverage report' - task: PublishCodeCoverageResults@1 inputs: codeCoverageTool: Cobertura summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/*coverage.xml' displayName: 'Publish coverage' - - task: mspremier.BuildQualityChecks.QualityChecks-task.BuildQualityChecks@6 - displayName: 'Check build quality' - inputs: - checkCoverage: true - coverageType: lines - coverageThreshold: 60 - coverageFailOption: fixed - job: macOS pool: vmImage: 'xcode9-macos10.13' + variables: + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1 steps: - task: NodeTool@0 inputs: versionSpec: '8.x' displayName: 'Install Node.js' - - script: yarn + - task: CacheBeta@1 + inputs: + key: yarn2 | $(Agent.OS) | yarn.lock + path: node_modules + displayName: Cache node modules + - script: yarn --frozen-lockfile displayName: 'Install dependencies and build' - script: yarn test-unit --forbid-only displayName: 'Unit tests' @@ -57,12 +60,19 @@ jobs: - job: Windows pool: vmImage: 'vs2017-win2016' + variables: + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1 steps: - task: NodeTool@0 inputs: versionSpec: '8.x' displayName: 'Install Node.js' - - script: yarn + - task: CacheBeta@1 + inputs: + key: yarn2 | $(Agent.OS) | yarn.lock + path: node_modules + displayName: Cache node modules + - script: yarn --frozen-lockfile displayName: 'Install dependencies and build' - script: yarn test-unit --forbid-only displayName: 'Unit tests' @@ -81,7 +91,7 @@ jobs: inputs: versionSpec: '1.x' displayName: 'Install Yarn' - - script: yarn + - script: yarn --frozen-lockfile displayName: 'Install dependencies and build' - script: | yarn start & @@ -97,7 +107,7 @@ jobs: inputs: versionSpec: '8.x' displayName: 'Install Node.js' - - script: yarn + - script: yarn --frozen-lockfile displayName: 'Install dependencies and build' - script: | yarn start & @@ -124,7 +134,11 @@ jobs: inputs: versionSpec: '1.x' displayName: 'Install Yarn' - - script: yarn + - task: CacheBeta@1 + inputs: + key: yarn2 | $(Agent.OS) | yarn.lock + path: node_modules + - script: yarn --frozen-lockfile displayName: 'Install dependencies and build' - script: NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js displayName: 'Package and publish to npm' diff --git a/bin/test.js b/bin/test.js index 3b08d4a15..bd1784fbb 100644 --- a/bin/test.js +++ b/bin/test.js @@ -6,6 +6,8 @@ const cp = require('child_process'); const path = require('path'); +const COVERAGE_LINES_THRESHOLD = 60; + // Add `out` to the NODE_PATH so absolute paths can be resolved. const env = { ...process.env }; env.NODE_PATH = path.resolve(__dirname, '../out'); @@ -28,8 +30,28 @@ if (process.argv.length > 2) { } } +const checkCoverage = flagArgs.indexOf('--coverage') >= 0; + +if (checkCoverage) { + flagArgs.splice(flagArgs.indexOf('--coverage'), 1); + const executable = npmBinScript('nyc'); + const args = ['--check-coverage', `--lines=${COVERAGE_LINES_THRESHOLD}`, npmBinScript('mocha'), ...testFiles, ...flagArgs]; + console.info('executable', executable); + console.info('args', args); + const run = cp.spawnSync( + executable, + args, + { + cwd: path.resolve(__dirname, '..'), + env, + stdio: 'inherit' + } + ); + process.exit(run.status); +} + const run = cp.spawnSync( - path.resolve(__dirname, '../node_modules/.bin/mocha'), + npmBinScript('mocha'), [...testFiles, ...flagArgs], { cwd: path.resolve(__dirname, '..'), @@ -38,4 +60,8 @@ const run = cp.spawnSync( } ); +function npmBinScript(script) { + return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ? `${script}.cmd` : script)); +} + process.exit(run.status); diff --git a/package.json b/package.json index 0150569d3..7ed717419 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "posttest": "npm run lint", "test-api": "mocha \"**/*.api.js\"", "test-unit": "node ./bin/test.js", + "test-unit-coverage": "node ./bin/test.js --coverage", "build": "tsc -b ./tsconfig.all.json", "prepare": "npm run setup", "setup": "npm run build", diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 74208482f..3994d532c 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1449,7 +1449,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._logService.debug('DECSET 1015 not supported (see #2507)'); break; case 25: // show cursor - this._terminal.cursorHidden = false; + this._coreService.isCursorHidden = false; break; case 1048: // alt screen cursor this.saveCursor(); @@ -1620,7 +1620,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._logService.debug('DECRST 1015 not supported (see #2507)'); break; case 25: // hide cursor - this._terminal.cursorHidden = true; + this._coreService.isCursorHidden = true; break; case 1048: // alt screen cursor this.restoreCursor(); @@ -1954,7 +1954,7 @@ export class InputHandler extends Disposable implements IInputHandler { * http://vt100.net/docs/vt220-rm/table4-10.html */ public softReset(params: IParams): void { - this._terminal.cursorHidden = false; + this._coreService.isCursorHidden = false; this._terminal.insertMode = false; this._terminal.originMode = false; this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 667bf0764..b55a516b2 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -163,17 +163,6 @@ describe('Terminal', () => { }); }); - describe('reset', () => { - it('should not affect cursorState', () => { - term.cursorState = 1; - term.reset(); - assert.equal(term.cursorState, 1); - term.cursorState = 0; - term.reset(); - assert.equal(term.cursorState, 0); - }); - }); - describe('clear', () => { it('should clear a buffer equal to rows', () => { const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); diff --git a/src/Terminal.ts b/src/Terminal.ts index e8e680172..704df3795 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -28,7 +28,7 @@ import { Viewport } from 'browser/Viewport'; import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard'; import { C0 } from 'common/data/EscapeSequences'; import { InputHandler } from './InputHandler'; -import { Renderer } from './renderer/Renderer'; +import { Renderer } from 'browser/renderer/Renderer'; import { Linkifier } from 'browser/Linkifier'; import { SelectionService } from 'browser/services/SelectionService'; import * as Browser from 'common/Platform'; @@ -38,7 +38,7 @@ import { SoundService } from 'browser/services/SoundService'; import { MouseZoneManager } from 'browser/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider } from 'xterm'; -import { DomRenderer } from './renderer/dom/DomRenderer'; +import { DomRenderer } from 'browser/renderer/dom/DomRenderer'; import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; import { EventEmitter, IEvent } from 'common/EventEmitter'; @@ -48,7 +48,7 @@ import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services'; import { OptionsService } from 'common/services/OptionsService'; -import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService } from 'browser/services/Services'; +import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; import { Disposable } from 'common/Lifecycle'; @@ -64,6 +64,7 @@ import { InstantiationService } from 'common/services/InstantiationService'; import { CoreMouseService } from 'common/services/CoreMouseService'; import { WriteBuffer } from 'common/input/WriteBuffer'; import { Linkifier2 } from 'browser/Linkifier2'; +import { CoreBrowserService } from 'browser/services/CoreBrowserService'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -91,10 +92,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // TODO: We should remove options once components adopt optionsService public get options(): ITerminalOptions { return this.optionsService.options; } - // TODO: This can be changed to an enum or boolean, 0 and 1 seem to be the only options - public cursorState: number; - public cursorHidden: boolean; - private _customKeyEventHandler: CustomKeyEventHandler; // common services @@ -253,8 +250,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _setup(): void { this._parent = document ? document.body : null; - this.cursorState = 0; - this.cursorHidden = false; this._customKeyEventHandler = null; // modes @@ -322,10 +317,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } } - public get isFocused(): boolean { - return document.activeElement === this.textarea && document.hasFocus(); - } - private _setupOptionsListeners(): void { // TODO: These listeners should be owned by individual components this.optionsService.onOptionChange(key => { @@ -540,6 +531,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur())); this._helperContainer.appendChild(this.textarea); + const coreBrowserService = this._instantiationService.createInstance(CoreBrowserService, this.textarea); + this._instantiationService.setService(ICoreBrowserService, coreBrowserService); + this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer); this._instantiationService.setService(ICharSizeService, this._charSizeService); @@ -641,8 +635,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _createRenderer(): IRenderer { switch (this.options.rendererType) { - case 'canvas': return new Renderer(this._colorManager.colors, this, this._bufferService, this._charSizeService, this.optionsService); - case 'dom': return new DomRenderer(this, this._colorManager.colors, this._charSizeService, this.optionsService); + case 'canvas': return this._instantiationService.createInstance(Renderer, this._colorManager.colors, this.screenElement, this.linkifier, this.linkifier2); + case 'dom': return this._instantiationService.createInstance(DomRenderer, this._colorManager.colors, this.element, this.screenElement, this._viewportElement, this.linkifier, this.linkifier2); default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); } } @@ -949,8 +943,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * Display the cursor element */ public showCursor(): void { - if (!this.cursorState) { - this.cursorState = 1; + if (!this._coreService.isCursorInitialized) { + this._coreService.isCursorInitialized = true; this.refresh(this.buffer.y, this.buffer.y); } } @@ -1518,7 +1512,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.options.cols = this.cols; const customKeyEventHandler = this._customKeyEventHandler; const inputHandler = this._inputHandler; - const cursorState = this.cursorState; const userScrolling = this._userScrolling; this._setup(); @@ -1530,7 +1523,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // reattach this._customKeyEventHandler = customKeyEventHandler; this._inputHandler = inputHandler; - this.cursorState = cursorState; this._userScrolling = userScrolling; // do a full screen refresh diff --git a/src/Terminal2.test.ts b/src/Terminal2.test.ts index efb2a4462..f7046240b 100644 --- a/src/Terminal2.test.ts +++ b/src/Terminal2.test.ts @@ -34,7 +34,6 @@ if (os.platform() === 'darwin') { // filter skipFilenames const FILES = TESTFILES.filter(value => SKIP_FILES.indexOf(value.split('/').slice(-1)[0]) === -1); - describe('Escape Sequence Files', function(): void { this.timeout(1000); @@ -44,6 +43,9 @@ describe('Escape Sequence Files', function(): void { let customHandler: IDisposable | undefined; before(() => { + if (process.platform === 'win32') { + return; + } ptyTerm = (pty as any).open({cols: COLS, rows: ROWS}); slaveEnd = ptyTerm._slave; term = new Terminal({cols: COLS, rows: ROWS}); @@ -51,12 +53,15 @@ describe('Escape Sequence Files', function(): void { }); after(() => { + if (process.platform === 'win32') { + return; + } ptyTerm._master.end(); ptyTerm._master.destroy(); }); FILES.forEach(filename => { - it(filename.split('/').slice(-1)[0], async () => { + (process.platform === 'win32' ? it.skip : it)(filename.split('/').slice(-1)[0], async () => { // reset terminal and handler if (customHandler) { customHandler.dispose(); diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 26d227d0c..605d7c931 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; +import { IRenderer, IRenderDimensions, CharacterJoinerHandler, IRequestRefreshRowsEvent } from 'browser/renderer/Types'; import { IInputHandlingTerminal, ICompositionHelper, ITerminal, IBrowser, ITerminalOptions } from './Types'; import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, CoreMouseEventType } from 'common/Types'; @@ -366,6 +366,7 @@ export class MockBuffer implements IBuffer { } export class MockRenderer implements IRenderer { + onRequestRefreshRows: IEvent; onCanvasResize: IEvent<{ width: number; height: number; }>; onRender: IEvent<{ start: number; end: number; }>; dispose(): void { diff --git a/src/Types.d.ts b/src/Types.d.ts index 2a93bfdd3..2fdc3a01d 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -38,7 +38,6 @@ export interface IInputHandlingTerminal { savedCols: number; mouseEvents: CoreMouseEventType; sendFocus: boolean; - cursorHidden: boolean; buffers: IBufferSet; buffer: IBuffer; @@ -154,11 +153,8 @@ export interface IInputHandler { export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor { screenElement: HTMLElement; browser: IBrowser; - cursorHidden: boolean; - cursorState: number; buffer: IBuffer; buffers: IBufferSet; - isFocused: boolean; viewport: IViewport; bracketedPasteMode: boolean; optionsService: IOptionsService; diff --git a/src/renderer/CursorRenderLayer.ts b/src/browser/renderer/CursorRenderLayer.ts similarity index 79% rename from src/renderer/CursorRenderLayer.ts rename to src/browser/renderer/CursorRenderLayer.ts index 1f2791f16..0ca1b97a8 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/browser/renderer/CursorRenderLayer.ts @@ -3,13 +3,14 @@ * @license MIT */ -import { IRenderDimensions } from 'browser/renderer/Types'; -import { BaseRenderLayer } from '../browser/renderer/BaseRenderLayer'; -import { ITerminal } from '../Types'; +import { IRenderDimensions, IRequestRefreshRowsEvent } from 'browser/renderer/Types'; +import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { IColorSet } from 'browser/Types'; -import { IBufferService, IOptionsService } from 'common/services/Services'; +import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; +import { IEventEmitter } from 'common/EventEmitter'; +import { ICoreBrowserService } from 'browser/services/Services'; interface ICursorState { x: number; @@ -27,25 +28,27 @@ const BLINK_INTERVAL = 600; export class CursorRenderLayer extends BaseRenderLayer { private _state: ICursorState; private _cursorRenderers: {[key: string]: (x: number, y: number, cell: ICellData) => void}; - private _cursorBlinkStateManager: CursorBlinkStateManager; + private _cursorBlinkStateManager: CursorBlinkStateManager | undefined; private _cell: ICellData = new CellData(); constructor( container: HTMLElement, zIndex: number, colors: IColorSet, - private _terminal: ITerminal, rendererId: number, + private _onRequestRefreshRowsEvent: IEventEmitter, readonly bufferService: IBufferService, - readonly optionsService: IOptionsService + readonly optionsService: IOptionsService, + private readonly _coreService: ICoreService, + private readonly _coreBrowserService: ICoreBrowserService ) { super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService); this._state = { - x: null, - y: null, - isFocused: null, - style: null, - width: null + x: 0, + y: 0, + isFocused: false, + style: '', + width: 0 }; this._cursorRenderers = { 'bar': this._renderBarCursor.bind(this), @@ -59,11 +62,11 @@ export class CursorRenderLayer extends BaseRenderLayer { super.resize(dim); // Resizing the canvas discards the contents of the canvas so clear state this._state = { - x: null, - y: null, - isFocused: null, - style: null, - width: null + x: 0, + y: 0, + isFocused: false, + style: '', + width: 0 }; } @@ -71,7 +74,7 @@ export class CursorRenderLayer extends BaseRenderLayer { this._clearCursor(); if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager.dispose(); - this._cursorBlinkStateManager = null; + this._cursorBlinkStateManager = undefined; this.onOptionsChanged(); } } @@ -80,33 +83,31 @@ export class CursorRenderLayer extends BaseRenderLayer { if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager.pause(); } - this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y); + this._onRequestRefreshRowsEvent.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y }); } public onFocus(): void { if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager.resume(); } else { - this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y); + this._onRequestRefreshRowsEvent.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y }); } } public onOptionsChanged(): void { if (this._optionsService.options.cursorBlink) { if (!this._cursorBlinkStateManager) { - this._cursorBlinkStateManager = new CursorBlinkStateManager(this._terminal.isFocused, () => { + this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => { this._render(true); }); } } else { - if (this._cursorBlinkStateManager) { - this._cursorBlinkStateManager.dispose(); - this._cursorBlinkStateManager = null; - } + this._cursorBlinkStateManager?.dispose(); + this._cursorBlinkStateManager = undefined; } // Request a refresh from the terminal as management of rendering is being // moved back to the terminal - this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y); + this._onRequestRefreshRowsEvent.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y }); } public onCursorMove(): void { @@ -125,7 +126,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _render(triggeredByAnimationFrame: boolean): void { // Don't draw the cursor if it's hidden - if (!this._terminal.cursorState || this._terminal.cursorHidden) { + if (!this._coreService.isCursorInitialized || this._coreService.isCursorHidden) { this._clearCursor(); return; } @@ -139,12 +140,12 @@ export class CursorRenderLayer extends BaseRenderLayer { return; } - this._bufferService.buffer.lines.get(cursorY).loadCell(this._bufferService.buffer.x, this._cell); + this._bufferService.buffer.lines.get(cursorY)!.loadCell(this._bufferService.buffer.x, this._cell); if (this._cell.content === undefined) { return; } - if (!this._terminal.isFocused) { + if (!this._coreBrowserService.isFocused) { this._clearCursor(); this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; @@ -173,7 +174,7 @@ export class CursorRenderLayer extends BaseRenderLayer { // The cursor is already in the correct spot, don't redraw if (this._state.x === this._bufferService.buffer.x && this._state.y === viewportRelativeCursorY && - this._state.isFocused === this._terminal.isFocused && + this._state.isFocused === this._coreBrowserService.isFocused && this._state.style === this._optionsService.options.cursorStyle && this._state.width === this._cell.getWidth()) { return; @@ -196,11 +197,11 @@ export class CursorRenderLayer extends BaseRenderLayer { if (this._state) { this._clearCells(this._state.x, this._state.y, this._state.width, 1); this._state = { - x: null, - y: null, - isFocused: null, - style: null, - width: null + x: 0, + y: 0, + isFocused: false, + style: '', + width: 0 }; } } @@ -239,16 +240,16 @@ export class CursorRenderLayer extends BaseRenderLayer { class CursorBlinkStateManager { public isCursorVisible: boolean; - private _animationFrame: number; - private _blinkStartTimeout: number; - private _blinkInterval: number; + private _animationFrame: number | undefined; + private _blinkStartTimeout: number | undefined; + private _blinkInterval: number | undefined; /** * The time at which the animation frame was restarted, this is used on the * next render to restart the timers so they don't need to restart the timers * multiple times over a short period. */ - private _animationTimeRestarted: number; + private _animationTimeRestarted: number | undefined; constructor( isFocused: boolean, @@ -265,15 +266,15 @@ class CursorBlinkStateManager { public dispose(): void { if (this._blinkInterval) { window.clearInterval(this._blinkInterval); - this._blinkInterval = null; + this._blinkInterval = undefined; } if (this._blinkStartTimeout) { window.clearTimeout(this._blinkStartTimeout); - this._blinkStartTimeout = null; + this._blinkStartTimeout = undefined; } if (this._animationFrame) { window.cancelAnimationFrame(this._animationFrame); - this._animationFrame = null; + this._animationFrame = undefined; } } @@ -288,7 +289,7 @@ class CursorBlinkStateManager { if (!this._animationFrame) { this._animationFrame = window.requestAnimationFrame(() => { this._renderCallback(); - this._animationFrame = null; + this._animationFrame = undefined; }); } } @@ -308,7 +309,7 @@ class CursorBlinkStateManager { // started if (this._animationTimeRestarted) { const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); - this._animationTimeRestarted = null; + this._animationTimeRestarted = undefined; if (time > 0) { this._restartInterval(time); return; @@ -319,7 +320,7 @@ class CursorBlinkStateManager { this.isCursorVisible = false; this._animationFrame = window.requestAnimationFrame(() => { this._renderCallback(); - this._animationFrame = null; + this._animationFrame = undefined; }); // Setup the blink interval @@ -329,7 +330,7 @@ class CursorBlinkStateManager { // calc time diff // Make restart interval do a setTimeout initially? const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); - this._animationTimeRestarted = null; + this._animationTimeRestarted = undefined; this._restartInterval(time); return; } @@ -338,7 +339,7 @@ class CursorBlinkStateManager { this.isCursorVisible = !this.isCursorVisible; this._animationFrame = window.requestAnimationFrame(() => { this._renderCallback(); - this._animationFrame = null; + this._animationFrame = undefined; }); }, BLINK_INTERVAL); }, timeToStart); @@ -348,20 +349,20 @@ class CursorBlinkStateManager { this.isCursorVisible = true; if (this._blinkInterval) { window.clearInterval(this._blinkInterval); - this._blinkInterval = null; + this._blinkInterval = undefined; } if (this._blinkStartTimeout) { window.clearTimeout(this._blinkStartTimeout); - this._blinkStartTimeout = null; + this._blinkStartTimeout = undefined; } if (this._animationFrame) { window.cancelAnimationFrame(this._animationFrame); - this._animationFrame = null; + this._animationFrame = undefined; } } public resume(): void { - this._animationTimeRestarted = null; + this._animationTimeRestarted = undefined; this._restartInterval(); this.restartBlinkAnimation(); } diff --git a/src/renderer/Renderer.ts b/src/browser/renderer/Renderer.ts similarity index 64% rename from src/renderer/Renderer.ts rename to src/browser/renderer/Renderer.ts index 7ae53d955..a1161d4f2 100644 --- a/src/renderer/Renderer.ts +++ b/src/browser/renderer/Renderer.ts @@ -3,18 +3,18 @@ * @license MIT */ -import { TextRenderLayer } from '../browser/renderer/TextRenderLayer'; -import { SelectionRenderLayer } from '../browser/renderer/SelectionRenderLayer'; -import { CursorRenderLayer } from './CursorRenderLayer'; -import { IRenderLayer, IRenderer, IRenderDimensions, CharacterJoinerHandler, ICharacterJoinerRegistry } from 'browser/renderer/Types'; -import { ITerminal } from '../Types'; -import { LinkRenderLayer } from '../browser/renderer/LinkRenderLayer'; +import { TextRenderLayer } from 'browser/renderer/TextRenderLayer'; +import { SelectionRenderLayer } from 'browser/renderer/SelectionRenderLayer'; +import { CursorRenderLayer } from 'browser/renderer/CursorRenderLayer'; +import { IRenderLayer, IRenderer, IRenderDimensions, CharacterJoinerHandler, ICharacterJoinerRegistry, IRequestRefreshRowsEvent } from 'browser/renderer/Types'; +import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer'; import { CharacterJoinerRegistry } from 'browser/renderer/CharacterJoinerRegistry'; import { Disposable } from 'common/Lifecycle'; -import { IColorSet } from 'browser/Types'; -import { ICharSizeService } from 'browser/services/Services'; -import { IBufferService, IOptionsService } from 'common/services/Services'; +import { IColorSet, ILinkifier, ILinkifier2 } from 'browser/Types'; +import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services'; +import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; let nextRendererId = 1; @@ -27,36 +27,43 @@ export class Renderer extends Disposable implements IRenderer { public dimensions: IRenderDimensions; + private _onRequestRefreshRows = new EventEmitter(); + public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } + constructor( private _colors: IColorSet, - private readonly _terminal: ITerminal, - readonly bufferService: IBufferService, - private readonly _charSizeService: ICharSizeService, - readonly optionsService: IOptionsService + private readonly _screenElement: HTMLElement, + readonly linkifier: ILinkifier, + readonly linkifier2: ILinkifier2, + @IBufferService private readonly _bufferService: IBufferService, + @ICharSizeService private readonly _charSizeService: ICharSizeService, + @IOptionsService private readonly _optionsService: IOptionsService, + @ICoreService readonly coreService: ICoreService, + @ICoreBrowserService readonly coreBrowserService: ICoreBrowserService ) { super(); - const allowTransparency = this._terminal.options.allowTransparency; - this._characterJoinerRegistry = new CharacterJoinerRegistry(bufferService); + const allowTransparency = this._optionsService.options.allowTransparency; + this._characterJoinerRegistry = new CharacterJoinerRegistry(this._bufferService); this._renderLayers = [ - new TextRenderLayer(this._terminal.screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, bufferService, optionsService), - new SelectionRenderLayer(this._terminal.screenElement, 1, this._colors, this._id, bufferService, optionsService), - new LinkRenderLayer(this._terminal.screenElement, 2, this._colors, this._id, this._terminal.linkifier, this._terminal.linkifier2, bufferService, optionsService), - new CursorRenderLayer(this._terminal.screenElement, 4, this._colors, this._terminal, this._id, bufferService, optionsService) + new TextRenderLayer(this._screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, this._bufferService, _optionsService), + new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, _optionsService), + new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier, linkifier2, this._bufferService, _optionsService), + new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRefreshRows, this._bufferService, _optionsService, coreService, coreBrowserService) ]; this.dimensions = { - scaledCharWidth: null, - scaledCharHeight: null, - scaledCellWidth: null, - scaledCellHeight: null, - scaledCharLeft: null, - scaledCharTop: null, - scaledCanvasWidth: null, - scaledCanvasHeight: null, - canvasWidth: null, - canvasHeight: null, - actualCellWidth: null, - actualCellHeight: null + scaledCharWidth: 0, + scaledCharHeight: 0, + scaledCellWidth: 0, + scaledCellHeight: 0, + scaledCharLeft: 0, + scaledCharTop: 0, + scaledCanvasWidth: 0, + scaledCanvasHeight: 0, + canvasWidth: 0, + canvasHeight: 0, + actualCellWidth: 0, + actualCellHeight: 0 }; this._devicePixelRatio = window.devicePixelRatio; this._updateDimensions(); @@ -74,7 +81,7 @@ export class Renderer extends Disposable implements IRenderer { // and the terminal needs to refreshed if (this._devicePixelRatio !== window.devicePixelRatio) { this._devicePixelRatio = window.devicePixelRatio; - this.onResize(this._terminal.cols, this._terminal.rows); + this.onResize(this._bufferService.cols, this._bufferService.rows); } } @@ -96,12 +103,12 @@ export class Renderer extends Disposable implements IRenderer { this._renderLayers.forEach(l => l.resize(this.dimensions)); // Resize the screen - this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`; - this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`; + this._screenElement.style.width = `${this.dimensions.canvasWidth}px`; + this._screenElement.style.height = `${this.dimensions.canvasHeight}px`; } public onCharSizeChanged(): void { - this.onResize(this._terminal.cols, this._terminal.rows); + this.onResize(this._bufferService.cols, this._bufferService.rows); } public onBlur(): void { @@ -163,23 +170,23 @@ export class Renderer extends Disposable implements IRenderer { // will be floored because since lineHeight can never be lower then 1, there // is a guarentee that the scaled line height will always be larger than // scaled char height. - this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); + this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.options.lineHeight); // Calculate the y coordinate within a cell that text should draw from in // order to draw in the center of a cell. - this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); + this.dimensions.scaledCharTop = this._optionsService.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); // Calculate the scaled cell width, taking the letterSpacing into account. - this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); + this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.options.letterSpacing); // Calculate the x coordinate with a cell that text should draw from in // order to draw in the center of a cell. - this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing / 2); + this.dimensions.scaledCharLeft = Math.floor(this._optionsService.options.letterSpacing / 2); // Recalculate the canvas dimensions; scaled* define the actual number of // pixel in the canvas - this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight; - this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth; + this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight; + this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth; // The the size of the canvas on the page. It's very important that this // rounds to nearest integer and not ceils as browsers often set @@ -194,8 +201,8 @@ export class Renderer extends Disposable implements IRenderer { // account window.devicePixelRatio. ICharSizeService.width/height by itself // is insufficient when the page is not at 100% zoom level as it's measured // in CSS pixels, but the actual char size on the canvas can differ. - this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows; - this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols; + this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows; + this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols; } public registerCharacterJoiner(handler: CharacterJoinerHandler): number { diff --git a/src/browser/renderer/Types.d.ts b/src/browser/renderer/Types.d.ts index 7f84dfd49..b4c321a19 100644 --- a/src/browser/renderer/Types.d.ts +++ b/src/browser/renderer/Types.d.ts @@ -5,6 +5,7 @@ import { IDisposable } from 'common/Types'; import { IColorSet } from 'browser/Types'; +import { IEvent } from 'common/EventEmitter'; export type CharacterJoinerHandler = (text: string) => [number, number][]; @@ -23,6 +24,11 @@ export interface IRenderDimensions { actualCellHeight: number; } +export interface IRequestRefreshRowsEvent { + start: number; + end: number; +} + /** * Note that IRenderer implementations should emit the refresh event after * rendering rows to the screen. @@ -30,6 +36,8 @@ export interface IRenderDimensions { export interface IRenderer extends IDisposable { readonly dimensions: IRenderDimensions; + readonly onRequestRefreshRows: IEvent; + dispose(): void; setColors(colors: IColorSet): void; onDevicePixelRatioChange(): void; diff --git a/src/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts similarity index 73% rename from src/renderer/dom/DomRenderer.ts rename to src/browser/renderer/dom/DomRenderer.ts index 83507b676..a1a610d2c 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -3,14 +3,14 @@ * @license MIT */ -import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; -import { ITerminal } from '../../Types'; +import { IRenderer, IRenderDimensions, CharacterJoinerHandler, IRequestRefreshRowsEvent } from 'browser/renderer/Types'; import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { Disposable } from 'common/Lifecycle'; -import { IColorSet, ILinkifierEvent } from 'browser/Types'; +import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types'; import { ICharSizeService } from 'browser/services/Services'; -import { IOptionsService } from 'common/services/Services'; +import { IOptionsService, IBufferService } from 'common/services/Services'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; const ROW_CONTAINER_CLASS = 'xterm-rows'; @@ -30,19 +30,27 @@ export class DomRenderer extends Disposable implements IRenderer { private _rowFactory: DomRendererRowFactory; private _terminalClass: number = nextTerminalId++; - private _themeStyleElement: HTMLStyleElement; - private _dimensionsStyleElement: HTMLStyleElement; + private _themeStyleElement!: HTMLStyleElement; + private _dimensionsStyleElement!: HTMLStyleElement; private _rowContainer: HTMLElement; private _rowElements: HTMLElement[] = []; private _selectionContainer: HTMLElement; public dimensions: IRenderDimensions; + private _onRequestRefreshRows = new EventEmitter(); + public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } + constructor( - private _terminal: ITerminal, private _colors: IColorSet, - private _charSizeService: ICharSizeService, - private _optionsService: IOptionsService + private readonly _element: HTMLElement, + private readonly _screenElement: HTMLElement, + private readonly _viewportElement: HTMLElement, + private readonly _linkifier: ILinkifier, + private readonly _linkifier2: ILinkifier2, + @ICharSizeService private readonly _charSizeService: ICharSizeService, + @IOptionsService private readonly _optionsService: IOptionsService, + @IBufferService private readonly _bufferService: IBufferService ) { super(); @@ -50,63 +58,63 @@ export class DomRenderer extends Disposable implements IRenderer { this._rowContainer.classList.add(ROW_CONTAINER_CLASS); this._rowContainer.style.lineHeight = 'normal'; this._rowContainer.setAttribute('aria-hidden', 'true'); - this._refreshRowElements(this._terminal.cols, this._terminal.rows); + this._refreshRowElements(this._bufferService.cols, this._bufferService.rows); this._selectionContainer = document.createElement('div'); this._selectionContainer.classList.add(SELECTION_CLASS); this._selectionContainer.setAttribute('aria-hidden', 'true'); this.dimensions = { - scaledCharWidth: null, - scaledCharHeight: null, - scaledCellWidth: null, - scaledCellHeight: null, - scaledCharLeft: null, - scaledCharTop: null, - scaledCanvasWidth: null, - scaledCanvasHeight: null, - canvasWidth: null, - canvasHeight: null, - actualCellWidth: null, - actualCellHeight: null + scaledCharWidth: 0, + scaledCharHeight: 0, + scaledCellWidth: 0, + scaledCellHeight: 0, + scaledCharLeft: 0, + scaledCharTop: 0, + scaledCanvasWidth: 0, + scaledCanvasHeight: 0, + canvasWidth: 0, + canvasHeight: 0, + actualCellWidth: 0, + actualCellHeight: 0 }; this._updateDimensions(); this._injectCss(); this._rowFactory = new DomRendererRowFactory(document, this._optionsService); - this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); - this._terminal.screenElement.appendChild(this._rowContainer); - this._terminal.screenElement.appendChild(this._selectionContainer); + this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); + this._screenElement.appendChild(this._rowContainer); + this._screenElement.appendChild(this._selectionContainer); - this._terminal.linkifier.onLinkHover(e => this._onLinkHover(e)); - this._terminal.linkifier.onLinkLeave(e => this._onLinkLeave(e)); + this._linkifier.onLinkHover(e => this._onLinkHover(e)); + this._linkifier.onLinkLeave(e => this._onLinkLeave(e)); - this._terminal.linkifier2.onShowTooltip(e => this._onLinkHover(e)); - this._terminal.linkifier2.onHideTooltip(e => this._onLinkLeave(e)); + this._linkifier2.onShowTooltip(e => this._onLinkHover(e)); + this._linkifier2.onHideTooltip(e => this._onLinkLeave(e)); } public dispose(): void { - this._terminal.element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass); - this._terminal.screenElement.removeChild(this._rowContainer); - this._terminal.screenElement.removeChild(this._selectionContainer); - this._terminal.screenElement.removeChild(this._themeStyleElement); - this._terminal.screenElement.removeChild(this._dimensionsStyleElement); + this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass); + this._screenElement.removeChild(this._rowContainer); + this._screenElement.removeChild(this._selectionContainer); + this._screenElement.removeChild(this._themeStyleElement); + this._screenElement.removeChild(this._dimensionsStyleElement); super.dispose(); } private _updateDimensions(): void { this.dimensions.scaledCharWidth = this._charSizeService.width * window.devicePixelRatio; this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio); - this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); - this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); + this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.options.letterSpacing); + this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.options.lineHeight); this.dimensions.scaledCharLeft = 0; this.dimensions.scaledCharTop = 0; - this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._terminal.cols; - this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._terminal.rows; + this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._bufferService.cols; + this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._bufferService.rows; this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio); this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio); - this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols; - this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows; + this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols; + this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows; this._rowElements.forEach(element => { element.style.width = `${this.dimensions.canvasWidth}px`; @@ -118,7 +126,7 @@ export class DomRenderer extends Disposable implements IRenderer { if (!this._dimensionsStyleElement) { this._dimensionsStyleElement = document.createElement('style'); - this._terminal.screenElement.appendChild(this._dimensionsStyleElement); + this._screenElement.appendChild(this._dimensionsStyleElement); } const styles = @@ -131,9 +139,9 @@ export class DomRenderer extends Disposable implements IRenderer { this._dimensionsStyleElement.innerHTML = styles; - this._selectionContainer.style.height = (this._terminal)._viewportElement.style.height; - this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`; - this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`; + this._selectionContainer.style.height = this._viewportElement.style.height; + this._screenElement.style.width = `${this.dimensions.canvasWidth}px`; + this._screenElement.style.height = `${this.dimensions.canvasHeight}px`; } public setColors(colors: IColorSet): void { @@ -144,7 +152,7 @@ export class DomRenderer extends Disposable implements IRenderer { private _injectCss(): void { if (!this._themeStyleElement) { this._themeStyleElement = document.createElement('style'); - this._terminal.screenElement.appendChild(this._themeStyleElement); + this._screenElement.appendChild(this._themeStyleElement); } // Base CSS @@ -152,16 +160,16 @@ export class DomRenderer extends Disposable implements IRenderer { `${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` + ` color: ${this._colors.foreground.css};` + ` background-color: ${this._colors.background.css};` + - ` font-family: ${this._terminal.options.fontFamily};` + - ` font-size: ${this._terminal.options.fontSize}px;` + + ` font-family: ${this._optionsService.options.fontFamily};` + + ` font-size: ${this._optionsService.options.fontSize}px;` + `}`; // Text styles styles += `${this._terminalSelector} span:not(.${BOLD_CLASS}) {` + - ` font-weight: ${this._terminal.options.fontWeight};` + + ` font-weight: ${this._optionsService.options.fontWeight};` + `}` + `${this._terminalSelector} span.${BOLD_CLASS} {` + - ` font-weight: ${this._terminal.options.fontWeightBold};` + + ` font-weight: ${this._optionsService.options.fontWeightBold};` + `}` + `${this._terminalSelector} span.${ITALIC_CLASS} {` + ` font-style: italic;` + @@ -245,7 +253,7 @@ export class DomRenderer extends Disposable implements IRenderer { } // Remove excess elements while (this._rowElements.length > rows) { - this._rowContainer.removeChild(this._rowElements.pop()); + this._rowContainer.removeChild(this._rowElements.pop()!); } } @@ -278,13 +286,13 @@ export class DomRenderer extends Disposable implements IRenderer { } // Translate from buffer position to viewport position - const viewportStartRow = start[1] - this._terminal.buffer.ydisp; - const viewportEndRow = end[1] - this._terminal.buffer.ydisp; + const viewportStartRow = start[1] - this._bufferService.buffer.ydisp; + const viewportEndRow = end[1] - this._bufferService.buffer.ydisp; const viewportCappedStartRow = Math.max(viewportStartRow, 0); - const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1); + const viewportCappedEndRow = Math.min(viewportEndRow, this._bufferService.rows - 1); // No need to draw the selection - if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) { + if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) { return; } @@ -298,15 +306,15 @@ export class DomRenderer extends Disposable implements IRenderer { } else { // Draw first row const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; - const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols; + const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._bufferService.cols; documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol)); // Draw middle rows const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1; - documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount)); + documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._bufferService.cols, middleRowsCount)); // Draw final row if (viewportCappedStartRow !== viewportCappedEndRow) { // Only draw viewportEndRow if it's not the same as viewporttartRow - const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols; + const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols; documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol)); } } @@ -336,7 +344,7 @@ export class DomRenderer extends Disposable implements IRenderer { // Force a refresh this._updateDimensions(); this._injectCss(); - this._terminal.refresh(0, this._terminal.rows - 1); + this._onRequestRefreshRows.fire({ start: 0, end: this._bufferService.rows - 1 }); } public clear(): void { @@ -344,20 +352,18 @@ export class DomRenderer extends Disposable implements IRenderer { } public renderRows(start: number, end: number): void { - const terminal = this._terminal; - - const cursorAbsoluteY = terminal.buffer.ybase + terminal.buffer.y; - const cursorX = this._terminal.buffer.x; - const cursorBlink = this._terminal.options.cursorBlink; + const cursorAbsoluteY = this._bufferService.buffer.ybase + this._bufferService.buffer.y; + const cursorX = this._bufferService.buffer.x; + const cursorBlink = this._optionsService.options.cursorBlink; for (let y = start; y <= end; y++) { const rowElement = this._rowElements[y]; rowElement.innerHTML = ''; - const row = y + terminal.buffer.ydisp; - const lineData = terminal.buffer.lines.get(row); - const cursorStyle = terminal.options.cursorStyle; - rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, terminal.cols)); + const row = y + this._bufferService.buffer.ydisp; + const lineData = this._bufferService.buffer.lines.get(row); + const cursorStyle = this._optionsService.options.cursorStyle; + rowElement.appendChild(this._rowFactory.createRow(lineData!, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols)); } } diff --git a/src/browser/services/CoreBrowserService.ts b/src/browser/services/CoreBrowserService.ts new file mode 100644 index 000000000..e585f44ab --- /dev/null +++ b/src/browser/services/CoreBrowserService.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICoreBrowserService } from './Services'; + +export class CoreBrowserService implements ICoreBrowserService { + serviceBrand: any; + + constructor( + private _textarea: HTMLTextAreaElement + ) { + } + + public get isFocused(): boolean { + return document.activeElement === this._textarea && document.hasFocus(); + } +} diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 63084f545..b28c99713 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -51,6 +51,9 @@ export class RenderService extends Disposable implements IRenderService { this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged())); this.register(charSizeService.onCharSizeChange(() => this.onCharSizeChanged())); + // No need to register this as renderer is explicitly disposed in RenderService.dispose + this._renderer.onRequestRefreshRows(e => this.refreshRows(e.start, e.end)); + // dprchange should handle this case, we need this as well for browsers that don't support the // matchMedia query. this.register(addDisposableDomListener(window, 'resize', () => this.onDevicePixelRatioChange())); @@ -105,12 +108,14 @@ export class RenderService extends Disposable implements IRenderService { public dispose(): void { this._renderer.dispose(); + super.dispose(); } public setRenderer(renderer: IRenderer): void { // TODO: RenderService should be the only one to dispose the renderer this._renderer.dispose(); this._renderer = renderer; + this._renderer.onRequestRefreshRows(e => this.refreshRows(e.start, e.end)); this.refreshRows(0, this._rowCount - 1); } diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 2170a04e1..785fec34e 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -23,6 +23,13 @@ export interface ICharSizeService { measure(): void; } +export const ICoreBrowserService = createDecorator('CoreBrowserService'); +export interface ICoreBrowserService { + serviceBrand: any; + + readonly isFocused: boolean; +} + export const IMouseService = createDecorator('MouseService'); export interface IMouseService { serviceBrand: any; diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index f2721bbe9..7d9c3aa60 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -43,6 +43,9 @@ export class MockCoreMouseService implements ICoreMouseService { } export class MockCoreService implements ICoreService { + isCursorInitialized: boolean = false; + isCursorHidden: boolean = false; + isFocused: boolean = false; serviceBrand: any; decPrivateModes: IDecPrivateModes = {} as any; onData: IEvent = new EventEmitter().event; diff --git a/src/common/services/CoreService.test.ts b/src/common/services/CoreService.test.ts new file mode 100644 index 000000000..00be49538 --- /dev/null +++ b/src/common/services/CoreService.test.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICoreService } from 'common/services/Services'; +import { CoreService } from 'common/services/CoreService'; +import { MockBufferService, MockLogService, MockOptionsService } from 'common/TestUtils.test'; +import { assert } from 'chai'; + +describe('CoreService', () => { + let coreService: ICoreService; + + beforeEach(() => { + coreService = new CoreService( + () => {}, + new MockBufferService(80, 30), + new MockLogService(), + new MockOptionsService()); + }); + + describe('reset', () => { + it('should not affect isCursorInitialized', () => { + coreService.isCursorInitialized = true; + coreService.reset(); + assert.equal(coreService.isCursorInitialized, true); + coreService.isCursorInitialized = false; + coreService.reset(); + assert.equal(coreService.isCursorInitialized, false); + }); + }); +}); diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 0e0ba6091..11c3f3053 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -15,6 +15,8 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({ export class CoreService implements ICoreService { serviceBrand: any; + public isCursorInitialized: boolean = false; + public isCursorHidden: boolean = false; public decPrivateModes: IDecPrivateModes; private _onData = new EventEmitter(); diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 0872d3db7..e4ff90d0f 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -58,6 +58,12 @@ export const ICoreService = createDecorator('CoreService'); export interface ICoreService { serviceBrand: any; + /** + * Initially the cursor will not be visible until the first time the terminal + * is focused. + */ + isCursorInitialized: boolean; + isCursorHidden: boolean; readonly decPrivateModes: IDecPrivateModes; readonly onData: IEvent;