diff --git a/src/vs/editor/common/modes/textToHtmlTokenizer.ts b/src/vs/editor/common/modes/textToHtmlTokenizer.ts index 80212b742647156da43cbb38be64653dcc5cae78..4b1afa955ea4b48c08a6a443fa976a90767732c8 100644 --- a/src/vs/editor/common/modes/textToHtmlTokenizer.ts +++ b/src/vs/editor/common/modes/textToHtmlTokenizer.ts @@ -77,6 +77,10 @@ export function tokenizeLineToHTML(text: string, viewLineTokens: IViewLineTokens partContent += '​'; break; + case CharCode.Space: + partContent += ' '; + break; + default: partContent += String.fromCharCode(charCode); } diff --git a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts index 781ea1bb6a5c3dcfd71caea0c31de139cb66d234..56dda979b62501c93639b5607a64118ad94a8caa 100644 --- a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts +++ b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts @@ -109,9 +109,9 @@ suite('Editor Modes - textToHtmlTokenizer', () => { [ '
', 'Ciao', - ' ', + ' ', 'hello', - ' ', + ' ', 'world!', '
' ].join('') @@ -122,9 +122,9 @@ suite('Editor Modes - textToHtmlTokenizer', () => { [ '
', 'Ciao', - ' ', + ' ', 'hello', - ' ', + ' ', 'w', '
' ].join('') @@ -135,9 +135,9 @@ suite('Editor Modes - textToHtmlTokenizer', () => { [ '
', 'Ciao', - ' ', + ' ', 'hello', - ' ', + ' ', '
' ].join('') ); @@ -147,9 +147,9 @@ suite('Editor Modes - textToHtmlTokenizer', () => { [ '
', 'iao', - ' ', + ' ', 'hello', - ' ', + ' ', '
' ].join('') ); @@ -158,9 +158,9 @@ suite('Editor Modes - textToHtmlTokenizer', () => { tokenizeLineToHTML(text, lineTokens, colorMap, 4, 11, 4), [ '
', - ' ', + ' ', 'hello', - ' ', + ' ', '
' ].join('') ); @@ -170,7 +170,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { [ '
', 'hello', - ' ', + ' ', '
' ].join('') ); @@ -193,6 +193,88 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); }); + test('tokenizeLineToHTML handle spaces #35954', () => { + const text = ' Ciao hello world!'; + const lineTokens = new ViewLineTokens([ + new ViewLineToken( + 2, + ( + (1 << MetadataConsts.FOREGROUND_OFFSET) + ) >>> 0 + ), + new ViewLineToken( + 6, + ( + (3 << MetadataConsts.FOREGROUND_OFFSET) + | ((FontStyle.Bold | FontStyle.Italic) << MetadataConsts.FONT_STYLE_OFFSET) + ) >>> 0 + ), + new ViewLineToken( + 9, + ( + (1 << MetadataConsts.FOREGROUND_OFFSET) + ) >>> 0 + ), + new ViewLineToken( + 14, + ( + (4 << MetadataConsts.FOREGROUND_OFFSET) + ) >>> 0 + ), + new ViewLineToken( + 15, + ( + (1 << MetadataConsts.FOREGROUND_OFFSET) + ) >>> 0 + ), + new ViewLineToken( + 21, + ( + (5 << MetadataConsts.FOREGROUND_OFFSET) + | ((FontStyle.Underline) << MetadataConsts.FONT_STYLE_OFFSET) + ) >>> 0 + ) + ]); + const colorMap = [null!, '#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff']; + + assert.equal( + tokenizeLineToHTML(text, lineTokens, colorMap, 0, 21, 4), + [ + '
', + '  ', + 'Ciao', + '   ', + 'hello', + ' ', + 'world!', + '
' + ].join('') + ); + + assert.equal( + tokenizeLineToHTML(text, lineTokens, colorMap, 0, 17, 4), + [ + '
', + '  ', + 'Ciao', + '   ', + 'hello', + ' ', + 'wo', + '
' + ].join('') + ); + + assert.equal( + tokenizeLineToHTML(text, lineTokens, colorMap, 0, 3, 4), + [ + '
', + '  ', + 'C', + '
' + ].join('') + ); + }); }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index c9e3bdb74a36f2627ff0e296e5e10a131f543c3c..418b2900f179e6e35a761abe2be50fe593226147 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -35,5 +35,5 @@ export interface ITerminalInstanceService { } export interface IBrowserTerminalConfigHelper extends ITerminalConfigHelper { - panelContainer: HTMLElement; + panelContainer: HTMLElement | undefined; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 23077803ab15c6e310069f1e3eaa178726f57590..41ceb3e71b1bc3dbc76a3c6d046d86002d9a5a8b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -1168,7 +1168,7 @@ export class ScrollToPreviousCommandAction extends Action { public run(): Promise { const instance = this.terminalService.getActiveInstance(); - if (instance) { + if (instance && instance.commandTracker) { instance.commandTracker.scrollToPreviousCommand(); instance.focus(); } @@ -1189,7 +1189,7 @@ export class ScrollToNextCommandAction extends Action { public run(): Promise { const instance = this.terminalService.getActiveInstance(); - if (instance) { + if (instance && instance.commandTracker) { instance.commandTracker.scrollToNextCommand(); instance.focus(); } @@ -1210,7 +1210,7 @@ export class SelectToPreviousCommandAction extends Action { public run(): Promise { const instance = this.terminalService.getActiveInstance(); - if (instance) { + if (instance && instance.commandTracker) { instance.commandTracker.selectToPreviousCommand(); instance.focus(); } @@ -1231,7 +1231,7 @@ export class SelectToNextCommandAction extends Action { public run(): Promise { const instance = this.terminalService.getActiveInstance(); - if (instance) { + if (instance && instance.commandTracker) { instance.commandTracker.selectToNextCommand(); instance.focus(); } @@ -1252,7 +1252,7 @@ export class SelectToPreviousLineAction extends Action { public run(): Promise { const instance = this.terminalService.getActiveInstance(); - if (instance) { + if (instance && instance.commandTracker) { instance.commandTracker.selectToPreviousLine(); instance.focus(); } @@ -1273,7 +1273,7 @@ export class SelectToNextLineAction extends Action { public run(): Promise { const instance = this.terminalService.getActiveInstance(); - if (instance) { + if (instance && instance.commandTracker) { instance.commandTracker.selectToNextLine(); instance.focus(); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts index 0da7d6c690bf807b7835513bfadd08282a173278..7d7aafdb6b39bdcdfb663e49a72afbcba61a48db 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts @@ -26,11 +26,11 @@ const MAXIMUM_FONT_SIZE = 25; * specific test cases can be written. */ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { - public panelContainer: HTMLElement; + public panelContainer: HTMLElement | undefined; - private _charMeasureElement: HTMLElement; - private _lastFontMeasurement: ITerminalFont; - public config: ITerminalConfiguration; + private _charMeasureElement: HTMLElement | undefined; + private _lastFontMeasurement: ITerminalFont | undefined; + public config!: ITerminalConfiguration; private readonly _onWorkspacePermissionsChanged = new Emitter(); public get onWorkspacePermissionsChanged(): Event { return this._onWorkspacePermissionsChanged.event; } @@ -55,49 +55,55 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { } public configFontIsMonospace(): boolean { - this._createCharMeasureElementIfNecessary(); const fontSize = 15; const fontFamily = this.config.fontFamily || this._configurationService.getValue('editor').fontFamily || EDITOR_FONT_DEFAULTS.fontFamily; const i_rect = this._getBoundingRectFor('i', fontFamily, fontSize); const w_rect = this._getBoundingRectFor('w', fontFamily, fontSize); - const invalidBounds = !i_rect.width || !w_rect.width; - if (invalidBounds) { - // There is no reason to believe the font is not Monospace. + // Check for invalid bounds, there is no reason to believe the font is not monospace + if (!i_rect || !w_rect || !i_rect.width || !w_rect.width) { return true; } return i_rect.width === w_rect.width; } - private _createCharMeasureElementIfNecessary() { + private _createCharMeasureElementIfNecessary(): HTMLElement { + if (!this.panelContainer) { + throw new Error('Cannot measure element when terminal is not attached'); + } // Create charMeasureElement if it hasn't been created or if it was orphaned by its parent if (!this._charMeasureElement || !this._charMeasureElement.parentElement) { this._charMeasureElement = document.createElement('div'); this.panelContainer.appendChild(this._charMeasureElement); } + return this._charMeasureElement; } - private _getBoundingRectFor(char: string, fontFamily: string, fontSize: number): ClientRect | DOMRect { - const style = this._charMeasureElement.style; + private _getBoundingRectFor(char: string, fontFamily: string, fontSize: number): ClientRect | DOMRect | undefined { + let charMeasureElement: HTMLElement; + try { + charMeasureElement = this._createCharMeasureElementIfNecessary(); + } catch { + return undefined; + } + const style = charMeasureElement.style; style.display = 'inline-block'; style.fontFamily = fontFamily; style.fontSize = fontSize + 'px'; style.lineHeight = 'normal'; - this._charMeasureElement.innerText = char; - const rect = this._charMeasureElement.getBoundingClientRect(); + charMeasureElement.innerText = char; + const rect = charMeasureElement.getBoundingClientRect(); style.display = 'none'; return rect; } private _measureFont(fontFamily: string, fontSize: number, letterSpacing: number, lineHeight: number): ITerminalFont { - this._createCharMeasureElementIfNecessary(); - const rect = this._getBoundingRectFor('X', fontFamily, fontSize); // Bounding client rect was invalid, use last font measurement if available. - if (this._lastFontMeasurement && !rect.width && !rect.height) { + if (this._lastFontMeasurement && (!rect || !rect.width || !rect.height)) { return this._lastFontMeasurement; } @@ -106,8 +112,8 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { fontSize, letterSpacing, lineHeight, - charWidth: rect.width, - charHeight: Math.ceil(rect.height) + charWidth: rect && rect.width ? rect.width : 0, + charHeight: rect && rect.height ? Math.ceil(rect.height) : 0 }; return this._lastFontMeasurement; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 1953e9f3b31c4faca77def702ec831d7601319bf..48edebe7c193aa0062b898f50247b32c5f251820 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -172,7 +172,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private static _lastKnownGridDimensions: IGridDimensions | undefined; private static _idCounter = 1; - private _processManager: ITerminalProcessManager; + private _processManager!: ITerminalProcessManager; private _pressAnyKeyToCloseListener: IDisposable | undefined; private _id: number; @@ -185,22 +185,22 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _wrapperElement: (HTMLElement & { xterm?: XTermTerminal }) | undefined; private _xterm: XTermTerminal | undefined; private _xtermSearch: SearchAddon | undefined; - private _xtermElement: HTMLDivElement; + private _xtermElement: HTMLDivElement | undefined; private _terminalHasTextContextKey: IContextKey; private _terminalA11yTreeFocusContextKey: IContextKey; - private _cols: number; - private _rows: number; + private _cols: number = 0; + private _rows: number = 0; private _dimensionsOverride: ITerminalDimensions | undefined; private _windowsShellHelper: IWindowsShellHelper | undefined; private _xtermReadyPromise: Promise; private _titleReadyPromise: Promise; - private _titleReadyComplete: (title: string) => any; + private _titleReadyComplete: ((title: string) => any) | undefined; private _messageTitleDisposable: IDisposable | undefined; - private _widgetManager: TerminalWidgetManager; - private _linkHandler: TerminalLinkHandler; - private _commandTrackerAddon: CommandTrackerAddon; + private _widgetManager: TerminalWidgetManager | undefined; + private _linkHandler: TerminalLinkHandler | undefined; + private _commandTrackerAddon: CommandTrackerAddon | undefined; private _navigationModeAddon: INavigationMode & ITerminalAddon | undefined; public disableLayout: boolean; @@ -228,7 +228,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { public get hadFocusOnExit(): boolean { return this._hadFocusOnExit; } public get isTitleSetByProcess(): boolean { return !!this._messageTitleDisposable; } public get shellLaunchConfig(): IShellLaunchConfig { return this._shellLaunchConfig; } - public get commandTracker(): CommandTrackerAddon { return this._commandTrackerAddon; } + public get commandTracker(): CommandTrackerAddon | undefined { return this._commandTrackerAddon; } public get navigationMode(): INavigationMode | undefined { return this._navigationModeAddon; } private readonly _onExit = new Emitter(); @@ -487,7 +487,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._xterm.onData(data => this._processManager.write(data)); // TODO: How does the cwd work on detached processes? this.processReady.then(async () => { - this._linkHandler.processCwd = await this._processManager.getInitialCwd(); + if (this._linkHandler) { + this._linkHandler.processCwd = await this._processManager.getInitialCwd(); + } }); // Init winpty compat and link handler after process creation as they rely on the // underlying process OS @@ -642,8 +644,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._wrapperElement.appendChild(this._xtermElement); this._container.appendChild(this._wrapperElement); - this._widgetManager = new TerminalWidgetManager(this._wrapperElement); - this._processManager.onProcessReady(() => this._linkHandler.setWidgetManager(this._widgetManager)); + const widgetManager = new TerminalWidgetManager(this._wrapperElement); + this._widgetManager = widgetManager; + this._processManager.onProcessReady(() => { + if (this._linkHandler) { + this._linkHandler.setWidgetManager(widgetManager); + } + }); const computedStyle = window.getComputedStyle(this._container); const width = parseInt(computedStyle.getPropertyValue('width').replace('px', ''), 10); @@ -713,7 +720,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } public registerLinkMatcher(regex: RegExp, handler: (url: string) => void, matchIndex?: number, validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void): number { - return this._linkHandler.registerCustomLinkHandler(regex, handler, matchIndex, validationCallback); + return this._linkHandler!.registerCustomLinkHandler(regex, handler, matchIndex, validationCallback); } public deregisterLinkMatcher(linkMatcherId: number): void { @@ -1179,7 +1186,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private async _updateProcessCwd(): Promise { // reset cwd if it has changed, so file based url paths can be resolved const cwd = await this.getCwd(); - if (cwd) { + if (cwd && this._linkHandler) { this._linkHandler.processCwd = cwd; } return cwd; @@ -1342,11 +1349,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._windowsShellHelper = undefined; } const didTitleChange = title !== this._title; - const oldTitle = this._title; this._title = title; if (didTitleChange) { - if (!oldTitle) { + if (this._titleReadyComplete) { this._titleReadyComplete(title); + this._titleReadyComplete = undefined; } this._onTitleChanged.fire(this); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts b/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts index ccfa0c6258ca325d1dcb19dcbf1b1132ea580683..ba6ddcea32f3663a8fe1bd88b720f43f1de192d3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import { dispose, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/terminalWidgetManager'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -67,7 +67,6 @@ interface IPath { export class TerminalLinkHandler { private readonly _hoverDisposables = new DisposableStore(); - private _mouseMoveDisposable: IDisposable; private _widgetManager: TerminalWidgetManager | undefined; private _processCwd: string | undefined; private _gitDiffPreImagePattern: RegExp; @@ -184,7 +183,6 @@ export class TerminalLinkHandler { public dispose(): void { this._hoverDisposables.dispose(); - this._mouseMoveDisposable = dispose(this._mouseMoveDisposable); } private _wrapLinkHandler(handler: (uri: string) => boolean | void): XtermLinkMatcherHandler { diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 6c2b76271916f0eaccefba773c40db765405e544..eb991bcabfa32fbe97516e360e6e906fb9859b4c 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -465,7 +465,7 @@ export interface ITerminalInstance { * An object that tracks when commands are run and enables navigating and selecting between * them. */ - readonly commandTracker: ICommandTracker; + readonly commandTracker: ICommandTracker | undefined; readonly navigationMode: INavigationMode | undefined; diff --git a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts index 2c2635f7c1c646c7bb5eccb3a9cd083a3caf813c..b8ad7fd4c28233ff8995a36cb9eaf997a185c276 100644 --- a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts @@ -19,7 +19,7 @@ import { findExecutable } from 'vs/workbench/contrib/terminal/node/terminalEnvir import { URI } from 'vs/base/common/uri'; export class TerminalProcess extends Disposable implements ITerminalChildProcess { - private _exitCode: number; + private _exitCode: number | undefined; private _closeTimeout: any; private _ptyProcess: pty.IPty | undefined; private _currentTitle: string = ''; @@ -188,7 +188,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } catch (ex) { // Swallow, the pty has already been killed } - this._onProcessExit.fire(this._exitCode); + this._onProcessExit.fire(this._exitCode || 0); this.dispose(); }); }