From 63ccc69f08f27bf5888d1706b80dd1a5a12286f5 Mon Sep 17 00:00:00 2001 From: Borja Zarco Date: Sun, 10 May 2020 23:57:21 -0400 Subject: [PATCH 001/212] Fix launch configuration input variable resolution. When resolving launch configuration variables during a debug session, the configuration target was not being specified, always defaulting to reading workspace folder inputs. This made it impossible for user or workspace file launch configurations to use input variables, as the inputs list was never found. This change forwards the launch configuration source to the configurationResolverService, so that it looks for the inputs list in the right place. Forwarding the source fixed single-root workspaces, but multi-root workspaces were skipping the inputs lookup, since they pass an undefined workspace folder... In this case, the workspace folder is not relevant, as the config and inputs are defined in the workspace file, and allowing resolution to continue yields the desired behavior. --- .../browser/debugConfigurationManager.ts | 13 ++++++-- .../workbench/contrib/debug/common/debug.ts | 3 +- .../contrib/debug/common/debugger.ts | 2 +- .../browser/configurationResolverService.ts | 6 ++-- .../configurationResolverService.test.ts | 33 +++++++++++++++++++ 5 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index d0eff031334..17c3b1bda8e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -625,8 +625,17 @@ abstract class AbstractLaunch { if (!config || !config.configurations) { return undefined; } - - return config.configurations.find(config => config && config.name === name); + const configuration = config.configurations.find(config => config && config.name === name); + if (configuration) { + if (this instanceof UserLaunch) { + configuration.__configurationTarget = ConfigurationTarget.USER; + } else if (this instanceof WorkspaceLaunch) { + configuration.__configurationTarget = ConfigurationTarget.WORKSPACE; + } else { + configuration.__configurationTarget = ConfigurationTarget.WORKSPACE_FOLDER; + } + } + return configuration; } async getInitialConfigurationContent(folderUri?: uri, type?: string, token?: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 997e8f4df84..26df2485c28 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -21,7 +21,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IDisposable } from 'vs/base/common/lifecycle'; import { TaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; @@ -512,6 +512,7 @@ export interface IConfig extends IEnvConfig { linux?: IEnvConfig; // internals + __configurationTarget?: ConfigurationTarget; __sessionId?: string; __restart?: any; __autoAttach?: boolean; diff --git a/src/vs/workbench/contrib/debug/common/debugger.ts b/src/vs/workbench/contrib/debug/common/debugger.ts index ca1ee7ebfc3..8ede6963b15 100644 --- a/src/vs/workbench/contrib/debug/common/debugger.ts +++ b/src/vs/workbench/contrib/debug/common/debugger.ts @@ -107,7 +107,7 @@ export class Debugger implements IDebugger { substituteVariables(folder: IWorkspaceFolder | undefined, config: IConfig): Promise { return this.configurationManager.substituteVariables(this.type, folder, config).then(config => { - return this.configurationResolverService.resolveWithInteractionReplace(folder, config, 'launch', this.variables); + return this.configurationResolverService.resolveWithInteractionReplace(folder, config, 'launch', this.variables, config.__configurationTarget); }); } diff --git a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts index 42b3431dc41..0a35a47cd77 100644 --- a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts @@ -147,8 +147,8 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR // get all "inputs" let inputs: ConfiguredInput[] = []; - if (folder && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && section) { - let result = this.configurationService.inspect(section, { resource: folder.uri }); + if (this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && section) { + let result = this.configurationService.inspect(section, { resource: folder?.uri }); if (result && (result.userValue || result.workspaceValue || result.workspaceFolderValue)) { switch (target) { case ConfigurationTarget.USER: inputs = (result.userValue)?.inputs; break; @@ -156,7 +156,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR default: inputs = (result.workspaceFolderValue)?.inputs; } } else { - const valueResult = this.configurationService.getValue(section, { resource: folder.uri }); + const valueResult = this.configurationService.getValue(section, { resource: folder?.uri }); if (valueResult) { inputs = valueResult.inputs; } diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index 9d1f709f55c..de7980bef6e 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -443,6 +443,7 @@ suite('Configuration Resolver Service', () => { assert.equal(1, mockCommandService.callCount); }); }); + test('a single prompt input variable', () => { const configuration = { @@ -470,6 +471,7 @@ suite('Configuration Resolver Service', () => { assert.equal(0, mockCommandService.callCount); }); }); + test('a single pick input variable', () => { const configuration = { @@ -497,6 +499,7 @@ suite('Configuration Resolver Service', () => { assert.equal(0, mockCommandService.callCount); }); }); + test('a single command input variable', () => { const configuration = { @@ -524,6 +527,7 @@ suite('Configuration Resolver Service', () => { assert.equal(1, mockCommandService.callCount); }); }); + test('several input variables and command', () => { const configuration = { @@ -553,6 +557,35 @@ suite('Configuration Resolver Service', () => { assert.equal(2, mockCommandService.callCount); }); }); + + test('input variable with undefined workspace folder', () => { + + const configuration = { + 'name': 'Attach to Process', + 'type': 'node', + 'request': 'attach', + 'processId': '${input:input1}', + 'port': 5858, + 'sourceMaps': false, + 'outDir': null + }; + + return configurationResolverService!.resolveWithInteractionReplace(undefined, configuration, 'tasks').then(result => { + + assert.deepEqual(result, { + 'name': 'Attach to Process', + 'type': 'node', + 'request': 'attach', + 'processId': 'resolvedEnterinput1', + 'port': 5858, + 'sourceMaps': false, + 'outDir': null + }); + + assert.equal(0, mockCommandService.callCount); + }); + }); + test('contributed variable', () => { const buildTask = 'npm: compile'; const variable = 'defaultBuildTask'; From 352f231bec54ae53cf56262f80d45d25fc7e3e29 Mon Sep 17 00:00:00 2001 From: Borja Zarco Date: Mon, 11 May 2020 08:03:24 -0400 Subject: [PATCH 002/212] Do not define resource override unless folder is defined. --- .../browser/configurationResolverService.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts index 0a35a47cd77..5b9afb5b030 100644 --- a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts @@ -11,7 +11,7 @@ import { Schemas } from 'vs/base/common/network'; import { toResource } from 'vs/workbench/common/editor'; import { IStringDictionary, forEach, fromMap } from 'vs/base/common/collections'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService, IConfigurationOverrides, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceFolder, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -148,7 +148,8 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR // get all "inputs" let inputs: ConfiguredInput[] = []; if (this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && section) { - let result = this.configurationService.inspect(section, { resource: folder?.uri }); + const overrides: IConfigurationOverrides = folder ? { resource: folder.uri } : {}; + let result = this.configurationService.inspect(section, overrides); if (result && (result.userValue || result.workspaceValue || result.workspaceFolderValue)) { switch (target) { case ConfigurationTarget.USER: inputs = (result.userValue)?.inputs; break; @@ -156,7 +157,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR default: inputs = (result.workspaceFolderValue)?.inputs; } } else { - const valueResult = this.configurationService.getValue(section, { resource: folder?.uri }); + const valueResult = this.configurationService.getValue(section, overrides); if (valueResult) { inputs = valueResult.inputs; } From c54735c3420728f36d5959711cd48adf1826ebdc Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 15 Oct 2020 15:59:46 -0700 Subject: [PATCH 003/212] wip --- .../terminal/browser/terminalConfigHelper.ts | 4 + .../terminal/browser/terminalInstance.ts | 3 + .../browser/terminalTypeAheadAddon.ts | 396 ++++++++++++++++++ .../terminal/browser/xterm-private.d.ts | 2 + .../contrib/terminal/common/terminal.ts | 2 + .../terminal/common/terminalConfiguration.ts | 30 ++ 6 files changed, 437 insertions(+) create mode 100644 src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts index 35b90cbeaa0..6dc11347198 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts @@ -40,6 +40,9 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { private readonly _onWorkspacePermissionsChanged = new Emitter(); public get onWorkspacePermissionsChanged(): Event { return this._onWorkspacePermissionsChanged.event; } + private readonly _onConfigChanged = new Emitter(); + public get onConfigChanged(): Event { return this._onConfigChanged.event; } + public constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @IExtensionManagementService private readonly _extensionManagementService: IExtensionManagementService, @@ -71,6 +74,7 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { configValues.fontWeightBold = this._normalizeFontWeight(configValues.fontWeightBold, DEFAULT_BOLD_FONT_WEIGHT); this.config = configValues; + this._onConfigChanged.fire(); } public configFontIsMonospace(): boolean { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index c781273cd58..707572a0506 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -45,6 +45,7 @@ import { EnvironmentVariableInfoWidget } from 'vs/workbench/contrib/terminal/bro import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { TerminalLaunchHelpAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { LatencyTelemetryAddon } from 'vs/workbench/contrib/terminal/browser/terminalLatencyTelemetryAddon'; +import { TypeAheadAddon } from 'vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon'; // How long in milliseconds should an average frame take to render for a notification to appear // which suggests the fallback DOM-based renderer @@ -452,6 +453,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const latencyAddon = this._register(this._instantiationService.createInstance(LatencyTelemetryAddon, this._processManager)); this._xterm.loadAddon(latencyAddon); + this._xterm.loadAddon(new TypeAheadAddon(this._processManager, this._configHelper)); + return xterm; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts new file mode 100644 index 00000000000..148c618cdb3 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts @@ -0,0 +1,396 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { Terminal, ITerminalAddon, IDisposable, IBuffer, IBufferCell } from 'xterm'; +import { ITerminalProcessManager, IBeforeProcessDataEvent } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; +import { Color } from 'vs/base/common/color'; + +const CSI = '\x1b['; +const SHOW_CURSOR = `${CSI}?25h`; +const HIDE_CURSOR = `${CSI}?25l`; + +const setCursorPos = (x: number, y: number) => `${CSI}${y + 1};${x + 1}H`; +const setCursorCoordinate = (buffer: IBuffer, c: ICoordinate) => setCursorPos(c.x, c.y + (c.baseY - buffer.baseY)); + +const enum ReapplyBehavior { + /** Applying the prediction to the line is a no-op */ + NoOp, + /** The prediction can be applied to the given line */ + Apply, + /** The prediction would overwrite other, unexpected data on the line */ + Mismatch, +} + +interface ICoordinate { + x: number; + y: number; + baseY: number; +} + +const getCellAtCoordinate = (b: IBuffer, c: ICoordinate) => b.getLine(c.y + c.baseY)?.getCell(c.x); + +interface IPrediction { + /** + * Returns a sequence to apply the prediction. + * @param buffer to write to + * @param cursor position to write the data. Should advance the cursor. + */ + apply(buffer: IBuffer, cursor: ICoordinate): string; + + /** + * Returns a sequence to roll back a previous `apply()` call. + */ + rollback(buffer: IBuffer): string; + + /** + * Returns whether the given input is one expected by this prediction. + */ + matches(input: string): boolean; + + /** + * Returns the reapply behavior for the given line. + */ + getReapplyBehavior(buffer: IBuffer, allPredictions: ReadonlyArray): ReapplyBehavior; +} + +/** + * Boundary added to the prediction timeline to indicate that predictions + * should be held and not applied until all previous predictions are validated. + */ +interface IPredictionBoundary { + /** + * Checks whether a change was made that satisfies the expectation for the + * prediction boundary. For instance is pressing enter, it checks that that + * results in a new line. + */ + test(input: string): boolean; +} + +const csiRe = /\x1b\[*?[a-zA-Z]/g; + +/** + * Boundary which never tests true. Will always discard predictions. + */ +class HardBoundary implements IPredictionBoundary { + public test() { + return false; + } +} + +class CharacterPrediction implements IPrediction { + private appliedAt?: { + x: number; + y: number; + baseY: number; + oldAttributes: string; + oldChar: string; + }; + + constructor(private readonly style: string, private readonly char: string) { } + + public apply(buffer: IBuffer, cursor: ICoordinate) { + const cell = getCellAtCoordinate(buffer, cursor); + this.appliedAt = cell + ? { ...cursor, oldAttributes: getBufferCellAttributes(cell), oldChar: cell.getChars() } + : { ...cursor, oldAttributes: '', oldChar: '' }; + + cursor.x++; + return this.style + this.char + `${CSI}0m`; + } + + public rollback(buffer: IBuffer) { + if (!this.appliedAt) { + return ''; // not applied + } + + const a = this.appliedAt; + this.appliedAt = undefined; + return setCursorCoordinate(buffer, a) + (a.oldChar ? `${a.oldAttributes}${a.oldChar}${setCursorCoordinate(buffer, a)}` : `${CSI}X`); + } + + public matches(input: string) { + csiRe.lastIndex = 0; + return input.replace(csiRe, '') === this.char; + } + + public getReapplyBehavior(buffer: IBuffer) { + if (!this.appliedAt) { + return ReapplyBehavior.Apply; + } + + const cellText = getCellAtCoordinate(buffer, this.appliedAt)?.getChars() ?? ''; + if (cellText === this.char) { + return ReapplyBehavior.NoOp; + } else if (cellText === this.appliedAt.oldChar) { + return ReapplyBehavior.Apply; + } else { + return ReapplyBehavior.Mismatch; + } + } +} + +interface IPredictionGroup { + boundary?: IPredictionBoundary; + predictions: IPrediction[]; +} + +const invalidatedRestoreInterval = 200; + +class PredictionTimeline { + /** + * Expected queue of events, separated whenever a PredictionBoundary is + * emitted. Prediction groups _always_ apply to the active row. + */ + private expected: IPredictionGroup[] = []; + + /** + * Last-invalidated set of predictions. These are restored if the predictions + * become valid again in a short period of time. Some terminal programs + * rewrite lines or the entire display on backspace, for example, which will + * invalidate the predictions even if they become valid again a moment later. + */ + private invalidated: { at: number; p: IPredictionGroup[] } | undefined; + + /** + * Cursor position -- kept outside the buffer since it can be ahead if + * typing swiftly. + */ + private cursor: ICoordinate | undefined; + + constructor(public readonly terminal: Terminal) { } + + /** + * Should be called when input is incoming to the temrinal. + */ + public beforeServerInput(input: string): string { + if (!this.expected.length) { + this.cursor = undefined; + return input; + } + + const buffer = this.getActiveBuffer(); + if (!buffer) { + this.cursor = undefined; + return input; + } + + let output = ''; + let brokeBoundary = false; + for (let i = 0; i < input.length; i++) { + const test = this.expected[0]?.predictions[0]; + const char = input[i]; + + // if we reached the end of our tests, try to break through the boundary + // and start applying its items. + if (!test) { + this.expected.shift(); + if (!this.expected.length) { + return output + input.slice(i); + } + brokeBoundary = true; + if (this.expected[0].boundary?.test(char) === false) { + this.expected = []; + return output + input.slice(i); // boundary assumption invalid, throw out predictions + } + } + // if the input character matches what the next prediction expected, undo + // the prediction and write the real character out. + else if (test.matches(char)) { + output += test.rollback(buffer) + char; + this.expected[0].predictions.shift(); + } + // otherwise, roll back all pending predictions and move the current stack + // of predictions into the "invalidated" key (for possible resurrection). + else { + this.invalidated = { at: Date.now(), p: this.expected }; + this.expected = []; + this.cursor = undefined; + if (!brokeBoundary) { // on a new boundary, we did not apply predictions yet + output += this.invalidated.p[0].predictions.map(p => p.rollback(buffer)).reverse().join(''); + } + + output += input.slice(i); + break; + } + } + + if (this.cursor) { + output += setCursorCoordinate(buffer, this.cursor); + } + + // prevent cursor flickering while typing, since output will *always* + // contains cursor moves if we did anything with predictions: + output = HIDE_CURSOR + output + SHOW_CURSOR; + + return output; + } + + /** + * Should be called after data is applied to the terminal. + */ + public afterServerInput() { + const buffer = this.getActiveBuffer(); + if (buffer && this.invalidated && Date.now() - this.invalidated.at < invalidatedRestoreInterval) { + this.tryReapplyInvalidated(buffer, this.invalidated.p); + } + } + + /** + * Appends a typeahead prediction. + */ + public addPrediction(buffer: IBuffer, prediction: IPrediction) { + const l = this.expected.length - 1; + if (l === -1) { + this.expected = [{ predictions: [prediction] }]; + } else { + this.expected[l].predictions.push(prediction); + } + + if (l <= 1) { + const text = prediction.apply(buffer, this.getCursor(buffer)); + console.log('prediction:', text); + this.terminal.write(text); + } + } + + /** + * Appends a boundary to the preduction. + */ + public addBoundary(boundary: IPredictionBoundary) { + this.expected.push({ boundary, predictions: [] }); + } + + private getCursor(buffer: IBuffer) { + if (!this.cursor) { + this.cursor = { baseY: buffer.baseY, y: buffer.cursorY, x: buffer.cursorX }; + } + + return this.cursor; + } + + private getActiveBuffer() { + const buffer = this.terminal.buffer.active; + return buffer.type === 'normal' ? buffer : undefined; + } + + private tryReapplyInvalidated(buffer: IBuffer, invalidated: IPredictionGroup[]) { + if (!invalidated.length) { + return; + } + + const predictions = invalidated[0].predictions; + let lastNoop = -1; + let hasApply = false; + for (let i = 0; i < predictions.length; i++) { + switch (predictions[i].getReapplyBehavior(buffer, predictions)) { + case ReapplyBehavior.NoOp: + if (!hasApply) { lastNoop = i; } + break; + case ReapplyBehavior.Mismatch: + return; // do not reapply any prediction in this set if there's a mismatch + case ReapplyBehavior.Apply: + hasApply = true; + break; + } + } + + if (!hasApply) { + return; + } + + for (let i = lastNoop + 1; i < predictions.length; i++) { + this.terminal.write(predictions[i].apply(buffer, this.getCursor(buffer))); + } + + this.expected = invalidated.slice(1); + + if (lastNoop < predictions.length - 1) { + this.expected.unshift({ predictions: predictions.slice(lastNoop) }); + } + } +} +/** + * Gets the escape sequence to restore state/appearence in the cell. + */ +const getBufferCellAttributes = (cell: IBufferCell) => cell.isAttributeDefault() + ? `${CSI}0m` + : [ + cell.isBold() && `${CSI}1m`, + cell.isDim() && `${CSI}2m`, + cell.isItalic() && `${CSI}3m`, + cell.isUnderline() && `${CSI}4m`, + cell.isBlink() && `${CSI}5m`, + cell.isInverse() && `${CSI}7m`, + cell.isInvisible() && `${CSI}8m`, + + cell.isFgRGB() && `${CSI}38;2;${cell.getFgColor() >>> 24};${(cell.getFgColor() >>> 16) & 0xFF};${cell.getFgColor() & 0xFF}m`, + cell.isFgPalette() && `${CSI}38;5;${cell.getFgColor()}m`, + cell.isFgDefault() && `${CSI}39m`, + + cell.isBgRGB() && `${CSI}48;2;${cell.getBgColor() >>> 24};${(cell.getBgColor() >>> 16) & 0xFF};${cell.getBgColor() & 0xFF}m`, + cell.isBgPalette() && `${CSI}48;5;${cell.getBgColor()}m`, + cell.isBgDefault() && `${CSI}49m`, + ].filter(seq => !!seq).join(''); + +const parseTypeheadStyle = (style: string | number) => { + if (typeof style === 'number') { + return `${CSI}${style}m`; + } + + const { r, g, b } = Color.fromHex(style).rgba; + return `${CSI}32;${r};${g};${b}m`; +}; + +export class TypeAheadAddon implements ITerminalAddon { + private disposables: IDisposable[] = []; + private typeheadStyle = parseTypeheadStyle(this.config.config.typeaheadStyle); + private timeline?: PredictionTimeline; + + constructor(private readonly _processManager: ITerminalProcessManager, private readonly config: TerminalConfigHelper) { + } + + public activate(terminal: Terminal): void { + this.timeline = new PredictionTimeline(terminal); + this.disposables.push(terminal.onData(e => this.onUserData(e))); + this.disposables.push(this.config.onConfigChanged(() => this.typeheadStyle = parseTypeheadStyle(this.config.config.typeaheadStyle))); + this.disposables.push(this._processManager.onBeforeProcessData(e => this.onBeforeProcessData(e))); + } + + public dispose(): void { + // this.disposables.forEach(d => d.dispose()); + } + + private onUserData(data: string): void { + if (this.timeline?.terminal.buffer.active.type !== 'normal') { + return; + } + + console.log('user data:', data); + if (data.length !== 1) { + this.timeline.addBoundary(new HardBoundary()); + return; + } + + const terminal = this.timeline.terminal; + const code = data.charCodeAt(0); + if (code >= 32 && code < 126) { + if (terminal.buffer.active.cursorX === terminal.cols - 1) { + } else { + this.timeline.addPrediction(terminal.buffer.active, new CharacterPrediction(this.typeheadStyle, data)); + } + } + } + + private onBeforeProcessData(event: IBeforeProcessDataEvent): void { + if (!this.timeline) { + return; + } + + console.log('incoming data:', event.data); + event.data = this.timeline.beforeServerInput(event.data); + console.log('emitted data:', event.data); + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts b/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts index c559ff7b8db..a46aa4477ff 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts @@ -12,6 +12,8 @@ export interface XTermCore { height: number; }; + writeSync(input: Buffer | string): void; + _coreService: { triggerDataEvent(data: string, wasUserInput?: boolean): void; }; diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 1af8a10e218..c8e63273704 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -135,6 +135,8 @@ export interface ITerminalConfiguration { enableFileLinks: boolean; unicodeVersion: '6' | '11'; experimentalLinkProvider: boolean; + typeaheadThreshold: number; + typeaheadStyle: number | string; } export interface ITerminalConfigHelper { diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 985fde8246b..2cce39b1a95 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -351,6 +351,36 @@ export const terminalConfiguration: IConfigurationNode = { description: localize('terminal.integrated.experimentalLinkProvider', "An experimental setting that aims to improve link detection in the terminal by improving when links are detected and by enabling shared link detection with the editor. Currently this only supports web links."), type: 'boolean', default: true + }, + 'terminal.integrated.typeaheadThreshold': { + description: localize('terminal.integrated.typeaheadThreshold', "Experimental: length of time, in milliseconds, where typeahead will active. If '0', typeahead will always be on, and if '-1' it will be disabled."), + type: 'integer', + minimum: -1, + default: -1, + }, + 'terminal.integrated.typeaheadStyle': { + description: localize('terminal.integrated.typeaheadStyle', "Experimental: terminal style of typeahead text, either a font style or an RGB color."), + default: 2, + oneOf: [ + { + type: 'integer', + default: 2, + enum: [0, 1, 2, 3, 4, 7], + enumDescriptions: [ + localize('terminal.integrated.typeaheadStyle.0', 'Normal'), + localize('terminal.integrated.typeaheadStyle.1', 'Bold'), + localize('terminal.integrated.typeaheadStyle.2', 'Dim'), + localize('terminal.integrated.typeaheadStyle.3', 'Italic'), + localize('terminal.integrated.typeaheadStyle.4', 'Underlined'), + localize('terminal.integrated.typeaheadStyle.7', 'Inverted'), + ] + }, + { + type: 'string', + format: 'color-hex', + default: '#ff0000', + } + ] } } }; From 2f15e9d441094b2fac6536752c07898706155b69 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 16 Oct 2020 16:07:38 +0200 Subject: [PATCH 004/212] fix sash styling for monaco editor --- src/vs/base/browser/ui/sash/sash.css | 32 +++++++++++++++++++ src/vs/workbench/contrib/sash/browser/sash.ts | 10 +----- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/vs/base/browser/ui/sash/sash.css b/src/vs/base/browser/ui/sash/sash.css index e8ccd5521d2..8eb8baf9f8b 100644 --- a/src/vs/base/browser/ui/sash/sash.css +++ b/src/vs/base/browser/ui/sash/sash.css @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +:root { + --sash-size: 4px; +} + .monaco-sash { position: absolute; z-index: 35; @@ -42,6 +46,34 @@ pointer-events: none !important; } +.monaco-sash.vertical { + cursor: ew-resize; + top: 0; + width: var(--sash-size); + height: 100%; +} + +.monaco-sash.horizontal { + cursor: ns-resize; + left: 0; + width: 100%; + height: var(--sash-size); +} + +.monaco-sash:not(.disabled).orthogonal-start::before, .monaco-sash:not(.disabled).orthogonal-end::after { + content: ' '; + height: calc(var(--sash-size) * 2); + width: calc(var(--sash-size) * 2); + z-index: 100; + display: block; + cursor: all-scroll; position: absolute; +} + +.monaco-sash.orthogonal-start.vertical::before { left: -calc(var(--sash-size) / 2); top: -var(--sash-size); } +.monaco-sash.orthogonal-end.vertical::after { left: -calc(var(--sash-size) / 2); bottom: -var(--sash-size); } +.monaco-sash.orthogonal-start.horizontal::before { top: -calc(var(--sash-size) / 2); left: -var(--sash-size); } +.monaco-sash.orthogonal-end.horizontal::after { top: -calc(var(--sash-size) / 2); right: -var(--sash-size); } + /** Debug **/ .monaco-sash.debug { diff --git a/src/vs/workbench/contrib/sash/browser/sash.ts b/src/vs/workbench/contrib/sash/browser/sash.ts index 1c76305bfcf..db5215c2fe1 100644 --- a/src/vs/workbench/contrib/sash/browser/sash.ts +++ b/src/vs/workbench/contrib/sash/browser/sash.ts @@ -34,15 +34,7 @@ export class SashSizeController extends Disposable implements IWorkbenchContribu private onDidChangeSizeConfiguration(): void { const size = clamp(this.configurationService.getValue(this.configurationName) ?? minSize, minSize, maxSize); - // Update styles - this.stylesheet.textContent = ` - .monaco-sash.vertical { cursor: ew-resize; top: 0; width: ${size}px; height: 100%; } - .monaco-sash.horizontal { cursor: ns-resize; left: 0; width: 100%; height: ${size}px; } - .monaco-sash:not(.disabled).orthogonal-start::before, .monaco-sash:not(.disabled).orthogonal-end::after { content: ' '; height: ${size * 2}px; width: ${size * 2}px; z-index: 100; display: block; cursor: all-scroll; position: absolute; } - .monaco-sash.orthogonal-start.vertical::before { left: -${size / 2}px; top: -${size}px; } - .monaco-sash.orthogonal-end.vertical::after { left: -${size / 2}px; bottom: -${size}px; } - .monaco-sash.orthogonal-start.horizontal::before { top: -${size / 2}px; left: -${size}px; } - .monaco-sash.orthogonal-end.horizontal::after { top: -${size / 2}px; right: -${size}px; }`; + document.documentElement.style.setProperty('--sash-size', size + 'px'); // Update behavor setGlobalSashSize(size); From 43e2e7d2948ed382019328b7b287bf6f78345723 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 16 Oct 2020 17:20:27 +0200 Subject: [PATCH 005/212] wip - resizable suggest widget --- src/vs/editor/contrib/suggest/resizable.ts | 83 ++++++++++ .../editor/contrib/suggest/suggestWidget.ts | 155 +++++++++++------- 2 files changed, 175 insertions(+), 63 deletions(-) create mode 100644 src/vs/editor/contrib/suggest/resizable.ts diff --git a/src/vs/editor/contrib/suggest/resizable.ts b/src/vs/editor/contrib/suggest/resizable.ts new file mode 100644 index 00000000000..7d9aa354a09 --- /dev/null +++ b/src/vs/editor/contrib/suggest/resizable.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event, Emitter } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Dimension } from 'vs/base/browser/dom'; +import { Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; + + +export class ResizableHTMLElement { + + readonly domNode: HTMLElement; + + private readonly _onDidResize = new Emitter(); + readonly onDidResize: Event = this._onDidResize.event; + + private readonly _eastSash: Sash; + private readonly _southSash: Sash; + private readonly _sashListener = new DisposableStore(); + + private _size?: Dimension; + + constructor() { + this.domNode = document.createElement('div'); + this._eastSash = new Sash(this.domNode, { getVerticalSashLeft: () => this._size?.width ?? 0 }, { orientation: Orientation.VERTICAL }); + this._southSash = new Sash(this.domNode, { getHorizontalSashTop: () => this._size?.height ?? 0 }, { orientation: Orientation.HORIZONTAL }); + + this._eastSash.orthogonalEndSash = this._southSash; + this._southSash.orthogonalEndSash = this._eastSash; + + let currentSize: Dimension | undefined; + let deltaY = 0; + let deltaX = 0; + + this._sashListener.add(Event.any(this._eastSash.onDidEnd, this._southSash.onDidEnd)(() => { + currentSize = undefined; + deltaY = 0; + deltaX = 0; + })); + this._sashListener.add(Event.any(this._eastSash.onDidStart, this._southSash.onDidStart)(() => { + currentSize = this._size; + deltaY = 0; + deltaX = 0; + })); + this._sashListener.add(this._southSash.onDidChange(e => { + if (currentSize) { + deltaY = e.currentY - e.startY; + this._resize(currentSize.height + deltaY, currentSize.width + deltaX); + } + })); + this._sashListener.add(this._eastSash.onDidChange(e => { + if (currentSize) { + deltaX = e.currentX - e.startX; + this._resize(currentSize.height + deltaY, currentSize.width + deltaX); + } + })); + } + + dispose(): void { + this._southSash.dispose(); + this._eastSash.dispose(); + this._sashListener.dispose(); + this.domNode.remove(); + } + + private _resize(height: number, width: number): void { + this.layout(height, width); + this._onDidResize.fire(this._size!); + } + + layout(height: number, width: number): void { + const newSize = new Dimension(width, height); + if (!Dimension.equals(newSize, this._size)) { + this.domNode.style.height = height + 'px'; + this.domNode.style.width = width + 'px'; + this._size = newSize; + this._southSash.layout(); + this._eastSash.layout(); + } + } +} diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 5d42225f42a..b43a0e9e2f9 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -34,10 +34,7 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { SuggestionDetails, canExpandCompletionItem } from './suggestWidgetDetails'; import { SuggestWidgetStatus } from 'vs/editor/contrib/suggest/suggestWidgetStatus'; import { getAriaId, ItemRenderer } from './suggestWidgetRenderer'; - -const expandSuggestionDocsByDefault = false; - - +import { ResizableHTMLElement } from './resizable'; /** * Suggest widget colors @@ -49,8 +46,6 @@ export const editorSuggestWidgetSelectedBackground = registerColor('editorSugges export const editorSuggestWidgetHighlightForeground = registerColor('editorSuggestWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hc: listHighlightForeground }, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.')); - - const enum State { Hidden, Loading, @@ -87,14 +82,14 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate; private status: SuggestWidgetStatus; private details: SuggestionDetails; - private listHeight?: number; + // private listHeight?: number; private readonly ctxSuggestWidgetVisible: IContextKey; private readonly ctxSuggestWidgetDetailsVisible: IContextKey; @@ -141,21 +136,25 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate { - if (e.target === this.element) { + this.element = new ResizableHTMLElement(); + this.element.domNode.classList.add('editor-widget', 'suggest-widget'); + this._disposables.add(this.element.onDidResize(e => { + this.layout(e.height, e.width); + })); + this._disposables.add(addDisposableListener(this.element.domNode, 'click', e => { + if (e.target === this.element.domNode) { this.hideWidget(); } })); - this.messageElement = append(this.element, $('.message')); - this.mainElement = append(this.element, $('.tree')); + this.messageElement = append(this.element.domNode, $('.message')); + this.mainElement = append(this.element.domNode, $('.tree')); - this.details = instantiationService.createInstance(SuggestionDetails, this.element, this.editor, markdownRenderer, kbToggleDetails); + this.details = instantiationService.createInstance(SuggestionDetails, this.element.domNode, this.editor, markdownRenderer, kbToggleDetails); this.details.onDidClose(this.toggleDetails, this, this._disposables); hide(this.details.element); - const applyIconStyle = () => this.element.classList.toggle('no-icons', !this.editor.getOption(EditorOption.suggest).showIcons); + const applyIconStyle = () => this.element.domNode.classList.toggle('no-icons', !this.editor.getOption(EditorOption.suggest).showIcons); applyIconStyle(); this.listContainer = append(this.mainElement, $('.list-container')); @@ -189,7 +188,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate this.element.classList.toggle('with-status-bar', this.editor.getOption(EditorOption.suggest).statusBar.visible); + const applyStatusBarStyle = () => this.element.domNode.classList.toggle('with-status-bar', this.editor.getOption(EditorOption.suggest).statusBar.visible); applyStatusBarStyle(); this._disposables.add(attachListStyler(this.list, themeService, { @@ -223,6 +222,17 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate this.onEditorMouseDown(e))); } + dispose(): void { + this.details.dispose(); + this.list.dispose(); + this.status.dispose(); + this._disposables.dispose(); + this.loadingTimeout.dispose(); + this.showTimeout.dispose(); + this.editor.removeContentWidget(this); + this.element.dispose(); + } + private onEditorMouseDown(mouseEvent: IEditorMouseEvent): void { // Clicking inside details if (this.details.element.contains(mouseEvent.target.element)) { @@ -230,7 +240,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate { - this.element.classList.add('visible'); + this.element.domNode.classList.add('visible'); this.onDidShowEmitter.fire(this); }, 100); } @@ -691,7 +701,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate widgetY; - const rowMode = this.element.classList.contains('docs-side'); + const rowMode = this.element.domNode.classList.contains('docs-side'); // row mode: reverse doc/list when being too far right // column mode: reverse doc/list when being too far down - this.element.classList.toggle( + this.element.domNode.classList.toggle( 'reverse', (rowMode && widgetX < cursorX - this.listWidth) || (!rowMode && aboveCursor) ); @@ -796,17 +835,17 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate { From 2f28a7f8b86244f9d3df00e6a79d763753b0222c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 16 Oct 2020 16:10:28 -0700 Subject: [PATCH 006/212] get it in a better state, support for more codes --- .../browser/terminalTypeAheadAddon.ts | 389 ++++++++++-------- 1 file changed, 228 insertions(+), 161 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts index 148c618cdb3..ed234e30dee 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts @@ -3,27 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { Terminal, ITerminalAddon, IDisposable, IBuffer, IBufferCell } from 'xterm'; -import { ITerminalProcessManager, IBeforeProcessDataEvent } from 'vs/workbench/contrib/terminal/common/terminal'; -import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { Color } from 'vs/base/common/color'; +import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; +import { IBeforeProcessDataEvent, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; +import type { IBuffer, IBufferCell, IDisposable, ITerminalAddon, Terminal } from 'xterm'; const CSI = '\x1b['; const SHOW_CURSOR = `${CSI}?25h`; const HIDE_CURSOR = `${CSI}?25l`; +const DELETE_CHAR = `${CSI}X`; +const CSI_STYLE_RE = /^\x1b\[[0-9;]+m/; +const CSI_MOVE_RE = /^\x1b\[([0-9]*)([DC])/; + +const enum CursorMoveDirection { + Back = 'D', + Forwards = 'C', +} const setCursorPos = (x: number, y: number) => `${CSI}${y + 1};${x + 1}H`; const setCursorCoordinate = (buffer: IBuffer, c: ICoordinate) => setCursorPos(c.x, c.y + (c.baseY - buffer.baseY)); -const enum ReapplyBehavior { - /** Applying the prediction to the line is a no-op */ - NoOp, - /** The prediction can be applied to the given line */ - Apply, - /** The prediction would overwrite other, unexpected data on the line */ - Mismatch, -} - interface ICoordinate { x: number; y: number; @@ -41,47 +40,86 @@ interface IPrediction { apply(buffer: IBuffer, cursor: ICoordinate): string; /** - * Returns a sequence to roll back a previous `apply()` call. + * Returns a sequence to roll back a previous `apply()` call. If + * `rollForwards` is not given, then this is also called if a prediction + * is correct before show the user's data. */ rollback(buffer: IBuffer): string; + /** + * If available, this will be called when the prediction is correct. + */ + rollForwards?(buffer: IBuffer, withInput: string): string; + /** * Returns whether the given input is one expected by this prediction. */ - matches(input: string): boolean; - - /** - * Returns the reapply behavior for the given line. - */ - getReapplyBehavior(buffer: IBuffer, allPredictions: ReadonlyArray): ReapplyBehavior; + matches(input: StringReader): boolean; } -/** - * Boundary added to the prediction timeline to indicate that predictions - * should be held and not applied until all previous predictions are validated. - */ -interface IPredictionBoundary { - /** - * Checks whether a change was made that satisfies the expectation for the - * prediction boundary. For instance is pressing enter, it checks that that - * results in a new line. - */ - test(input: string): boolean; -} -const csiRe = /\x1b\[*?[a-zA-Z]/g; +class StringReader { + public index = 0; + + public get remaining() { + return this.input.length - this.index; + } + + constructor(private readonly input: string) { } + + public eatStr(substr: string) { + if (this.input.slice(this.index, this.index + substr.length) !== substr) { + return; + } + + this.index += substr.length; + return substr; + } + + public eatRe(re: RegExp) { + const match = re.exec(this.input.slice(this.index)); + if (!match) { + return; + } + + this.index += match[0].length; + return match; + } + + public eatCharCode(min = 0, max = Infinity) { + const code = this.input.charCodeAt(this.index); + if (code < min || code >= max) { + return undefined; + } + + this.index++; + return code; + } + + public rest() { + return this.input.slice(this.index); + } +} /** * Boundary which never tests true. Will always discard predictions. */ -class HardBoundary implements IPredictionBoundary { - public test() { +class HardBoundary implements IPrediction { + public apply() { + return ''; + } + + public rollback() { + return ''; + } + + public matches() { return false; } } class CharacterPrediction implements IPrediction { - private appliedAt?: { + protected appliedAt?: { x: number; y: number; baseY: number; @@ -108,51 +146,112 @@ class CharacterPrediction implements IPrediction { const a = this.appliedAt; this.appliedAt = undefined; - return setCursorCoordinate(buffer, a) + (a.oldChar ? `${a.oldAttributes}${a.oldChar}${setCursorCoordinate(buffer, a)}` : `${CSI}X`); + return setCursorCoordinate(buffer, a) + (a.oldChar ? `${a.oldAttributes}${a.oldChar}${setCursorCoordinate(buffer, a)}` : DELETE_CHAR); } - public matches(input: string) { - csiRe.lastIndex = 0; - return input.replace(csiRe, '') === this.char; - } + public matches(input: StringReader) { + let startIndex = input.index; - public getReapplyBehavior(buffer: IBuffer) { - if (!this.appliedAt) { - return ReapplyBehavior.Apply; + // remove any styling CSI before checking the char + while (input.eatRe(CSI_STYLE_RE)) { } + if (input.eatStr(this.char)) { + return true; } - const cellText = getCellAtCoordinate(buffer, this.appliedAt)?.getChars() ?? ''; - if (cellText === this.char) { - return ReapplyBehavior.NoOp; - } else if (cellText === this.appliedAt.oldChar) { - return ReapplyBehavior.Apply; - } else { - return ReapplyBehavior.Mismatch; - } + input.index = startIndex; + return false; } } -interface IPredictionGroup { - boundary?: IPredictionBoundary; - predictions: IPrediction[]; +class BackspacePrediction extends CharacterPrediction { + public apply(buffer: IBuffer, cursor: ICoordinate) { + const cell = getCellAtCoordinate(buffer, cursor); + this.appliedAt = cell + ? { ...cursor, oldAttributes: getBufferCellAttributes(cell), oldChar: cell.getChars() } + : { ...cursor, oldAttributes: '', oldChar: '' }; + + cursor.x--; + return setCursorCoordinate(buffer, cursor) + DELETE_CHAR; + } + + public matches(input: StringReader) { + return !!input.eatStr('\b'); + } +} + +class NewlinePrediction implements IPrediction { + protected prevPosition?: ICoordinate; + + public apply(_: IBuffer, cursor: ICoordinate) { + this.prevPosition = { ...cursor }; + cursor.x = 0; + cursor.y++; + return '\r\n'; + } + + public rollback(buffer: IBuffer) { + if (!this.prevPosition) { + return ''; // not applied + } + + const p = this.prevPosition; + this.prevPosition = undefined; + return setCursorCoordinate(buffer, p) + DELETE_CHAR; + } + + public rollForwards() { + return ''; // does not need to rewrite + } + + public matches(input: StringReader) { + return !!input.eatStr('\r\n'); + } +} + +class CursorMovePrediction implements IPrediction { + constructor(private readonly direction: CursorMoveDirection, private readonly amount: number) { } + + public apply(_: IBuffer, cursor: ICoordinate) { + const { amount, direction } = this; + cursor.x += (direction === CursorMoveDirection.Back ? -1 : 1) * amount; + return `${CSI}${amount}${direction}`; + } + + public rollback() { + const fn = this.direction === CursorMoveDirection.Back ? CursorMoveDirection.Forwards : CursorMoveDirection.Back; + return `${CSI}${this.amount}${fn}`; + } + + public rollForwards() { + return ''; // does not need to rewrite + } + + public matches(input: StringReader) { + const { amount, direction } = this; + if (amount === 1 && input.eatStr(`${CSI}${direction}`)) { + return true; + } + + if (amount === 1 && this.direction === CursorMoveDirection.Back && input.eatStr('\b')) { + return true; + } + + return !!input.eatStr(`${CSI}${amount}${direction}`); + } } -const invalidatedRestoreInterval = 200; class PredictionTimeline { /** - * Expected queue of events, separated whenever a PredictionBoundary is - * emitted. Prediction groups _always_ apply to the active row. + * Expected queue of events. Only predictions for the lowest are + * written into the terminal. */ - private expected: IPredictionGroup[] = []; + private expected: ({ gen: number; p: IPrediction })[] = []; /** - * Last-invalidated set of predictions. These are restored if the predictions - * become valid again in a short period of time. Some terminal programs - * rewrite lines or the entire display on backspace, for example, which will - * invalidate the predictions even if they become valid again a moment later. + * Current prediction generation. */ - private invalidated: { at: number; p: IPredictionGroup[] } | undefined; + private currentGen = 0; /** * Cursor position -- kept outside the buffer since it can be ahead if @@ -178,42 +277,43 @@ class PredictionTimeline { } let output = ''; - let brokeBoundary = false; - for (let i = 0; i < input.length; i++) { - const test = this.expected[0]?.predictions[0]; - const char = input[i]; - // if we reached the end of our tests, try to break through the boundary - // and start applying its items. - if (!test) { - this.expected.shift(); - if (!this.expected.length) { - return output + input.slice(i); - } - brokeBoundary = true; - if (this.expected[0].boundary?.test(char) === false) { - this.expected = []; - return output + input.slice(i); // boundary assumption invalid, throw out predictions - } - } + const reader = new StringReader(input); + const startingGen = this.expected[0].gen; + while (this.expected.length && reader.remaining > 0) { + const prediction = this.expected[0].p; + let beforeTestReaderIndex = reader.index; + // if the input character matches what the next prediction expected, undo // the prediction and write the real character out. - else if (test.matches(char)) { - output += test.rollback(buffer) + char; - this.expected[0].predictions.shift(); + if (prediction.matches(reader)) { + const eaten = input.slice(beforeTestReaderIndex, reader.index); + output += prediction.rollForwards?.(buffer, eaten) + ?? (prediction.rollback(buffer) + input.slice(beforeTestReaderIndex, reader.index)); + this.expected.shift(); } - // otherwise, roll back all pending predictions and move the current stack - // of predictions into the "invalidated" key (for possible resurrection). + // otherwise, roll back all pending predictions else { - this.invalidated = { at: Date.now(), p: this.expected }; + output += this.expected.filter(p => p.gen === startingGen) + .map(({ p }) => p.rollback(buffer)) + .reverse() + .join(''); this.expected = []; this.cursor = undefined; - if (!brokeBoundary) { // on a new boundary, we did not apply predictions yet - output += this.invalidated.p[0].predictions.map(p => p.rollback(buffer)).reverse().join(''); + break; + } + } + + output += reader.rest(); + + // If we passed a generation boundary, apply the current generation's predictions + if (this.expected.length && startingGen !== this.expected[0].gen) { + for (const { p, gen } of this.expected) { + if (gen !== this.expected[0].gen) { + break; } - output += input.slice(i); - break; + output += p.apply(buffer, this.getCursor(buffer)); } } @@ -228,42 +328,26 @@ class PredictionTimeline { return output; } - /** - * Should be called after data is applied to the terminal. - */ - public afterServerInput() { - const buffer = this.getActiveBuffer(); - if (buffer && this.invalidated && Date.now() - this.invalidated.at < invalidatedRestoreInterval) { - this.tryReapplyInvalidated(buffer, this.invalidated.p); - } - } - /** * Appends a typeahead prediction. */ public addPrediction(buffer: IBuffer, prediction: IPrediction) { - const l = this.expected.length - 1; - if (l === -1) { - this.expected = [{ predictions: [prediction] }]; - } else { - this.expected[l].predictions.push(prediction); - } - - if (l <= 1) { + this.expected.push({ gen: this.currentGen, p: prediction }); + if (this.currentGen === this.expected[0].gen) { const text = prediction.apply(buffer, this.getCursor(buffer)); - console.log('prediction:', text); + console.log('prediction:', JSON.stringify(text)); this.terminal.write(text); } } /** - * Appends a boundary to the preduction. + * Appends a boundary to the prediction. */ - public addBoundary(boundary: IPredictionBoundary) { - this.expected.push({ boundary, predictions: [] }); + public addBoundary() { + this.currentGen++; } - private getCursor(buffer: IBuffer) { + public getCursor(buffer: IBuffer) { if (!this.cursor) { this.cursor = { baseY: buffer.baseY, y: buffer.cursorY, x: buffer.cursorX }; } @@ -275,42 +359,6 @@ class PredictionTimeline { const buffer = this.terminal.buffer.active; return buffer.type === 'normal' ? buffer : undefined; } - - private tryReapplyInvalidated(buffer: IBuffer, invalidated: IPredictionGroup[]) { - if (!invalidated.length) { - return; - } - - const predictions = invalidated[0].predictions; - let lastNoop = -1; - let hasApply = false; - for (let i = 0; i < predictions.length; i++) { - switch (predictions[i].getReapplyBehavior(buffer, predictions)) { - case ReapplyBehavior.NoOp: - if (!hasApply) { lastNoop = i; } - break; - case ReapplyBehavior.Mismatch: - return; // do not reapply any prediction in this set if there's a mismatch - case ReapplyBehavior.Apply: - hasApply = true; - break; - } - } - - if (!hasApply) { - return; - } - - for (let i = lastNoop + 1; i < predictions.length; i++) { - this.terminal.write(predictions[i].apply(buffer, this.getCursor(buffer))); - } - - this.expected = invalidated.slice(1); - - if (lastNoop < predictions.length - 1) { - this.expected.unshift({ predictions: predictions.slice(lastNoop) }); - } - } } /** * Gets the escape sequence to restore state/appearence in the cell. @@ -368,19 +416,38 @@ export class TypeAheadAddon implements ITerminalAddon { return; } - console.log('user data:', data); - if (data.length !== 1) { - this.timeline.addBoundary(new HardBoundary()); - return; - } + console.log('user data:', JSON.stringify(data)); const terminal = this.timeline.terminal; - const code = data.charCodeAt(0); - if (code >= 32 && code < 126) { - if (terminal.buffer.active.cursorX === terminal.cols - 1) { - } else { - this.timeline.addPrediction(terminal.buffer.active, new CharacterPrediction(this.typeheadStyle, data)); + const buffer = terminal.buffer.active; + const reader = new StringReader(data); + while (reader.remaining > 0) { + if (reader.eatStr('\b')) { // backspace + this.timeline.addPrediction(buffer, new BackspacePrediction(this.typeheadStyle, '\b')); + continue; } + + if (reader.eatCharCode(32, 126)) { // alphanum + const char = data[reader.index - 1]; + this.timeline.addPrediction(buffer, new CharacterPrediction(this.typeheadStyle, char)); + if (this.timeline.getCursor(buffer).x === terminal.cols) { + this.timeline.addPrediction(buffer, new NewlinePrediction()); + this.timeline.addBoundary(); + } + continue; + } + + const cursorMv = reader.eatRe(CSI_MOVE_RE); + if (cursorMv) { + this.timeline.addPrediction(buffer, new CursorMovePrediction( + cursorMv[2] as CursorMoveDirection, Number(cursorMv[1]) || 1)); + continue; + } + + // something else + this.timeline.addPrediction(buffer, new HardBoundary()); + this.timeline.addBoundary(); + break; } } @@ -389,8 +456,8 @@ export class TypeAheadAddon implements ITerminalAddon { return; } - console.log('incoming data:', event.data); + console.log('incoming data:', JSON.stringify(event.data)); event.data = this.timeline.beforeServerInput(event.data); - console.log('emitted data:', event.data); + console.log('emitted data:', JSON.stringify(event.data)); } } From 3f9a7a2e05e4a48c56f9877916d2848f68074a43 Mon Sep 17 00:00:00 2001 From: a5hk <5412540+a5hk@users.noreply.github.com> Date: Fri, 16 Oct 2020 10:24:08 +0330 Subject: [PATCH 007/212] closes #97890 --- .../inspectEditorTokens/inspectEditorTokens.css | 12 ++++++++++++ .../inspectEditorTokens/inspectEditorTokens.ts | 12 +++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css index 63601ef1c79..35526df5a0b 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css @@ -34,6 +34,18 @@ text-align: right; word-break: break-word; } + +.tiw-metadata-values { + list-style: none; + max-height: 300px; + overflow-y: auto; + margin-right: -10px; +} + +.tiw-metadata-values > .tiw-metadata-value { + margin-right: 10px; +} + .tiw-metadata-key { vertical-align: top; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index 75138d4a33c..97a0010dfbd 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -594,11 +594,17 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { theme.resolveScopes(definition, scopesDefinition); const matchingRule = scopesDefinition[property]; if (matchingRule && scopesDefinition.scope) { - const strScopes = Array.isArray(matchingRule.scope) ? matchingRule.scope.join(', ') : String(matchingRule.scope); + const scopes = $('ul.tiw-metadata-values'); + const strScopes = Array.isArray(matchingRule.scope) ? matchingRule.scope : [String(matchingRule.scope)]; + + for (let strScope of strScopes) { + scopes.appendChild($('li.tiw-metadata-value.tiw-metadata-scopes', undefined, strScope)); + } + elements.push( scopesDefinition.scope.join(' '), - $('br'), - $('code.tiw-theme-selector', undefined, strScopes, $('br'), JSON.stringify(matchingRule.settings, null, '\t'))); + scopes, + $('code.tiw-theme-selector', undefined, JSON.stringify(matchingRule.settings, null, '\t'))); return elements; } return elements; From 177eba74b695d1cfe02d2bc7aea89dd974f40d42 Mon Sep 17 00:00:00 2001 From: a5hk <5412540+a5hk@users.noreply.github.com> Date: Sat, 17 Oct 2020 17:20:15 +0330 Subject: [PATCH 008/212] left aligned values --- .../browser/inspectEditorTokens/inspectEditorTokens.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css index 35526df5a0b..5f7ec45d7f7 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css @@ -31,7 +31,6 @@ .tiw-metadata-value { font-family: var(--monaco-monospace-font); - text-align: right; word-break: break-word; } @@ -40,6 +39,7 @@ max-height: 300px; overflow-y: auto; margin-right: -10px; + padding-left: 0; } .tiw-metadata-values > .tiw-metadata-value { @@ -47,6 +47,10 @@ } .tiw-metadata-key { + width: 1px; + min-width: 150px; + padding-right: 10px; + white-space: nowrap; vertical-align: top; } From 5e1d9eb316e63eaf129ab02b670d510f4a418d9e Mon Sep 17 00:00:00 2001 From: Kenny Smith Date: Sat, 17 Oct 2020 08:31:00 -0700 Subject: [PATCH 009/212] Trash/delete shortcut for forward delete on MacOS --- .../contrib/files/browser/fileActions.contribution.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 086fa8c123a..6b097374647 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -80,7 +80,8 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext, ExplorerResourceMoveableToTrash), primary: KeyCode.Delete, mac: { - primary: KeyMod.CtrlCmd | KeyCode.Backspace + primary: KeyMod.CtrlCmd | KeyCode.Backspace, + secondary: [KeyCode.Delete] }, handler: moveFileToTrashHandler }); From 8d7ad831e57e9038d8c982d861c647d2a487f40e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 19 Oct 2020 08:04:47 +0200 Subject: [PATCH 010/212] Improve window.withProgress in status bar to use a static spin icon (fix #108657) --- src/vs/base/browser/codicons.ts | 6 +- src/vs/base/browser/dom.ts | 9 +- .../browser/parts/statusbar/statusbarPart.ts | 91 +++++++++++++++---- .../extensionProfileService.ts | 5 +- .../contrib/remote/browser/remoteIndicator.ts | 5 +- .../progress/browser/progressService.ts | 3 +- .../services/statusbar/common/statusbar.ts | 5 + 7 files changed, 100 insertions(+), 24 deletions(-) diff --git a/src/vs/base/browser/codicons.ts b/src/vs/base/browser/codicons.ts index ab29f13ce8b..cf55c1a9b92 100644 --- a/src/vs/base/browser/codicons.ts +++ b/src/vs/base/browser/codicons.ts @@ -18,7 +18,7 @@ export function renderCodicons(text: string): Array { textStart = (match.index || 0) + match[0].length; const [, escaped, codicon, name, animation] = match; - elements.push(escaped ? `$(${codicon})` : dom.$(`span.codicon.codicon-${name}${animation ? `.codicon-animation-${animation}` : ''}`)); + elements.push(escaped ? `$(${codicon})` : renderCodicon(name, animation)); } if (textStart < text.length) { @@ -26,3 +26,7 @@ export function renderCodicons(text: string): Array { } return elements; } + +export function renderCodicon(name: string, animation: string): HTMLSpanElement { + return dom.$(`span.codicon.codicon-${name}${animation ? `.codicon-animation-${animation}` : ''}`); +} diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 0c19deeb873..8a3e808a871 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -998,8 +998,15 @@ export function prepend(parent: HTMLElement, child: T): T { /** * Removes all children from `parent` and appends `children` */ -export function reset(parent: HTMLElement, ...children: Array) { +export function reset(parent: HTMLElement, ...children: Array): void { parent.innerText = ''; + appendChildren(parent, ...children); +} + +/** + * Appends `children` to `parent` + */ +export function appendChildren(parent: HTMLElement, ...children: Array): void { for (const child of children) { if (child instanceof Node) { parent.appendChild(child); diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index ce83ddf403b..ced7b6480a6 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -21,7 +21,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { isThemeColor } from 'vs/editor/common/editorCommon'; import { Color } from 'vs/base/common/color'; -import { EventHelper, createStyleSheet, addDisposableListener, EventType, hide, show, isAncestor } from 'vs/base/browser/dom'; +import { EventHelper, createStyleSheet, addDisposableListener, EventType, hide, show, isAncestor, appendChildren } from 'vs/base/browser/dom'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage'; import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -39,6 +39,7 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { renderCodicon, renderCodicons } from 'vs/base/browser/codicons'; interface IPendingStatusbarEntry { id: string; @@ -64,17 +65,18 @@ class StatusbarViewModel extends Disposable { static readonly HIDDEN_ENTRIES_KEY = 'workbench.statusbar.hidden'; + private readonly _onDidChangeEntryVisibility = this._register(new Emitter<{ id: string, visible: boolean }>()); + readonly onDidChangeEntryVisibility = this._onDidChangeEntryVisibility.event; + private readonly _entries: IStatusbarViewModelEntry[] = []; get entries(): IStatusbarViewModelEntry[] { return this._entries; } - private hidden!: Set; + private _lastFocusedEntry: IStatusbarViewModelEntry | undefined; get lastFocusedEntry(): IStatusbarViewModelEntry | undefined { return this._lastFocusedEntry && !this.isHidden(this._lastFocusedEntry.id) ? this._lastFocusedEntry : undefined; } - private _lastFocusedEntry: IStatusbarViewModelEntry | undefined; - private readonly _onDidChangeEntryVisibility = this._register(new Emitter<{ id: string, visible: boolean }>()); - readonly onDidChangeEntryVisibility = this._onDidChangeEntryVisibility.event; + private hidden!: Set; constructor(private readonly storageService: IStorageService) { super(); @@ -706,12 +708,67 @@ export class StatusbarPart extends Part implements IStatusbarService { } } +class StatusBarCodiconLabel extends CodiconLabel { + + private readonly progressCodicon = renderCodicon('sync', 'spin'); + + private currentText = ''; + private currentShowProgress = false; + + constructor( + private readonly container: HTMLElement + ) { + super(container); + } + + set showProgress(showProgress: boolean) { + if (this.currentShowProgress !== showProgress) { + this.currentShowProgress = showProgress; + this.text = this.currentText; + } + } + + set text(text: string) { + + // Progress: insert progress codicon as first element as needed + // but keep it stable so that the animation does not reset + if (this.currentShowProgress) { + + // Append as needed + if (this.container.firstChild !== this.progressCodicon) { + this.container.appendChild(this.progressCodicon); + } + + // Remove others + for (const node of Array.from(this.container.childNodes)) { + if (node !== this.progressCodicon) { + node.remove(); + } + } + + // If we have text to show, add a space to separate from progress + let textContent = text ?? ''; + if (textContent) { + textContent = ` ${textContent}`; + } + + // Append new elements + appendChildren(this.container, ...renderCodicons(textContent)); + } + + // No Progress: no special handling + else { + super.text = text; + } + } +} + class StatusbarEntryItem extends Disposable { - private entry!: IStatusbarEntry; + readonly labelContainer: HTMLElement; + private readonly label: StatusBarCodiconLabel; - labelContainer!: HTMLElement; - private label!: CodiconLabel; + private entry: IStatusbarEntry | undefined = undefined; private readonly foregroundListener = this._register(new MutableDisposable()); private readonly backgroundListener = this._register(new MutableDisposable()); @@ -729,26 +786,25 @@ class StatusbarEntryItem extends Disposable { ) { super(); - this.create(); - this.update(entry); - } - - private create(): void { - // Label Container this.labelContainer = document.createElement('a'); this.labelContainer.tabIndex = -1; // allows screen readers to read title, but still prevents tab focus. this.labelContainer.setAttribute('role', 'button'); - // Label - this.label = new CodiconLabel(this.labelContainer); + // Label (with support for progress) + this.label = new StatusBarCodiconLabel(this.labelContainer); // Add to parent this.container.appendChild(this.labelContainer); + + this.update(entry); } update(entry: IStatusbarEntry): void { + // Update: Progress + this.label.showProgress = !!entry.showProgress; + // Update: Text if (!this.entry || entry.text !== this.entry.text) { this.label.text = entry.text; @@ -760,8 +816,9 @@ class StatusbarEntryItem extends Disposable { } } + // Set the aria label on both elements so screen readers would read + // the correct thing without duplication #96210 if (!this.entry || entry.ariaLabel !== this.entry.ariaLabel) { - // Set the aria label on both elements so screen readers would read the correct thing without duplication #96210 this.container.setAttribute('aria-label', entry.ariaLabel); this.labelContainer.setAttribute('aria-label', entry.ariaLabel); } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts index d491483b338..34cdcea7efa 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts @@ -82,7 +82,8 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio if (visible) { const indicator: IStatusbarEntry = { - text: '$(sync~spin) ' + nls.localize('profilingExtensionHost', "Profiling Extension Host"), + text: nls.localize('profilingExtensionHost', "Profiling Extension Host"), + showProgress: true, ariaLabel: nls.localize('profilingExtensionHost', "Profiling Extension Host"), tooltip: nls.localize('selectAndStartDebug', "Click to stop profiling."), command: 'workbench.action.extensionHostProfilder.stop' @@ -91,7 +92,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio const timeStarted = Date.now(); const handle = setInterval(() => { if (this.profilingStatusBarIndicator) { - this.profilingStatusBarIndicator.update({ ...indicator, text: '$(sync~spin) ' + nls.localize('profilingExtensionHostTime', "Profiling Extension Host ({0} sec)", Math.round((new Date().getTime() - timeStarted) / 1000)), }); + this.profilingStatusBarIndicator.update({ ...indicator, text: nls.localize('profilingExtensionHostTime', "Profiling Extension Host ({0} sec)", Math.round((new Date().getTime() - timeStarted) / 1000)), }); } }, 1000); this.profilingStatusBarIndicatorLabelUpdater.value = toDisposable(() => clearInterval(handle)); diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index 8a1a986c824..83e5499b936 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -197,7 +197,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr const hostLabel = this.labelService.getHostLabel(Schemas.vscodeRemote, this.remoteAuthority) || this.remoteAuthority; switch (this.connectionState) { case 'initializing': - this.renderRemoteStatusIndicator(`$(sync~spin) ${nls.localize('host.open', "Opening Remote...")}`, nls.localize('host.open', "Opening Remote...")); + this.renderRemoteStatusIndicator(nls.localize('host.open', "Opening Remote..."), nls.localize('host.open', "Opening Remote..."), undefined, true /* progress */); break; case 'disconnected': this.renderRemoteStatusIndicator(`$(alert) ${nls.localize('disconnectedFrom', "Disconnected from {0}", hostLabel)}`, nls.localize('host.tooltipDisconnected', "Disconnected from {0}", hostLabel)); @@ -219,7 +219,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr } } - private renderRemoteStatusIndicator(text: string, tooltip?: string, command?: string): void { + private renderRemoteStatusIndicator(text: string, tooltip?: string, command?: string, showProgress?: boolean): void { const name = nls.localize('remoteHost', "Remote Host"); if (typeof command !== 'string' && this.remoteMenu.getActions().length > 0) { command = RemoteStatusIndicator.REMOTE_ACTIONS_COMMAND_ID; @@ -230,6 +230,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr color: themeColorFromId(STATUS_BAR_HOST_NAME_FOREGROUND), ariaLabel: name, text, + showProgress, tooltip, command }; diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 597aaf8c80c..481205112be 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -151,7 +151,8 @@ export class ProgressService extends Disposable implements IProgressService { } const statusEntryProperties: IStatusbarEntry = { - text: `$(sync~spin) ${text}`, + text, + showProgress: true, ariaLabel: text, tooltip: title, command: progressCommand diff --git a/src/vs/workbench/services/statusbar/common/statusbar.ts b/src/vs/workbench/services/statusbar/common/statusbar.ts index 14a52e61d68..3d65b4fbaa1 100644 --- a/src/vs/workbench/services/statusbar/common/statusbar.ts +++ b/src/vs/workbench/services/statusbar/common/statusbar.ts @@ -63,6 +63,11 @@ export interface IStatusbarEntry { * Whether to show a beak above the status bar entry. */ readonly showBeak?: boolean; + + /** + * Will enable a spinning icon in front of the text to indicate progress. + */ + readonly showProgress?: boolean; } export interface IStatusbarService { From 845d0144295d4ed519c25fde044a344c3304af8d Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 19 Oct 2020 09:55:52 +0200 Subject: [PATCH 011/212] [scss] Normalise SCSS attributes with CSS/LESS/SASS. Fixes #108840 --- .../scss/test/colorize-results/test_scss.json | 20 +++++++++---------- extensions/theme-defaults/themes/dark_vs.json | 1 - .../themes/hc_black_defaults.json | 3 --- .../theme-defaults/themes/light_vs.json | 1 - 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/extensions/scss/test/colorize-results/test_scss.json b/extensions/scss/test/colorize-results/test_scss.json index 5841c3b8e3f..66f3e1f376d 100644 --- a/extensions/scss/test/colorize-results/test_scss.json +++ b/extensions/scss/test/colorize-results/test_scss.json @@ -16206,11 +16206,11 @@ "c": "rel", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.attribute-selector.scss entity.other.attribute-name.attribute.scss", "r": { - "dark_plus": "entity.other.attribute-name.attribute.scss: #D7BA7D", - "light_plus": "entity.other.attribute-name.attribute.scss: #800000", - "dark_vs": "entity.other.attribute-name.attribute.scss: #D7BA7D", - "light_vs": "entity.other.attribute-name.attribute.scss: #800000", - "hc_black": "entity.other.attribute-name.attribute.scss: #D7BA7D" + "dark_plus": "entity.other.attribute-name: #9CDCFE", + "light_plus": "entity.other.attribute-name: #FF0000", + "dark_vs": "entity.other.attribute-name: #9CDCFE", + "light_vs": "entity.other.attribute-name: #FF0000", + "hc_black": "entity.other.attribute-name: #9CDCFE" } }, { @@ -20606,11 +20606,11 @@ "c": "data-icon", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.attribute-selector.scss entity.other.attribute-name.attribute.scss", "r": { - "dark_plus": "entity.other.attribute-name.attribute.scss: #D7BA7D", - "light_plus": "entity.other.attribute-name.attribute.scss: #800000", - "dark_vs": "entity.other.attribute-name.attribute.scss: #D7BA7D", - "light_vs": "entity.other.attribute-name.attribute.scss: #800000", - "hc_black": "entity.other.attribute-name.attribute.scss: #D7BA7D" + "dark_plus": "entity.other.attribute-name: #9CDCFE", + "light_plus": "entity.other.attribute-name: #FF0000", + "dark_vs": "entity.other.attribute-name: #9CDCFE", + "light_vs": "entity.other.attribute-name: #FF0000", + "hc_black": "entity.other.attribute-name: #9CDCFE" } }, { diff --git a/extensions/theme-defaults/themes/dark_vs.json b/extensions/theme-defaults/themes/dark_vs.json index 1b4cf8b967e..3a5008aef7a 100644 --- a/extensions/theme-defaults/themes/dark_vs.json +++ b/extensions/theme-defaults/themes/dark_vs.json @@ -86,7 +86,6 @@ "entity.other.attribute-name.pseudo-class.css", "entity.other.attribute-name.pseudo-element.css", "source.css.less entity.other.attribute-name.id", - "entity.other.attribute-name.attribute.scss", "entity.other.attribute-name.scss" ], "settings": { diff --git a/extensions/theme-defaults/themes/hc_black_defaults.json b/extensions/theme-defaults/themes/hc_black_defaults.json index 495a15238dc..d0382cec294 100644 --- a/extensions/theme-defaults/themes/hc_black_defaults.json +++ b/extensions/theme-defaults/themes/hc_black_defaults.json @@ -105,10 +105,7 @@ "entity.other.attribute-name.parent-selector.css", "entity.other.attribute-name.pseudo-class.css", "entity.other.attribute-name.pseudo-element.css", - "source.css.less entity.other.attribute-name.id", - - "entity.other.attribute-name.attribute.scss", "entity.other.attribute-name.scss" ], "settings": { diff --git a/extensions/theme-defaults/themes/light_vs.json b/extensions/theme-defaults/themes/light_vs.json index 3410551898b..23881ae8dc7 100644 --- a/extensions/theme-defaults/themes/light_vs.json +++ b/extensions/theme-defaults/themes/light_vs.json @@ -87,7 +87,6 @@ "entity.other.attribute-name.pseudo-class.css", "entity.other.attribute-name.pseudo-element.css", "source.css.less entity.other.attribute-name.id", - "entity.other.attribute-name.attribute.scss", "entity.other.attribute-name.scss" ], "settings": { From c84c1fd4ad2b310c7c3edf17ee1a484b6cf7bb03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Mon, 12 Oct 2020 16:11:11 +0200 Subject: [PATCH 012/212] :lipstick: --- src/vs/workbench/api/browser/mainThreadSCM.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 2f0b06a143d..9594c9137d6 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -133,8 +133,7 @@ class MainThreadSCMProvider implements ISCMProvider { private readonly _handle: number, private readonly _contextValue: string, private readonly _label: string, - private readonly _rootUri: URI | undefined, - @ISCMService scmService: ISCMService + private readonly _rootUri: URI | undefined ) { } $updateSourceControl(features: SCMProviderFeatures): void { @@ -290,7 +289,7 @@ export class MainThreadSCM implements MainThreadSCMShape { } $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined): void { - const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined, this.scmService); + const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined); const repository = this.scmService.registerSCMProvider(provider); this._repositories.set(handle, repository); From b9c17796cc907810bc4e850f6854f8fa7b777318 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 19 Oct 2020 10:45:09 +0200 Subject: [PATCH 013/212] towards details as overlay widget --- src/vs/editor/contrib/suggest/resizable.ts | 36 ++++++++++------- .../editor/contrib/suggest/suggestWidget.ts | 9 ++--- .../contrib/suggest/suggestWidgetDetails.ts | 40 +++++++++++++++++-- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/src/vs/editor/contrib/suggest/resizable.ts b/src/vs/editor/contrib/suggest/resizable.ts index 7d9aa354a09..0d81215d2e4 100644 --- a/src/vs/editor/contrib/suggest/resizable.ts +++ b/src/vs/editor/contrib/suggest/resizable.ts @@ -9,12 +9,20 @@ import { Dimension } from 'vs/base/browser/dom'; import { Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; +export interface IResizeEvent { + dimenion: Dimension; + done: boolean; +} + export class ResizableHTMLElement { readonly domNode: HTMLElement; - private readonly _onDidResize = new Emitter(); - readonly onDidResize: Event = this._onDidResize.event; + private readonly _onDidWillResize = new Emitter(); + readonly onDidWillResize: Event = this._onDidWillResize.event; + + private readonly _onDidResize = new Emitter(); + readonly onDidResize: Event = this._onDidResize.event; private readonly _eastSash: Sash; private readonly _southSash: Sash; @@ -34,26 +42,31 @@ export class ResizableHTMLElement { let deltaY = 0; let deltaX = 0; - this._sashListener.add(Event.any(this._eastSash.onDidEnd, this._southSash.onDidEnd)(() => { - currentSize = undefined; - deltaY = 0; - deltaX = 0; - })); this._sashListener.add(Event.any(this._eastSash.onDidStart, this._southSash.onDidStart)(() => { + this._onDidWillResize.fire(); currentSize = this._size; deltaY = 0; deltaX = 0; })); + this._sashListener.add(Event.any(this._eastSash.onDidEnd, this._southSash.onDidEnd)(() => { + currentSize = undefined; + deltaY = 0; + deltaX = 0; + this._onDidResize.fire({ dimenion: this._size!, done: false }); + })); + this._sashListener.add(this._southSash.onDidChange(e => { if (currentSize) { deltaY = e.currentY - e.startY; - this._resize(currentSize.height + deltaY, currentSize.width + deltaX); + this.layout(currentSize.height + deltaY, currentSize.width + deltaX); + this._onDidResize.fire({ dimenion: this._size!, done: false }); } })); this._sashListener.add(this._eastSash.onDidChange(e => { if (currentSize) { deltaX = e.currentX - e.startX; - this._resize(currentSize.height + deltaY, currentSize.width + deltaX); + this.layout(currentSize.height + deltaY, currentSize.width + deltaX); + this._onDidResize.fire({ dimenion: this._size!, done: false }); } })); } @@ -65,11 +78,6 @@ export class ResizableHTMLElement { this.domNode.remove(); } - private _resize(height: number, width: number): void { - this.layout(height, width); - this._onDidResize.fire(this._size!); - } - layout(height: number, width: number): void { const newSize = new Dimension(width, height); if (!Dimension.equals(newSize, this._size)) { diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index b43a0e9e2f9..f958ff0f669 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -31,7 +31,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { TimeoutTimer, CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { SuggestionDetails, canExpandCompletionItem } from './suggestWidgetDetails'; +import { SuggestDetailsWidget, canExpandCompletionItem } from './suggestWidgetDetails'; import { SuggestWidgetStatus } from 'vs/editor/contrib/suggest/suggestWidgetStatus'; import { getAriaId, ItemRenderer } from './suggestWidgetRenderer'; import { ResizableHTMLElement } from './resizable'; @@ -88,8 +88,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate; private status: SuggestWidgetStatus; - private details: SuggestionDetails; - // private listHeight?: number; + private details: SuggestDetailsWidget; private readonly ctxSuggestWidgetVisible: IContextKey; private readonly ctxSuggestWidgetDetailsVisible: IContextKey; @@ -139,7 +138,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate { - this.layout(e.height, e.width); + this.layout(e.dimenion.height, e.dimenion.width); })); this._disposables.add(addDisposableListener(this.element.domNode, 'click', e => { if (e.target === this.element.domNode) { @@ -150,7 +149,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate Date: Mon, 19 Oct 2020 10:48:34 +0200 Subject: [PATCH 014/212] web - allow to download folders (fix #83579) --- src/vs/base/browser/dom.ts | 45 ++++ src/vs/platform/files/common/files.ts | 8 +- src/vs/workbench/browser/contextkeys.ts | 8 +- .../files/browser/fileActions.contribution.ts | 13 +- .../contrib/files/browser/fileActions.ts | 238 +++++++++++++++--- .../files/browser/views/explorerViewer.ts | 21 +- 6 files changed, 284 insertions(+), 49 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 8a3e808a871..2d753c91e78 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1390,3 +1390,48 @@ function toBinary(str: string): string { export function multibyteAwareBtoa(str: string): string { return btoa(toBinary(str)); } + +/** + * Typings for the https://wicg.github.io/file-system-access + * + * Use `supported(window)` to find out if the browser supports this kind of API. + */ +export namespace WebFileSystemAccess { + + // https://wicg.github.io/file-system-access/#dom-window-showdirectorypicker + export interface FileSystemAccess { + showDirectoryPicker: () => Promise; + } + + // https://wicg.github.io/file-system-access/#api-filesystemdirectoryhandle + export interface FileSystemDirectoryHandle { + readonly kind: 'directory', + readonly name: string, + + getFileHandle: (name: string, options?: { create?: boolean }) => Promise; + getDirectoryHandle: (name: string, options?: { create?: boolean }) => Promise; + } + + // https://wicg.github.io/file-system-access/#api-filesystemfilehandle + export interface FileSystemFileHandle { + readonly kind: 'file', + readonly name: string, + + createWritable: (options?: { keepExistingData?: boolean }) => Promise; + } + + // https://wicg.github.io/file-system-access/#api-filesystemwritablefilestream + export interface FileSystemWritableFileStream { + write: (buffer: Uint8Array) => Promise; + close: () => Promise; + } + + export function supported(obj: any & Window): obj is FileSystemAccess { + const candidate = obj as FileSystemAccess; + if (typeof candidate?.showDirectoryPicker === 'function') { + return true; + } + + return false; + } +} diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 52e9ecdf293..2d2cdcfeef4 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -11,7 +11,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Event } from 'vs/base/common/event'; import { startsWithIgnoreCase } from 'vs/base/common/strings'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { isUndefinedOrNull } from 'vs/base/common/types'; +import { isNumber, isUndefinedOrNull } from 'vs/base/common/types'; import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; import { ReadableStreamEvents } from 'vs/base/common/stream'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -978,8 +978,12 @@ export class BinarySize { static readonly TB = BinarySize.GB * BinarySize.KB; static formatSize(size: number): string { + if (!isNumber(size)) { + size = 0; + } + if (size < BinarySize.KB) { - return localize('sizeB', "{0}B", size); + return localize('sizeB', "{0}B", size.toFixed(0)); } if (size < BinarySize.MB) { diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index 1e9fb3756bb..10e7b53ef34 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -8,7 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; import { ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, TEXT_DIFF_EDITOR_ID, SplitEditorsVertically, InEditorZenModeContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, EditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext } from 'vs/workbench/common/editor'; -import { trackFocus, addDisposableListener, EventType } from 'vs/base/browser/dom'; +import { trackFocus, addDisposableListener, EventType, WebFileSystemAccess } from 'vs/base/browser/dom'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -32,6 +32,9 @@ export const RemoteNameContext = new RawContextKey('remoteName', ''); export const IsFullscreenContext = new RawContextKey('isFullscreen', false); +// Support for FileSystemAccess web APIs (https://wicg.github.io/file-system-access) +export const HasWebFileSystemAccess = new RawContextKey('hasWebFileSystemAccess', false); + export class WorkbenchContextKeysHandler extends Disposable { private inputFocusedContext: IContextKey; @@ -85,6 +88,9 @@ export class WorkbenchContextKeysHandler extends Disposable { RemoteNameContext.bindTo(this.contextKeyService).set(getRemoteName(this.environmentService.remoteAuthority) || ''); + // Capabilities + HasWebFileSystemAccess.bindTo(this.contextKeyService).set(WebFileSystemAccess.supported(window)); + // Development IsDevelopmentContext.bindTo(this.contextKeyService).set(!this.environmentService.isBuilt || this.environmentService.isExtensionDevelopment); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 086fa8c123a..29b83a97cc2 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -22,7 +22,7 @@ import { AutoSaveAfterShortDelayContext } from 'vs/workbench/services/filesConfi import { ResourceContextKey } from 'vs/workbench/common/resources'; import { WorkbenchListDoubleSelection } from 'vs/platform/list/browser/listService'; import { Schemas } from 'vs/base/common/network'; -import { DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys'; +import { DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, HasWebFileSystemAccess, WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { OpenFileFolderAction, OpenFileAction, OpenFolderAction, OpenWorkspaceAction } from 'vs/workbench/browser/actions/workspaceActions'; @@ -222,7 +222,7 @@ appendToCommandPalette(COMPARE_WITH_SAVED_COMMAND_ID, { value: nls.localize('com appendToCommandPalette(SAVE_FILE_AS_COMMAND_ID, { value: SAVE_FILE_AS_LABEL, original: 'Save As...' }, category); appendToCommandPalette(NEW_FILE_COMMAND_ID, { value: NEW_FILE_LABEL, original: 'New File' }, category, WorkspaceFolderCountContext.notEqualsTo('0')); appendToCommandPalette(NEW_FOLDER_COMMAND_ID, { value: NEW_FOLDER_LABEL, original: 'New Folder' }, category, WorkspaceFolderCountContext.notEqualsTo('0')); -appendToCommandPalette(DOWNLOAD_COMMAND_ID, { value: DOWNLOAD_LABEL, original: 'Download' }, category, ContextKeyExpr.and(ResourceContextKey.Scheme.notEqualsTo(Schemas.file))); +appendToCommandPalette(DOWNLOAD_COMMAND_ID, { value: DOWNLOAD_LABEL, original: 'Download...' }, category, ContextKeyExpr.and(ResourceContextKey.Scheme.notEqualsTo(Schemas.file))); appendToCommandPalette(NEW_UNTITLED_FILE_COMMAND_ID, { value: NEW_UNTITLED_FILE_LABEL, original: 'New Untitled File' }, category); // Menu registration - open editors @@ -489,7 +489,14 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, ({ id: DOWNLOAD_COMMAND_ID, title: DOWNLOAD_LABEL, }, - when: ContextKeyExpr.or(ContextKeyExpr.and(ResourceContextKey.Scheme.notEqualsTo(Schemas.file), IsWebContext.toNegated()), ContextKeyExpr.and(ResourceContextKey.Scheme.notEqualsTo(Schemas.file), ExplorerFolderContext.toNegated(), ExplorerRootContext.toNegated())) + when: ContextKeyExpr.or( + // native: for any remote resource + ContextKeyExpr.and(IsWebContext.toNegated(), ResourceContextKey.Scheme.notEqualsTo(Schemas.file)), + // web: for any files + ContextKeyExpr.and(IsWebContext, ExplorerFolderContext.toNegated(), ExplorerRootContext.toNegated()), + // web: for any folders if file system API support is provided + ContextKeyExpr.and(IsWebContext, HasWebFileSystemAccess) + ) })); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index b5437e7859a..f91217259bd 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -11,10 +11,10 @@ import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Action } from 'vs/base/common/actions'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { VIEWLET_ID, IExplorerService, IFilesConfiguration, VIEW_ID } from 'vs/workbench/contrib/files/common/files'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { BinarySize, IFileService, IFileStatWithMetadata, IFileStreamContent } from 'vs/platform/files/common/files'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IQuickInputService, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; @@ -39,7 +39,7 @@ import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/e import { coalesce } from 'vs/base/common/arrays'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { getErrorMessage } from 'vs/base/common/errors'; -import { triggerDownload } from 'vs/base/browser/dom'; +import { WebFileSystemAccess, triggerDownload } from 'vs/base/browser/dom'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; @@ -49,6 +49,9 @@ import { once } from 'vs/base/common/functional'; import { Codicon } from 'vs/base/common/codicons'; import { IViewsService } from 'vs/workbench/common/views'; import { trim, rtrim } from 'vs/base/common/strings'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { ILogService } from 'vs/platform/log/common/log'; export const NEW_FILE_COMMAND_ID = 'explorer.newFile'; export const NEW_FILE_LABEL = nls.localize('newFile', "New File"); @@ -66,7 +69,7 @@ export const PASTE_FILE_LABEL = nls.localize('pasteFile', "Paste"); export const FileCopiedContext = new RawContextKey('fileCopied', false); -export const DOWNLOAD_LABEL = nls.localize('download', "Download"); +export const DOWNLOAD_LABEL = nls.localize('download', "Download..."); const CONFIRM_DELETE_SETTING_KEY = 'explorer.confirmDelete'; @@ -997,49 +1000,212 @@ export const cutFileHandler = async (accessor: ServicesAccessor) => { export const DOWNLOAD_COMMAND_ID = 'explorer.download'; const downloadFileHandler = (accessor: ServicesAccessor) => { + const logService = accessor.get(ILogService); const fileService = accessor.get(IFileService); const workingCopyFileService = accessor.get(IWorkingCopyFileService); const fileDialogService = accessor.get(IFileDialogService); const explorerService = accessor.get(IExplorerService); - const stats = explorerService.getContext(true); + const progressService = accessor.get(IProgressService); - let canceled = false; - sequence(stats.map(s => async () => { - if (canceled) { - return; - } + const context = explorerService.getContext(true); + const explorerItems = context.length ? context : explorerService.roots; - if (isWeb) { - if (!s.isDirectory) { - let bufferOrUri: Uint8Array | URI; - try { - bufferOrUri = (await fileService.readFile(s.resource, { limits: { size: 1024 * 1024 /* set a limit to reduce memory pressure */ } })).value.buffer; - } catch (error) { - bufferOrUri = FileAccess.asBrowserUri(s.resource); + const cts = new CancellationTokenSource(); + + const downloadPromise = progressService.withProgress({ + location: ProgressLocation.Window, + delay: 800, + cancellable: isWeb, + title: nls.localize('downloadingFiles', "Downloading") + }, async progress => { + return sequence(explorerItems.map(explorerItem => async () => { + if (cts.token.isCancellationRequested) { + return; + } + + // Web: use DOM APIs to download files with optional support + // for folders and large files + if (isWeb) { + const stat = await fileService.resolve(explorerItem.resource, { resolveMetadata: true }); + + if (cts.token.isCancellationRequested) { + return; } - triggerDownload(bufferOrUri, s.name); - } - } else { - let defaultUri = s.isDirectory ? fileDialogService.defaultFolderPath(Schemas.file) : fileDialogService.defaultFilePath(Schemas.file); - if (defaultUri) { - defaultUri = resources.joinPath(defaultUri, s.name); + const maxBlobDownloadSize = 32 * BinarySize.MB; // avoid to download via blob-trick >32MB to avoid memory pressure + const preferFileSystemAccessWebApis = stat.isDirectory || stat.size > maxBlobDownloadSize; + + // Folder: use FS APIs to download files and folders if available and preferred + if (preferFileSystemAccessWebApis && WebFileSystemAccess.supported(window)) { + + interface IDownloadOperation { + startTime: number, + + filesTotal: number; + filesDownloaded: number; + + totalBytesDownloaded: 0 + fileBytesDownloaded: 0 + } + + async function pipeContents(name: string, source: IFileStreamContent, target: WebFileSystemAccess.FileSystemWritableFileStream, operation: IDownloadOperation): Promise { + return new Promise((resolve, reject) => { + const sourceStream = source.value; + + const disposables = new DisposableStore(); + disposables.add(toDisposable(() => target.close())); + + let disposed = false; + disposables.add(toDisposable(() => disposed = true)); + + disposables.add(once(cts.token.onCancellationRequested)(() => { + disposables.dispose(); + reject(); + })); + + sourceStream.on('data', data => { + if (!disposed) { + target.write(data.buffer); + reportProgress(name, source.size, data.byteLength, operation); + } + }); + + sourceStream.on('error', error => { + disposables.dispose(); + reject(error); + }); + + sourceStream.on('end', () => { + disposables.dispose(); + resolve(); + }); + }); + } + + async function downloadFile(targetFolder: WebFileSystemAccess.FileSystemDirectoryHandle, name: string, resource: URI, operation: IDownloadOperation): Promise { + + // Report progress + operation.filesDownloaded++; + operation.fileBytesDownloaded = 0; // reset for this file + reportProgress(name, 0, 0, operation); + + // Start to download + const targetFile = await targetFolder.getFileHandle(name, { create: true }); + const targetFileWriter = await targetFile.createWritable(); + + return pipeContents(name, await fileService.readFileStream(resource), targetFileWriter, operation); + } + + async function downloadFolder(folder: IFileStatWithMetadata, targetFolder: WebFileSystemAccess.FileSystemDirectoryHandle, operation: IDownloadOperation): Promise { + if (folder.children) { + operation.filesTotal += (folder.children.map(child => child.isFile)).length; + + for (const child of folder.children) { + if (cts.token.isCancellationRequested) { + return; + } + + if (child.isFile) { + await downloadFile(targetFolder, child.name, child.resource, operation); + } else { + const childFolder = await targetFolder.getDirectoryHandle(child.name, { create: true }); + const resolvedChildFolder = await fileService.resolve(child.resource, { resolveMetadata: true }); + + await downloadFolder(resolvedChildFolder, childFolder, operation); + } + } + } + } + + function reportProgress(name: string, fileSize: number, bytesDownloaded: number, operation: IDownloadOperation): void { + operation.fileBytesDownloaded += bytesDownloaded; + operation.totalBytesDownloaded += bytesDownloaded; + + const bytesDownloadedPerSecond = operation.totalBytesDownloaded / ((Date.now() - operation.startTime) / 1000); + + // Small file + let message: string; + if (fileSize < BinarySize.MB) { + if (operation.filesTotal === 1) { + message = name; + } else { + message = nls.localize('downloadProgressSmallMany', "{0} of {1} files ({2}/s)", operation.filesDownloaded, operation.filesTotal, BinarySize.formatSize(bytesDownloadedPerSecond)); + } + } + + // Large file + else { + message = nls.localize('downloadProgressLarge', "{0} ({1} of {2}, {3}/s)", name, BinarySize.formatSize(operation.fileBytesDownloaded), BinarySize.formatSize(fileSize), BinarySize.formatSize(bytesDownloadedPerSecond)); + } + + progress.report({ message }); + } + + try { + const targetFolder: WebFileSystemAccess.FileSystemDirectoryHandle = await window.showDirectoryPicker(); + const operation: IDownloadOperation = { + startTime: Date.now(), + + filesTotal: stat.isDirectory ? 0 : 1, // folders increment filesTotal within downloadFolder method + filesDownloaded: 0, + + totalBytesDownloaded: 0, + fileBytesDownloaded: 0 + }; + + if (stat.isDirectory) { + await downloadFolder(stat, targetFolder, operation); + } else { + await downloadFile(targetFolder, stat.name, stat.resource, operation); + } + } catch (error) { + logService.trace(error); + cts.cancel(); // `showDirectoryPicker` will throw an error when the user cancels + } + } + + // File: use traditional download to circumvent browser limitations + else if (stat.isFile) { + let bufferOrUri: Uint8Array | URI; + try { + bufferOrUri = (await fileService.readFile(stat.resource, { limits: { size: maxBlobDownloadSize } })).value.buffer; + } catch (error) { + bufferOrUri = FileAccess.asBrowserUri(stat.resource); + } + + if (!cts.token.isCancellationRequested) { + triggerDownload(bufferOrUri, stat.name); + } + } } - const destination = await fileDialogService.showSaveDialog({ - availableFileSystems: [Schemas.file], - saveLabel: mnemonicButtonLabel(nls.localize('download', "Download")), - title: s.isDirectory ? nls.localize('downloadFolder', "Download Folder") : nls.localize('downloadFile', "Download File"), - defaultUri - }); - if (destination) { - await workingCopyFileService.copy([{ source: s.resource, target: destination }], { overwrite: true }); - } else { - // User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100 - canceled = true; + // Native: use working copy file service to get at the contents + else { + progress.report({ message: explorerItem.name }); + + let defaultUri = explorerItem.isDirectory ? fileDialogService.defaultFolderPath(Schemas.file) : fileDialogService.defaultFilePath(Schemas.file); + if (defaultUri) { + defaultUri = resources.joinPath(defaultUri, explorerItem.name); + } + + const destination = await fileDialogService.showSaveDialog({ + availableFileSystems: [Schemas.file], + saveLabel: mnemonicButtonLabel(nls.localize('downloadButton', "Download")), + title: explorerItem.isDirectory ? nls.localize('downloadFolder', "Download Folder") : nls.localize('downloadFile', "Download File"), + defaultUri + }); + + if (destination) { + await workingCopyFileService.copy([{ source: explorerItem.resource, target: destination }], { overwrite: true }); + } else { + cts.cancel(); // User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100 + } } - } - })); + })); + }, () => cts.dispose(true)); + + // Also indicate progress in the files view + progressService.withProgress({ location: VIEW_ID, delay: 800 }, () => downloadPromise); }; CommandsRegistry.registerCommand({ diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 4ed199508e9..01b3dbc05a5 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -1037,6 +1037,9 @@ export class FileDragAndDrop implements ITreeDragAndDrop { title: localize('uploadingFiles', "Uploading") }, async progress => { for (let entry of entries) { + if (cts.token.isCancellationRequested) { + break; + } // Confirm overwrite as needed if (target && entry.name && target.getChild(entry.name)) { @@ -1081,15 +1084,19 @@ export class FileDragAndDrop implements ITreeDragAndDrop { const bytesUploadedPerSecond = operation.bytesUploaded / ((Date.now() - operation.startTime) / 1000); + // Small file let message: string; - if (operation.filesTotal === 1 && entry.name) { - message = entry.name; - } else { - message = localize('uploadProgress', "{0} of {1} files ({2}/s)", operation.filesUploaded, operation.filesTotal, BinarySize.formatSize(bytesUploadedPerSecond)); + if (fileSize < BinarySize.MB) { + if (operation.filesTotal === 1) { + message = `${entry.name}`; + } else { + message = localize('uploadProgressSmallMany', "{0} of {1} files ({2}/s)", operation.filesUploaded, operation.filesTotal, BinarySize.formatSize(bytesUploadedPerSecond)); + } } - if (fileSize > BinarySize.MB) { - message = localize('uploadProgressDetail', "{0} ({1} of {2}, {3}/s)", message, BinarySize.formatSize(fileBytesUploaded), BinarySize.formatSize(fileSize), BinarySize.formatSize(bytesUploadedPerSecond)); + // Large file + else { + message = localize('uploadProgressLarge', "{0} ({1} of {2}, {3}/s)", entry.name, BinarySize.formatSize(fileBytesUploaded), BinarySize.formatSize(fileSize), BinarySize.formatSize(bytesUploadedPerSecond)); } progress.report({ message }); @@ -1140,7 +1147,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } else { done = true; // an empty array is a signal that all entries have been read } - } while (!done); + } while (!done && !token.isCancellationRequested); // Update operation total based on new counts operation.filesTotal += childEntries.length; From e1692b33b135cf9155b6602977965b77e18c972d Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 19 Oct 2020 10:59:36 +0200 Subject: [PATCH 015/212] Abort rebase command polish (#108919) * Abort rebase command polish --- extensions/git/package.json | 4 ++++ extensions/git/src/commands.ts | 6 +++++- extensions/git/src/repository.ts | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index dbb65d6c7c7..0e08b6a84dc 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -623,6 +623,10 @@ "command": "git.commitAllAmend", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, + { + "command": "git.rebaseAbort", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && gitRebaseInProgress" + }, { "command": "git.commitNoVerify", "when": "config.git.enabled && !git.missing && config.git.allowNoVerifyCommit && gitOpenRepositoryCount != 0" diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index fb66583c3d4..99068f285cc 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -2498,7 +2498,11 @@ export class CommandCenter { @command('git.rebaseAbort', { repository: true }) async rebaseAbort(repository: Repository): Promise { - await repository.rebaseAbort(); + if (repository.rebaseCommit) { + await repository.rebaseAbort(); + } else { + await window.showInformationMessage(localize('no rebase', "No rebase in progress.")); + } } private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 5250ab4920a..c9ac43f8960 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration } from 'vscode'; +import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands } from 'vscode'; import * as nls from 'vscode-nls'; import { Branch, Change, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, CommitOptions, BranchQuery } from './api/git'; import { AutoFetcher } from './autofetch'; @@ -642,6 +642,7 @@ export class Repository implements Disposable { } this._rebaseCommit = rebaseCommit; + commands.executeCommand('setContext', 'gitRebaseInProgress', !!this._rebaseCommit); } get rebaseCommit(): Commit | undefined { From 04799ce2f9641b66564e769f4539bb630efbc1ed Mon Sep 17 00:00:00 2001 From: Andre Weinand Date: Mon, 19 Oct 2020 12:51:34 +0200 Subject: [PATCH 016/212] fix typo; fixes #108339 --- src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts b/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts index 8b5ab068ee3..a2a177f6e94 100644 --- a/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts +++ b/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts @@ -41,7 +41,7 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { } onMessage(callback: (message: DebugProtocol.ProtocolMessage) => void): void { - if (this.eventCallback) { + if (this.messageCallback) { this._onError.fire(new Error(`attempt to set more than one 'Message' callback`)); } this.messageCallback = callback; From 0ea7e1ca9425b3814c9d0a3c0aa719daad92a53a Mon Sep 17 00:00:00 2001 From: isidor Date: Mon, 19 Oct 2020 13:54:56 +0200 Subject: [PATCH 017/212] debug issues assign to Andre --- .github/classifier.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/classifier.json b/.github/classifier.json index e483a011577..e4acc63c170 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -20,8 +20,8 @@ "context-keys": {"assign": []}, "css-less-scss": {"assign": ["aeschli"]}, "custom-editors": {"assign": ["mjbvz"]}, - "debug": {"assign": ["connor4312 "]}, - "debug-console": {"assign": ["connor4312 "]}, + "debug": {"assign": ["weinand"]}, + "debug-console": {"assign": ["weinand"]}, "dialogs": {"assign": ["sbatten"]}, "diff-editor": {"assign": []}, "dropdown": {"assign": []}, From 9a51f2450226dbcdc40aff47463cf42fc35081e2 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 19 Oct 2020 14:19:37 +0200 Subject: [PATCH 018/212] add message to assertion --- .../extensionRecommendationsService.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index 94fc4af4425..40a1185bb6d 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -448,9 +448,9 @@ suite('ExtensionRecommendationsService Test', () => { await testObject.activationPromise; const recommendations = testObject.getAllRecommendationsWithReason(); - assert.ok(recommendations['ms-python.python']); - assert.ok(!recommendations['mockpublisher2.mockextension2']); - assert.ok(!recommendations['ms-dotnettools.csharp']); + assert.ok(recommendations['ms-python.python'], 'ms-python.python extension shall exist'); + assert.ok(!recommendations['mockpublisher2.mockextension2'], 'mockpublisher2.mockextension2 extension shall not exist'); + assert.ok(!recommendations['ms-dotnettools.csharp'], 'ms-dotnettools.csharp extension shall not exist'); }); test('ExtensionRecommendationsService: Able to dynamically ignore/unignore global recommendations', async () => { From df696e3afc079cbee4aadbae18389c762530af80 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 19 Oct 2020 14:46:34 +0200 Subject: [PATCH 019/212] move assertions out of promise --- .../electron-browser/configurationService.test.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index 7692bb61260..57a0b059141 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -53,6 +53,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import product from 'vs/platform/product/common/product'; import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; +import { Event } from 'vs/base/common/event'; class TestWorkbenchEnvironmentService extends NativeWorkbenchEnvironmentService { @@ -1218,15 +1219,12 @@ suite('WorkspaceConfigurationService - Folder', () => { const workspaceSettingsResource = URI.file(path.join(workspaceDir, '.vscode', 'settings.json')); await fileService.writeFile(workspaceSettingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); - await new Promise(async (c) => { - const disposable = testObject.onDidChangeConfiguration(e => { - assert.ok(e.affectsConfiguration('configurationService.folder.testSetting')); - assert.equal(testObject.getValue('configurationService.folder.testSetting'), 'userValue'); - disposable.dispose(); - c(); - }); + const e = await new Promise(async (c) => { + Event.once(testObject.onDidChangeConfiguration)(c); await fileService.del(workspaceSettingsResource); }); + assert.ok(e.affectsConfiguration('configurationService.folder.testSetting')); + assert.equal(testObject.getValue('configurationService.folder.testSetting'), 'userValue'); }); }); From fff9302b367acf12ef7e183fe8b03708b5476e2e Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 19 Oct 2020 15:59:47 +0200 Subject: [PATCH 020/212] folding: provider event to signal that folding ranges have changed. Fixes #99914 --- src/vs/editor/common/modes.ts | 6 +++++ src/vs/editor/contrib/folding/folding.ts | 2 +- .../contrib/folding/syntaxRangeProvider.ts | 15 ++++++++++-- .../contrib/folding/test/syntaxFold.test.ts | 2 +- src/vs/monaco.d.ts | 4 ++++ src/vs/vscode.proposed.d.ts | 11 +++++++++ .../api/browser/mainThreadLanguageFeatures.ts | 24 +++++++++++++++---- .../workbench/api/common/extHost.protocol.ts | 3 ++- .../api/common/extHostLanguageFeatures.ts | 18 ++++++++++---- 9 files changed, 71 insertions(+), 14 deletions(-) diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index faad9ab6da5..34f041ea626 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1293,6 +1293,12 @@ export interface FoldingContext { * A provider of folding ranges for editor models. */ export interface FoldingRangeProvider { + + /** + * An optional event to signal that the folding ranges from this provider have changed. + */ + onDidChange?: Event; + /** * Provides the folding ranges for a specific model. */ diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index 251f7aa1607..0bfa5f68166 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -264,7 +264,7 @@ export class FoldingController extends Disposable implements IEditorContribution }, 30000); return rangeProvider; // keep memento in case there are still no foldingProviders on the next request. } else if (foldingProviders.length > 0) { - this.rangeProvider = new SyntaxRangeProvider(editorModel, foldingProviders); + this.rangeProvider = new SyntaxRangeProvider(editorModel, foldingProviders, () => this.onModelContentChanged()); } } this.foldingStateMemento = null; diff --git a/src/vs/editor/contrib/folding/syntaxRangeProvider.ts b/src/vs/editor/contrib/folding/syntaxRangeProvider.ts index 4e66801be36..898694bb2f8 100644 --- a/src/vs/editor/contrib/folding/syntaxRangeProvider.ts +++ b/src/vs/editor/contrib/folding/syntaxRangeProvider.ts @@ -9,6 +9,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { RangeProvider } from './folding'; import { MAX_LINE_NUMBER, FoldingRegions } from './foldingRanges'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; const MAX_FOLDING_REGIONS = 5000; @@ -25,7 +26,17 @@ export class SyntaxRangeProvider implements RangeProvider { readonly id = ID_SYNTAX_PROVIDER; - constructor(private readonly editorModel: ITextModel, private providers: FoldingRangeProvider[], private limit = MAX_FOLDING_REGIONS) { + readonly disposables: DisposableStore | undefined; + + constructor(private readonly editorModel: ITextModel, private providers: FoldingRangeProvider[], handleFoldingRangesChange: () => void, private limit = MAX_FOLDING_REGIONS) { + for (const provider of providers) { + if (typeof provider.onDidChange === 'function') { + if (!this.disposables) { + this.disposables = new DisposableStore(); + } + this.disposables.add(provider.onDidChange(handleFoldingRangesChange)); + } + } } compute(cancellationToken: CancellationToken): Promise { @@ -39,8 +50,8 @@ export class SyntaxRangeProvider implements RangeProvider { } dispose() { + this.disposables?.dispose(); } - } function collectSyntaxRanges(providers: FoldingRangeProvider[], model: ITextModel, cancellationToken: CancellationToken): Promise { diff --git a/src/vs/editor/contrib/folding/test/syntaxFold.test.ts b/src/vs/editor/contrib/folding/test/syntaxFold.test.ts index 86a4280259e..915ba173661 100644 --- a/src/vs/editor/contrib/folding/test/syntaxFold.test.ts +++ b/src/vs/editor/contrib/folding/test/syntaxFold.test.ts @@ -74,7 +74,7 @@ suite('Syntax folding', () => { let providers = [new TestFoldingRangeProvider(model, ranges)]; async function assertLimit(maxEntries: number, expectedRanges: IndentRange[], message: string) { - let indentRanges = await new SyntaxRangeProvider(model, providers, maxEntries).compute(CancellationToken.None); + let indentRanges = await new SyntaxRangeProvider(model, providers, () => { }, maxEntries).compute(CancellationToken.None); let actual: IndentRange[] = []; if (indentRanges) { for (let i = 0; i < indentRanges.length; i++) { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index a44cb3bb307..1cd72bf8512 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -6137,6 +6137,10 @@ declare namespace monaco.languages { * A provider of folding ranges for editor models. */ export interface FoldingRangeProvider { + /** + * An optional event to signal that the folding ranges from this provider have changed. + */ + onDidChange?: IEvent; /** * Provides the folding ranges for a specific model. */ diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 5a3de044162..63712708d62 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2190,4 +2190,15 @@ declare module 'vscode' { canReply: boolean; } //#endregion + + //#region https://github.com/microsoft/vscode/issues/108929 FoldingRangeProvider.onDidChangeFoldingRanges @aeschli + export interface FoldingRangeProvider2 extends FoldingRangeProvider { + + /** + * An optional event to signal that the folding ranges from this provider have changed. + */ + onDidChangeFoldingRanges?: Event; + + } + //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 635730ba75b..9cbb21d567f 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -571,13 +571,27 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- folding - $registerFoldingRangeProvider(handle: number, selector: IDocumentFilterDto[]): void { - const proxy = this._proxy; - this._registrations.set(handle, modes.FoldingRangeProviderRegistry.register(selector, { + $registerFoldingRangeProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void { + const provider = { provideFoldingRanges: (model, context, token) => { - return proxy.$provideFoldingRanges(handle, model.uri, context, token); + return this._proxy.$provideFoldingRanges(handle, model.uri, context, token); } - })); + }; + + if (typeof eventHandle === 'number') { + const emitter = new Emitter(); + this._registrations.set(eventHandle, emitter); + provider.onDidChange = emitter.event; + } + + this._registrations.set(handle, modes.FoldingRangeProviderRegistry.register(selector, provider)); + } + + $emitFoldingRangeEvent(eventHandle: number, event?: any): void { + const obj = this._registrations.get(eventHandle); + if (obj instanceof Emitter) { + obj.fire(event); + } } // -- smart select diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index ddca6becf25..dd64b66a1ac 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -398,7 +398,8 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; $registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void; $registerDocumentColorProvider(handle: number, selector: IDocumentFilterDto[]): void; - $registerFoldingRangeProvider(handle: number, selector: IDocumentFilterDto[]): void; + $registerFoldingRangeProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; + $emitFoldingRangeEvent(eventHandle: number, event?: any): void; $registerSelectionRangeProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerCallHierarchyProvider(handle: number, selector: IDocumentFilterDto[]): void; $setLanguageConfiguration(handle: number, languageId: string, configuration: ILanguageConfigurationDto): void; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index ef49bb42954..8e65422a946 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1810,10 +1810,20 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, ColorProviderAdapter, adapter => adapter.provideColorPresentations(URI.revive(resource), colorInfo, token), undefined); } - registerFoldingRangeProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.FoldingRangeProvider): vscode.Disposable { - const handle = this._addNewAdapter(new FoldingProviderAdapter(this._documents, provider), extension); - this._proxy.$registerFoldingRangeProvider(handle, this._transformDocumentSelector(selector)); - return this._createDisposable(handle); + registerFoldingRangeProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.FoldingRangeProvider2): vscode.Disposable { + const handle = this._nextHandle(); + const eventHandle = typeof provider.onDidChangeFoldingRanges === 'function' ? this._nextHandle() : undefined; + + this._adapter.set(handle, new AdapterData(new FoldingProviderAdapter(this._documents, provider), extension)); + this._proxy.$registerFoldingRangeProvider(handle, this._transformDocumentSelector(selector), eventHandle); + let result = this._createDisposable(handle); + + if (eventHandle !== undefined) { + const subscription = provider.onDidChangeFoldingRanges!(_ => this._proxy.$emitFoldingRangeEvent(eventHandle)); + result = Disposable.from(result, subscription); + } + + return result; } $provideFoldingRanges(handle: number, resource: UriComponents, context: vscode.FoldingContext, token: CancellationToken): Promise { From af993f9c0a63c9af595dce38c83cdb496e0a14f2 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 19 Oct 2020 16:33:28 +0200 Subject: [PATCH 021/212] #108548 do not apply padding for output --- src/vs/workbench/contrib/output/browser/outputView.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 262c391623d..460b5e5a564 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -207,6 +207,7 @@ export class OutputEditor extends AbstractTextResourceEditor { options.renderLineHighlight = 'none'; options.minimap = { enabled: false }; options.renderValidationDecorations = 'editable'; + options.padding = undefined; const outputConfig = this.configurationService.getValue('[Log]'); if (outputConfig) { From b233cec28384ee40a20de8f5564d8e3c3db055e7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 19 Oct 2020 17:28:22 +0200 Subject: [PATCH 022/212] fix bad quick access when tabs disabled (#107543) (#108736) --- .../browser/parts/editor/noTabsTitleControl.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 49944a9ae70..66a7caec173 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -9,7 +9,7 @@ import { TitleControl, IToolbarActions } from 'vs/workbench/browser/parts/editor import { ResourceLabel, IResourceLabel } from 'vs/workbench/browser/labels'; import { TAB_ACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; -import { addDisposableListener, EventType, EventHelper, Dimension } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, EventHelper, Dimension, isAncestor } from 'vs/base/browser/dom'; import { IAction } from 'vs/base/common/actions'; import { CLOSE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { Color } from 'vs/base/common/color'; @@ -110,6 +110,16 @@ export class NoTabsTitleControl extends TitleControl { } private onTitleTap(e: GestureEvent): void { + + // We only want to open the quick access picker when + // the tap occured over the editor label, so we need + // to check on the target + // (https://github.com/microsoft/vscode/issues/107543) + const target = e.initialTarget; + if (!(target instanceof HTMLElement) || !this.editorLabel || !isAncestor(target, this.editorLabel.element)) { + return; + } + // TODO@rebornix gesture tap should open the quick access // editorGroupView will focus on the editor again when there are mouse/pointer/touch down events // we need to wait a bit as `GesureEvent.Tap` is generated from `touchstart` and then `touchend` evnets, which are not an atom event. From dbc90498b440183bb94394269e935930ed52989d Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Mon, 19 Oct 2020 17:49:54 +0200 Subject: [PATCH 023/212] Add hint (fixes microsoft/vscode-remote-release#486) --- .../configuration-editing/schemas/devContainer.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/configuration-editing/schemas/devContainer.schema.json b/extensions/configuration-editing/schemas/devContainer.schema.json index b37e07fa65c..1d3f7640ef1 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.json @@ -298,7 +298,7 @@ }, "workspaceFolder": { "type": "string", - "description": "The path of the workspace folder inside the container." + "description": "The path of the workspace folder inside the container. This is typically the target path of a volume mount in the docker-compose.yml." }, "shutdownAction": { "type": "string", From bfe35d4427220bb28022bd3c3b9d6f5e4fed99b9 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Mon, 19 Oct 2020 17:50:36 +0200 Subject: [PATCH 024/212] Settings copied only once (microsoft/vscode-remote-release#484) --- .../configuration-editing/schemas/attachContainer.schema.json | 2 +- .../configuration-editing/schemas/devContainer.schema.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/configuration-editing/schemas/attachContainer.schema.json b/extensions/configuration-editing/schemas/attachContainer.schema.json index 2b7952446f0..b408c46f42e 100644 --- a/extensions/configuration-editing/schemas/attachContainer.schema.json +++ b/extensions/configuration-editing/schemas/attachContainer.schema.json @@ -21,7 +21,7 @@ }, "settings": { "$ref": "vscode://schemas/settings/machine", - "description": "Machine specific settings that should be copied into the container." + "description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time." }, "remoteEnv": { "type": "object", diff --git a/extensions/configuration-editing/schemas/devContainer.schema.json b/extensions/configuration-editing/schemas/devContainer.schema.json index 1d3f7640ef1..4719d53e6db 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.json @@ -23,7 +23,7 @@ }, "settings": { "$ref": "vscode://schemas/settings/machine", - "description": "Machine specific settings that should be copied into the container." + "description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time, rebuilding the container then triggers it again." }, "forwardPorts": { "type": "array", From 2c53d687894305ce78bffd9080b6d27802c28d71 Mon Sep 17 00:00:00 2001 From: Kai Maetzel Date: Mon, 19 Oct 2020 17:22:06 +0000 Subject: [PATCH 025/212] fix GDPR annotation --- .../contrib/terminal/browser/terminalLatencyTelemetryAddon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalLatencyTelemetryAddon.ts b/src/vs/workbench/contrib/terminal/browser/terminalLatencyTelemetryAddon.ts index ba307010967..563f3f55251 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalLatencyTelemetryAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalLatencyTelemetryAddon.ts @@ -55,7 +55,7 @@ export class LatencyTelemetryAddon extends Disposable implements ITerminalAddon "min" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "max" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "median" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true } } */ const median = this._unprocessedLatencies.sort()[Math.floor(this._unprocessedLatencies.length / 2)]; From 0c50806e8ceaeae401b8bc7bc13fe7fb7f9401b0 Mon Sep 17 00:00:00 2001 From: Raymond Zhao Date: Mon, 19 Oct 2020 10:55:41 -0700 Subject: [PATCH 026/212] Refactor Emmet abbrevationAction files --- extensions/emmet/src/abbreviationActions.ts | 42 +++---- .../emmet/src/test/abbreviationAction.test.ts | 108 +++++++++--------- 2 files changed, 75 insertions(+), 75 deletions(-) diff --git a/extensions/emmet/src/abbreviationActions.ts b/extensions/emmet/src/abbreviationActions.ts index 6a2734cb9fa..9badf78f4f9 100644 --- a/extensions/emmet/src/abbreviationActions.ts +++ b/extensions/emmet/src/abbreviationActions.ts @@ -66,7 +66,7 @@ function doWrapping(individualLines: boolean, args: any) { const helper = getEmmetHelper(); // Fetch general information for the succesive expansions. i.e. the ranges to replace and its contents - let rangesToReplace: PreviewRangesWithContent[] = editor.selections.sort((a: vscode.Selection, b: vscode.Selection) => { return a.start.compareTo(b.start); }).map(selection => { + const rangesToReplace: PreviewRangesWithContent[] = editor.selections.sort((a: vscode.Selection, b: vscode.Selection) => { return a.start.compareTo(b.start); }).map(selection => { let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection; if (!rangeToReplace.isSingleLine && rangeToReplace.end.character === 0) { const previousLine = rangeToReplace.end.line - 1; @@ -88,7 +88,7 @@ function doWrapping(individualLines: boolean, args: any) { rangeToReplace = new vscode.Range(rangeToReplace.start.line, rangeToReplace.start.character + extraWhitespaceSelected, rangeToReplace.end.line, rangeToReplace.end.character); let textToWrapInPreview: string[]; - let textToReplace = editor.document.getText(rangeToReplace); + const textToReplace = editor.document.getText(rangeToReplace); if (individualLines) { textToWrapInPreview = textToReplace.split('\n').map(x => x.trim()); } else { @@ -144,7 +144,7 @@ function doWrapping(individualLines: boolean, args: any) { const oldPreviewLines = oldPreviewRange.end.line - oldPreviewRange.start.line + 1; const newLinesInserted = expandedTextLines.length - oldPreviewLines; - let newPreviewLineStart = oldPreviewRange.start.line + totalLinesInserted; + const newPreviewLineStart = oldPreviewRange.start.line + totalLinesInserted; let newPreviewStart = oldPreviewRange.start.character; const newPreviewLineEnd = oldPreviewRange.end.line + totalLinesInserted + newLinesInserted; let newPreviewEnd = expandedTextLines[expandedTextLines.length - 1].length; @@ -177,19 +177,19 @@ function doWrapping(individualLines: boolean, args: any) { return inPreview ? revertPreview().then(() => { return false; }) : Promise.resolve(inPreview); } - let extractedResults = helper.extractAbbreviationFromText(inputAbbreviation); + const extractedResults = helper.extractAbbreviationFromText(inputAbbreviation); if (!extractedResults) { return Promise.resolve(inPreview); } else if (extractedResults.abbreviation !== inputAbbreviation) { // Not clear what should we do in this case. Warn the user? How? } - let { abbreviation, filter } = extractedResults; + const { abbreviation, filter } = extractedResults; if (definitive) { const revertPromise = inPreview ? revertPreview() : Promise.resolve(); return revertPromise.then(() => { const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => { - let rangeToReplace = rangesAndContent.originalRange; + const rangeToReplace = rangesAndContent.originalRange; let textToWrap: string[]; if (individualLines) { textToWrap = rangesAndContent.textToWrapInPreview; @@ -270,17 +270,17 @@ export function expandEmmetAbbreviation(args: any): Thenable { + const getAbbreviation = (document: vscode.TextDocument, selection: vscode.Selection, position: vscode.Position, syntax: string): [vscode.Range | null, string, string] => { position = document.validatePosition(position); let rangeToReplace: vscode.Range = selection; let abbr = document.getText(rangeToReplace); if (!rangeToReplace.isEmpty) { - let extractedResults = helper.extractAbbreviationFromText(abbr); + const extractedResults = helper.extractAbbreviationFromText(abbr); if (extractedResults) { return [rangeToReplace, extractedResults.abbreviation, extractedResults.filter]; } @@ -293,23 +293,23 @@ export function expandEmmetAbbreviation(args: any): Thenable explicitly // else we will end up with <
if (syntax === 'html') { - let matches = textTillPosition.match(/<(\w+)$/); + const matches = textTillPosition.match(/<(\w+)$/); if (matches) { abbr = matches[1]; rangeToReplace = new vscode.Range(position.translate(0, -(abbr.length + 1)), position); return [rangeToReplace, abbr, '']; } } - let extractedResults = helper.extractAbbreviation(toLSTextDocument(editor.document), position, false); + const extractedResults = helper.extractAbbreviation(toLSTextDocument(editor.document), position, false); if (!extractedResults) { return [null, '', '']; } - let { abbreviationRange, abbreviation, filter } = extractedResults; + const { abbreviationRange, abbreviation, filter } = extractedResults; return [new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character), abbreviation, filter]; }; - let selectionsInReverseOrder = editor.selections.slice(0); + const selectionsInReverseOrder = editor.selections.slice(0); selectionsInReverseOrder.sort((a, b) => { const posA = a.isReversed ? a.anchor : a.active; const posB = b.isReversed ? b.anchor : b.active; @@ -322,7 +322,7 @@ export function expandEmmetAbbreviation(args: any): Thenable 1000) { rootNode = parsePartialStylesheet(editor.document, editor.selection.isReversed ? editor.selection.anchor : editor.selection.active); } else { @@ -333,8 +333,8 @@ export function expandEmmetAbbreviation(args: any): Thenable { - let position = selection.isReversed ? selection.anchor : selection.active; - let [rangeToReplace, abbreviation, filter] = getAbbreviation(editor.document, selection, position, syntax); + const position = selection.isReversed ? selection.anchor : selection.active; + const [rangeToReplace, abbreviation, filter] = getAbbreviation(editor.document, selection, position, syntax); if (!rangeToReplace) { return; } @@ -578,7 +578,7 @@ function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: Ex // Snippet to replace at multiple cursors are not the same // `editor.insertSnippet` will have to be called for each instance separately // We will not be able to maintain multiple cursors after snippet insertion - let insertPromises: Thenable[] = []; + const insertPromises: Thenable[] = []; if (!insertSameSnippet) { expandAbbrList.sort((a: ExpandAbbreviationInput, b: ExpandAbbreviationInput) => { return b.rangeToReplace.start.compareTo(a.rangeToReplace.start); }).forEach((expandAbbrInput: ExpandAbbreviationInput) => { let expandedText = expandAbbr(expandAbbrInput); @@ -596,8 +596,8 @@ function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: Ex // We can pass all ranges to `editor.insertSnippet` in a single call so that // all cursors are maintained after snippet insertion const anyExpandAbbrInput = expandAbbrList[0]; - let expandedText = expandAbbr(anyExpandAbbrInput); - let allRanges = expandAbbrList.map(value => { + const expandedText = expandAbbr(anyExpandAbbrInput); + const allRanges = expandAbbrList.map(value => { return new vscode.Range(value.rangeToReplace.start.line, value.rangeToReplace.start.character, value.rangeToReplace.end.line, value.rangeToReplace.end.character); }); if (expandedText) { @@ -614,7 +614,7 @@ function walk(root: any, fn: ((node: any) => boolean)): boolean { let ctx = root; while (ctx) { - let next = ctx.next; + const next = ctx.next; if (fn(ctx) === false || walk(ctx.firstChild, fn) === false) { return false; } @@ -653,7 +653,7 @@ function expandAbbr(input: ExpandAbbreviationInput): string | undefined { // Expand the abbreviation if (input.textToWrap) { - let parsedAbbr = helper.parseAbbreviation(input.abbreviation, expandOptions); + const parsedAbbr = helper.parseAbbreviation(input.abbreviation, expandOptions); if (input.rangeToReplace.isSingleLine && input.textToWrap.length === 1) { // Fetch rightmost element in the parsed abbreviation (i.e the element that will contain the wrapped text). diff --git a/extensions/emmet/src/test/abbreviationAction.test.ts b/extensions/emmet/src/test/abbreviationAction.test.ts index cedf3244903..7577cd6a718 100644 --- a/extensions/emmet/src/test/abbreviationAction.test.ts +++ b/extensions/emmet/src/test/abbreviationAction.test.ts @@ -61,7 +61,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { return withRandomFileEditor('img', 'html', async (editor, _doc) => { editor.selection = new Selection(0, 3, 0, 3); await expandEmmetAbbreviation(null); - assert.equal(editor.document.getText(), '\"\"'); + assert.strictEqual(editor.document.getText(), '\"\"'); return Promise.resolve(); }); }); @@ -72,14 +72,14 @@ suite('Tests for Expand Abbreviations (HTML)', () => { const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); if (!completionPromise) { - assert.equal(!completionPromise, false, `Got unexpected undefined instead of a completion promise`); + assert.strictEqual(!completionPromise, false, `Got unexpected undefined instead of a completion promise`); return Promise.resolve(); } const completionList = await completionPromise; - assert.equal(completionList && completionList.items && completionList.items.length > 0, true); + assert.strictEqual(completionList && completionList.items && completionList.items.length > 0, true); if (completionList) { - assert.equal(completionList.items[0].label, 'img'); - assert.equal(((completionList.items[0].documentation) || '').replace(/\|/g, ''), '\"\"'); + assert.strictEqual(completionList.items[0].label, 'img'); + assert.strictEqual(((completionList.items[0].documentation) || '').replace(/\|/g, ''), '\"\"'); } return Promise.resolve(); }); @@ -161,7 +161,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => { editor.selection = new Selection(2, 4, 2, 4); await expandEmmetAbbreviation(null); - assert.equal(editor.document.getText(), htmlContents); + assert.strictEqual(editor.document.getText(), htmlContents); return Promise.resolve(); }); }); @@ -171,7 +171,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { editor.selection = new Selection(2, 4, 2, 4); const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); - assert.equal(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); + assert.strictEqual(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); return Promise.resolve(); }); }); @@ -180,7 +180,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => { editor.selection = new Selection(9, 8, 9, 8); await expandEmmetAbbreviation(null); - assert.equal(editor.document.getText(), htmlContents); + assert.strictEqual(editor.document.getText(), htmlContents); return Promise.resolve(); }); }); @@ -190,7 +190,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { editor.selection = new Selection(9, 8, 9, 8); const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); - assert.equal(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); + assert.strictEqual(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); return Promise.resolve(); }); }); @@ -200,7 +200,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { return withRandomFileEditor(fileContents, 'html', async (editor, _doc) => { editor.selection = new Selection(0, 6, 0, 6); await expandEmmetAbbreviation(null); - assert.equal(editor.document.getText(), fileContents); + assert.strictEqual(editor.document.getText(), fileContents); return Promise.resolve(); }); }); @@ -211,7 +211,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { editor.selection = new Selection(0, 6, 0, 6); const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); - assert.equal(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); + assert.strictEqual(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); return Promise.resolve(); }); }); @@ -219,12 +219,12 @@ suite('Tests for Expand Abbreviations (HTML)', () => { test('Expand css when inside style tag (HTML)', () => { return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => { editor.selection = new Selection(13, 16, 13, 19); - let expandPromise = expandEmmetAbbreviation({ language: 'css' }); + const expandPromise = expandEmmetAbbreviation({ language: 'css' }); if (!expandPromise) { return Promise.resolve(); } await expandPromise; - assert.equal(editor.document.getText(), htmlContents.replace('m10', 'margin: 10px;')); + assert.strictEqual(editor.document.getText(), htmlContents.replace('m10', 'margin: 10px;')); return Promise.resolve(); }); }); @@ -238,19 +238,19 @@ suite('Tests for Expand Abbreviations (HTML)', () => { const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); if (!completionPromise) { - assert.equal(1, 2, `Problem with expanding m10`); + assert.strictEqual(1, 2, `Problem with expanding m10`); return Promise.resolve(); } const completionList = await completionPromise; if (!completionList || !completionList.items || !completionList.items.length) { - assert.equal(1, 2, `Problem with expanding m10`); + assert.strictEqual(1, 2, `Problem with expanding m10`); return Promise.resolve(); } const emmetCompletionItem = completionList.items[0]; - assert.equal(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`); - assert.equal(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); - assert.equal(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`); + assert.strictEqual(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`); + assert.strictEqual(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); + assert.strictEqual(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`); return Promise.resolve(); }); }); @@ -259,7 +259,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => { editor.selection = new Selection(13, 14, 13, 14); await expandEmmetAbbreviation(null); - assert.equal(editor.document.getText(), htmlContents); + assert.strictEqual(editor.document.getText(), htmlContents); return Promise.resolve(); }); }); @@ -268,12 +268,12 @@ suite('Tests for Expand Abbreviations (HTML)', () => { const styleAttributeContent = '
'; return withRandomFileEditor(styleAttributeContent, 'html', async (editor, _doc) => { editor.selection = new Selection(0, 15, 0, 15); - let expandPromise = expandEmmetAbbreviation(null); + const expandPromise = expandEmmetAbbreviation(null); if (!expandPromise) { return Promise.resolve(); } await expandPromise; - assert.equal(editor.document.getText(), styleAttributeContent.replace('m10', 'margin: 10px;')); + assert.strictEqual(editor.document.getText(), styleAttributeContent.replace('m10', 'margin: 10px;')); return Promise.resolve(); }); }); @@ -287,19 +287,19 @@ suite('Tests for Expand Abbreviations (HTML)', () => { const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); if (!completionPromise) { - assert.equal(1, 2, `Problem with expanding m10`); + assert.strictEqual(1, 2, `Problem with expanding m10`); return Promise.resolve(); } const completionList = await completionPromise; if (!completionList || !completionList.items || !completionList.items.length) { - assert.equal(1, 2, `Problem with expanding m10`); + assert.strictEqual(1, 2, `Problem with expanding m10`); return Promise.resolve(); } const emmetCompletionItem = completionList.items[0]; - assert.equal(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`); - assert.equal(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); - assert.equal(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`); + assert.strictEqual(emmetCompletionItem.label, expandedText, `Label of completion item doesnt match.`); + assert.strictEqual(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); + assert.strictEqual(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`); return Promise.resolve(); }); }); @@ -307,12 +307,12 @@ suite('Tests for Expand Abbreviations (HTML)', () => { test('Expand html when inside script tag with html type (HTML)', () => { return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => { editor.selection = new Selection(21, 12, 21, 12); - let expandPromise = expandEmmetAbbreviation(null); + const expandPromise = expandEmmetAbbreviation(null); if (!expandPromise) { return Promise.resolve(); } await expandPromise; - assert.equal(editor.document.getText(), htmlContents.replace('span.hello', '')); + assert.strictEqual(editor.document.getText(), htmlContents.replace('span.hello', '')); return Promise.resolve(); }); }); @@ -326,18 +326,18 @@ suite('Tests for Expand Abbreviations (HTML)', () => { const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); if (!completionPromise) { - assert.equal(1, 2, `Problem with expanding span.hello`); + assert.strictEqual(1, 2, `Problem with expanding span.hello`); return Promise.resolve(); } const completionList = await completionPromise; if (!completionList || !completionList.items || !completionList.items.length) { - assert.equal(1, 2, `Problem with expanding span.hello`); + assert.strictEqual(1, 2, `Problem with expanding span.hello`); return Promise.resolve(); } const emmetCompletionItem = completionList.items[0]; - assert.equal(emmetCompletionItem.label, abbreviation, `Label of completion item doesnt match.`); - assert.equal(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); + assert.strictEqual(emmetCompletionItem.label, abbreviation, `Label of completion item doesnt match.`); + assert.strictEqual(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); return Promise.resolve(); }); }); @@ -346,7 +346,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { return withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => { editor.selection = new Selection(24, 12, 24, 12); await expandEmmetAbbreviation(null); - assert.equal(editor.document.getText(), htmlContents); + assert.strictEqual(editor.document.getText(), htmlContents); return Promise.resolve(); }); }); @@ -356,7 +356,7 @@ suite('Tests for Expand Abbreviations (HTML)', () => { editor.selection = new Selection(24, 12, 24, 12); const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); - assert.equal(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); + assert.strictEqual(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); return Promise.resolve(); }); }); @@ -365,12 +365,12 @@ suite('Tests for Expand Abbreviations (HTML)', () => { await workspace.getConfiguration('emmet').update('includeLanguages', { 'javascript': 'html' }, ConfigurationTarget.Global); await withRandomFileEditor(htmlContents, 'html', async (editor, _doc) => { editor.selection = new Selection(24, 10, 24, 10); - let expandPromise = expandEmmetAbbreviation(null); + const expandPromise = expandEmmetAbbreviation(null); if (!expandPromise) { return Promise.resolve(); } await expandPromise; - assert.equal(editor.document.getText(), htmlContents.replace('span.bye', '')); + assert.strictEqual(editor.document.getText(), htmlContents.replace('span.bye', '')); }); return workspace.getConfiguration('emmet').update('includeLanguages', oldValueForInlcudeLanguages || {}, ConfigurationTarget.Global); }); @@ -384,17 +384,17 @@ suite('Tests for Expand Abbreviations (HTML)', () => { const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); if (!completionPromise) { - assert.equal(1, 2, `Problem with expanding span.bye`); + assert.strictEqual(1, 2, `Problem with expanding span.bye`); return Promise.resolve(); } const completionList = await completionPromise; if (!completionList || !completionList.items || !completionList.items.length) { - assert.equal(1, 2, `Problem with expanding span.bye`); + assert.strictEqual(1, 2, `Problem with expanding span.bye`); return Promise.resolve(); } const emmetCompletionItem = completionList.items[0]; - assert.equal(emmetCompletionItem.label, abbreviation, `Label of completion item (${emmetCompletionItem.label}) doesnt match.`); - assert.equal(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); + assert.strictEqual(emmetCompletionItem.label, abbreviation, `Label of completion item (${emmetCompletionItem.label}) doesnt match.`); + assert.strictEqual(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); return Promise.resolve(); }); return workspace.getConfiguration('emmet').update('includeLanguages', oldValueForInlcudeLanguages || {}, ConfigurationTarget.Global); @@ -433,7 +433,7 @@ suite('Tests for jsx, xml and xsl', () => { return withRandomFileEditor('ul.nav', 'javascriptreact', async (editor, _doc) => { editor.selection = new Selection(0, 6, 0, 6); await expandEmmetAbbreviation({ language: 'javascriptreact' }); - assert.equal(editor.document.getText(), '
    '); + assert.strictEqual(editor.document.getText(), '
      '); return Promise.resolve(); }); }); @@ -442,7 +442,7 @@ suite('Tests for jsx, xml and xsl', () => { return withRandomFileEditor('img', 'javascriptreact', async (editor, _doc) => { editor.selection = new Selection(0, 6, 0, 6); await expandEmmetAbbreviation({ language: 'javascriptreact' }); - assert.equal(editor.document.getText(), ''); + assert.strictEqual(editor.document.getText(), ''); return Promise.resolve(); }); }); @@ -452,7 +452,7 @@ suite('Tests for jsx, xml and xsl', () => { return withRandomFileEditor('img', 'javascriptreact', async (editor, _doc) => { editor.selection = new Selection(0, 6, 0, 6); await expandEmmetAbbreviation({ language: 'javascriptreact' }); - assert.equal(editor.document.getText(), '\'\'/'); + assert.strictEqual(editor.document.getText(), '\'\'/'); return workspace.getConfiguration('emmet').update('syntaxProfiles', oldValueForSyntaxProfiles ? oldValueForSyntaxProfiles.globalValue : undefined, ConfigurationTarget.Global); }); }); @@ -461,7 +461,7 @@ suite('Tests for jsx, xml and xsl', () => { return withRandomFileEditor('img', 'xml', async (editor, _doc) => { editor.selection = new Selection(0, 6, 0, 6); await expandEmmetAbbreviation({ language: 'xml' }); - assert.equal(editor.document.getText(), ''); + assert.strictEqual(editor.document.getText(), ''); return Promise.resolve(); }); }); @@ -470,7 +470,7 @@ suite('Tests for jsx, xml and xsl', () => { return withRandomFileEditor('img', 'html', async (editor, _doc) => { editor.selection = new Selection(0, 6, 0, 6); await expandEmmetAbbreviation({ language: 'html' }); - assert.equal(editor.document.getText(), ''); + assert.strictEqual(editor.document.getText(), ''); return Promise.resolve(); }); }); @@ -479,7 +479,7 @@ suite('Tests for jsx, xml and xsl', () => { return withRandomFileEditor('if (foo < 10) { span.bar', 'javascriptreact', async (editor, _doc) => { editor.selection = new Selection(0, 27, 0, 27); await expandEmmetAbbreviation({ language: 'javascriptreact' }); - assert.equal(editor.document.getText(), 'if (foo < 10) { '); + assert.strictEqual(editor.document.getText(), 'if (foo < 10) { '); return Promise.resolve(); }); }); @@ -505,15 +505,15 @@ suite('Tests for jsx, xml and xsl', () => { function testExpandAbbreviation(syntax: string, selection: Selection, abbreviation: string, expandedText: string, shouldFail?: boolean): Thenable { return withRandomFileEditor(htmlContents, syntax, async (editor, _doc) => { editor.selection = selection; - let expandPromise = expandEmmetAbbreviation(null); + const expandPromise = expandEmmetAbbreviation(null); if (!expandPromise) { if (!shouldFail) { - assert.equal(1, 2, `Problem with expanding ${abbreviation} to ${expandedText}`); + assert.strictEqual(1, 2, `Problem with expanding ${abbreviation} to ${expandedText}`); } return Promise.resolve(); } await expandPromise; - assert.equal(editor.document.getText(), htmlContents.replace(abbreviation, expandedText)); + assert.strictEqual(editor.document.getText(), htmlContents.replace(abbreviation, expandedText)); return Promise.resolve(); }); } @@ -525,7 +525,7 @@ function testHtmlCompletionProvider(selection: Selection, abbreviation: string, const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); if (!completionPromise) { if (!shouldFail) { - assert.equal(1, 2, `Problem with expanding ${abbreviation} to ${expandedText}`); + assert.strictEqual(1, 2, `Problem with expanding ${abbreviation} to ${expandedText}`); } return Promise.resolve(); } @@ -533,13 +533,13 @@ function testHtmlCompletionProvider(selection: Selection, abbreviation: string, const completionList = await completionPromise; if (!completionList || !completionList.items || !completionList.items.length) { if (!shouldFail) { - assert.equal(1, 2, `Problem with expanding ${abbreviation} to ${expandedText}`); + assert.strictEqual(1, 2, `Problem with expanding ${abbreviation} to ${expandedText}`); } return Promise.resolve(); } const emmetCompletionItem = completionList.items[0]; - assert.equal(emmetCompletionItem.label, abbreviation, `Label of completion item doesnt match.`); - assert.equal(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); + assert.strictEqual(emmetCompletionItem.label, abbreviation, `Label of completion item doesnt match.`); + assert.strictEqual(((emmetCompletionItem.documentation) || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); return Promise.resolve(); }); } @@ -549,7 +549,7 @@ function testNoCompletion(syntax: string, fileContents: string, selection: Selec editor.selection = selection; const cancelSrc = new CancellationTokenSource(); const completionPromise = completionProvider.provideCompletionItems(editor.document, editor.selection.active, cancelSrc.token, { triggerKind: CompletionTriggerKind.Invoke }); - assert.equal(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); + assert.strictEqual(!completionPromise, true, `Got unexpected comapletion promise instead of undefined`); return Promise.resolve(); }); } From d10e2fc0e6c109a383c028edf98432877b6e3cea Mon Sep 17 00:00:00 2001 From: Raymond Zhao Date: Mon, 19 Oct 2020 11:26:12 -0700 Subject: [PATCH 027/212] Use new helper extractAbbreviation function --- extensions/emmet/src/abbreviationActions.ts | 2 +- .../emmet/src/defaultCompletionProvider.ts | 5 ++- extensions/emmet/yarn.lock | 41 +++++++++++++++---- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/extensions/emmet/src/abbreviationActions.ts b/extensions/emmet/src/abbreviationActions.ts index 9badf78f4f9..16dc7b61d6d 100644 --- a/extensions/emmet/src/abbreviationActions.ts +++ b/extensions/emmet/src/abbreviationActions.ts @@ -300,7 +300,7 @@ export function expandEmmetAbbreviation(args: any): Thenable Date: Mon, 19 Oct 2020 11:47:07 -0700 Subject: [PATCH 028/212] Update deps to include libgbm (#107611) Fixes #106936 --- resources/linux/debian/control.template | 2 +- resources/linux/rpm/dependencies.json | 3 ++- resources/linux/snap/snapcraft.yaml | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/linux/debian/control.template b/resources/linux/debian/control.template index 903640cd503..5a6d7be652b 100644 --- a/resources/linux/debian/control.template +++ b/resources/linux/debian/control.template @@ -1,7 +1,7 @@ Package: @@NAME@@ Version: @@VERSION@@ Section: devel -Depends: libnss3 (>= 2:3.26), gnupg, apt, libxkbfile1, libsecret-1-0, libgtk-3-0 (>= 3.10.0), libxss1 +Depends: libnss3 (>= 2:3.26), gnupg, apt, libxkbfile1, libsecret-1-0, libgtk-3-0 (>= 3.10.0), libxss1, libgbm1 Priority: optional Architecture: @@ARCHITECTURE@@ Maintainer: Microsoft Corporation diff --git a/resources/linux/rpm/dependencies.json b/resources/linux/rpm/dependencies.json index 07e2b307fcd..7f95cd3e5db 100644 --- a/resources/linux/rpm/dependencies.json +++ b/resources/linux/rpm/dependencies.json @@ -62,7 +62,8 @@ "libc.so.6(GLIBC_2.9)(64bit)", "libxcb.so.1()(64bit)", "libxkbfile.so.1()(64bit)", - "libsecret-1.so.0()(64bit)" + "libsecret-1.so.0()(64bit)", + "libgbm.so.1()(64bit)" ], "aarch64": [ "libpthread.so.0()(aarch64)", diff --git a/resources/linux/snap/snapcraft.yaml b/resources/linux/snap/snapcraft.yaml index 7dbc1680ba2..046158e888e 100644 --- a/resources/linux/snap/snapcraft.yaml +++ b/resources/linux/snap/snapcraft.yaml @@ -31,6 +31,7 @@ parts: - libgconf-2-4 - libglib2.0-bin - libgnome-keyring0 + - libgbm1 - libgtk-3-0 - libnotify4 - libnspr4 From c4d7a5b362b63a9cc7698417c3e4e60301dd2fe9 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 19 Oct 2020 12:23:40 -0700 Subject: [PATCH 029/212] polish --- .../browser/terminalTypeAheadAddon.ts | 329 +++++++++++++++--- 1 file changed, 272 insertions(+), 57 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts index ed234e30dee..62c39bf2830 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts @@ -12,8 +12,18 @@ const CSI = '\x1b['; const SHOW_CURSOR = `${CSI}?25h`; const HIDE_CURSOR = `${CSI}?25l`; const DELETE_CHAR = `${CSI}X`; -const CSI_STYLE_RE = /^\x1b\[[0-9;]+m/; -const CSI_MOVE_RE = /^\x1b\[([0-9]*)([DC])/; +const CSI_STYLE_RE = /^\x1b\[[0-9;]*m/; +const CSI_MOVE_RE = /^\x1b\[([0-9]*)(;5)?([DC])/; +const PASSWORD_INPUT_RE = /(password|passphrase|passwd).*: /i; +const WHITESPACE_RE = /\s/; + +/** + * Codes that should be omitted from sending to the prediction engine and + * insted omitted directly: + * - cursor hide/show + * - mode set/reset + */ +const PREDICTION_OMIT_RE = /^(\x1b\[\??25[hl])+/; const enum CursorMoveDirection { Back = 'D', @@ -31,6 +41,46 @@ interface ICoordinate { const getCellAtCoordinate = (b: IBuffer, c: ICoordinate) => b.getLine(c.y + c.baseY)?.getCell(c.x); +const moveToWordBoundary = (b: IBuffer, cursor: ICoordinate, direction: -1 | 1) => { + let ateLeadingWhitespace = false; + if (direction < 0) { + cursor.x--; + } + + while (cursor.x >= 0) { + const cell = getCellAtCoordinate(b, cursor); + if (!cell?.getCode()) { + return; + } + + const chars = cell.getChars(); + if (WHITESPACE_RE.test(chars)) { + if (ateLeadingWhitespace) { + break; + } + } else { + ateLeadingWhitespace = true; + } + + cursor.x += direction; + } + + if (direction < 0) { + cursor.x++; // we want to place the cursor after the whitespace starting the word + } + + cursor.x = Math.max(0, cursor.x); +}; + +const enum MatchResult { + /** matched successfully */ + Success, + /** failed to match */ + Failure, + /** buffer data, it might match in the future one more data comes in */ + Buffer, +} + interface IPrediction { /** * Returns a sequence to apply the prediction. @@ -54,10 +104,9 @@ interface IPrediction { /** * Returns whether the given input is one expected by this prediction. */ - matches(input: StringReader): boolean; + matches(input: StringReader): MatchResult; } - class StringReader { public index = 0; @@ -65,10 +114,33 @@ class StringReader { return this.input.length - this.index; } + public get eof() { + return this.index === this.input.length; + } + + public get rest() { + return this.input.slice(this.index); + } + constructor(private readonly input: string) { } + /** + * Advances the reader and returns the character if it matches. + */ + public eatChar(char: string) { + if (this.input[this.index] !== char) { + return; + } + + this.index++; + return char; + } + + /** + * Advances the reader and returns the string if it matches. + */ public eatStr(substr: string) { - if (this.input.slice(this.index, this.index + substr.length) !== substr) { + if (this.input.slice(this.index, substr.length) !== substr) { return; } @@ -76,6 +148,30 @@ class StringReader { return substr; } + /** + * Matches and eats the substring character-by-character. If EOF is reached + * before the substring is consumed, it will buffer. Index is not moved + * if it's not a match. + */ + public eatGradually(substr: string): MatchResult { + let prevIndex = this.index; + for (const char of substr) { + if (!this.eatChar(char)) { + this.index = prevIndex; + return MatchResult.Failure; + } + + if (this.eof) { + return MatchResult.Buffer; + } + } + + return MatchResult.Success; + } + + /** + * Advances the reader and returns the regex if it matches. + */ public eatRe(re: RegExp) { const match = re.exec(this.input.slice(this.index)); if (!match) { @@ -86,7 +182,10 @@ class StringReader { return match; } - public eatCharCode(min = 0, max = Infinity) { + /** + * Advances the reader and returns the character if the code matches. + */ + public eatCharCode(min = 0, max = min + 1) { const code = this.input.charCodeAt(this.index); if (code < min || code >= max) { return undefined; @@ -95,14 +194,11 @@ class StringReader { this.index++; return code; } - - public rest() { - return this.input.slice(this.index); - } } /** - * Boundary which never tests true. Will always discard predictions. + * Preidction which never tests true. Will always discard predictions made + * after it. */ class HardBoundary implements IPrediction { public apply() { @@ -114,15 +210,15 @@ class HardBoundary implements IPrediction { } public matches() { - return false; + return MatchResult.Failure; } } +/** + * Prediction for a single alphanumeric character. + */ class CharacterPrediction implements IPrediction { - protected appliedAt?: { - x: number; - y: number; - baseY: number; + protected appliedAt?: ICoordinate & { oldAttributes: string; oldChar: string; }; @@ -154,16 +250,25 @@ class CharacterPrediction implements IPrediction { // remove any styling CSI before checking the char while (input.eatRe(CSI_STYLE_RE)) { } - if (input.eatStr(this.char)) { - return true; + + if (input.eof) { + return MatchResult.Buffer; + } + + if (input.eatChar(this.char)) { + return MatchResult.Success; } input.index = startIndex; - return false; + return MatchResult.Failure; } } class BackspacePrediction extends CharacterPrediction { + constructor() { + super('', '\b'); + } + public apply(buffer: IBuffer, cursor: ICoordinate) { const cell = getCellAtCoordinate(buffer, cursor); this.appliedAt = cell @@ -175,7 +280,15 @@ class BackspacePrediction extends CharacterPrediction { } public matches(input: StringReader) { - return !!input.eatStr('\b'); + // if at end of line, allow backspace + clear line. Zsh does this. + if (this.appliedAt?.oldChar === '') { + const r = input.eatGradually(`\b${CSI}K`); + if (r !== MatchResult.Failure) { + return r; + } + } + + return input.eatChar('\b') ? MatchResult.Success : MatchResult.Failure; } } @@ -204,22 +317,48 @@ class NewlinePrediction implements IPrediction { } public matches(input: StringReader) { - return !!input.eatStr('\r\n'); + return input.eatGradually('\r\n'); } } class CursorMovePrediction implements IPrediction { - constructor(private readonly direction: CursorMoveDirection, private readonly amount: number) { } + private applied?: { + rollForward: string; + rollBack: string; + amount: number; + }; - public apply(_: IBuffer, cursor: ICoordinate) { - const { amount, direction } = this; - cursor.x += (direction === CursorMoveDirection.Back ? -1 : 1) * amount; - return `${CSI}${amount}${direction}`; + constructor( + private readonly direction: CursorMoveDirection, + private readonly moveByWords: boolean, + private readonly amount: number, + ) { } + + public apply(buffer: IBuffer, cursor: ICoordinate) { + let rollBack = setCursorCoordinate(buffer, cursor); + const currentCell = getCellAtCoordinate(buffer, cursor); + if (currentCell) { + rollBack += getBufferCellAttributes(currentCell); + } + + const { amount, direction, moveByWords } = this; + const delta = direction === CursorMoveDirection.Back ? -1 : 1; + const startX = cursor.x; + if (moveByWords) { + for (let i = 0; i < amount; i++) { + moveToWordBoundary(buffer, cursor, delta); + } + } else { + cursor.x += delta * amount; + } + + const rollForward = setCursorCoordinate(buffer, cursor); + this.applied = { amount: Math.abs(cursor.x - startX), rollBack, rollForward }; + return this.applied.rollForward; } public rollback() { - const fn = this.direction === CursorMoveDirection.Back ? CursorMoveDirection.Forwards : CursorMoveDirection.Back; - return `${CSI}${this.amount}${fn}`; + return this.applied?.rollBack ?? ''; } public rollForwards() { @@ -227,16 +366,36 @@ class CursorMovePrediction implements IPrediction { } public matches(input: StringReader) { - const { amount, direction } = this; - if (amount === 1 && input.eatStr(`${CSI}${direction}`)) { - return true; + if (!this.applied) { + return MatchResult.Failure; } - if (amount === 1 && this.direction === CursorMoveDirection.Back && input.eatStr('\b')) { - return true; + const direction = this.direction; + const { amount, rollForward } = this.applied; + + if (amount === 1) { + // arg can be omitted to move one character + const r = input.eatGradually(`${CSI}${direction}`); + if (r !== MatchResult.Failure) { + return r; + } + + // \b is the equivalent to moving one character back + if (direction === CursorMoveDirection.Back && input.eatChar('\b')) { + return MatchResult.Success; + } } - return !!input.eatStr(`${CSI}${amount}${direction}`); + // check if the cursor position is set absolutely + if (rollForward) { + const r = input.eatGradually(rollForward); + if (r !== MatchResult.Failure) { + return r; + } + } + + // check for a relative move in the direction + return input.eatGradually(`${CSI}${amount}${direction}`); } } @@ -259,12 +418,23 @@ class PredictionTimeline { */ private cursor: ICoordinate | undefined; + /** + * Previously sent data that was buffered and should be prepended to the + * next input. + */ + private inputBuffer?: string; + constructor(public readonly terminal: Terminal) { } /** * Should be called when input is incoming to the temrinal. */ public beforeServerInput(input: string): string { + if (this.inputBuffer) { + input = this.inputBuffer + input; + this.inputBuffer = undefined; + } + if (!this.expected.length) { this.cursor = undefined; return input; @@ -280,31 +450,55 @@ class PredictionTimeline { const reader = new StringReader(input); const startingGen = this.expected[0].gen; - while (this.expected.length && reader.remaining > 0) { + const emitPredictionOmitted = () => { + const omit = reader.eatRe(PREDICTION_OMIT_RE); + if (omit) { + output += omit[0]; + } + }; + + ReadLoop: while (this.expected.length && reader.remaining > 0) { + emitPredictionOmitted(); + const prediction = this.expected[0].p; let beforeTestReaderIndex = reader.index; - - // if the input character matches what the next prediction expected, undo - // the prediction and write the real character out. - if (prediction.matches(reader)) { - const eaten = input.slice(beforeTestReaderIndex, reader.index); - output += prediction.rollForwards?.(buffer, eaten) - ?? (prediction.rollback(buffer) + input.slice(beforeTestReaderIndex, reader.index)); - this.expected.shift(); - } - // otherwise, roll back all pending predictions - else { - output += this.expected.filter(p => p.gen === startingGen) - .map(({ p }) => p.rollback(buffer)) - .reverse() - .join(''); - this.expected = []; - this.cursor = undefined; - break; + switch (prediction.matches(reader)) { + case MatchResult.Success: + // if the input character matches what the next prediction expected, undo + // the prediction and write the real character out. + const eaten = input.slice(beforeTestReaderIndex, reader.index); + output += prediction.rollForwards?.(buffer, eaten) + ?? (prediction.rollback(buffer) + input.slice(beforeTestReaderIndex, reader.index)); + this.expected.shift(); + break; + case MatchResult.Buffer: + // on a buffer, store the remaining data and completely read data + // to be output as normal. + this.inputBuffer = input.slice(beforeTestReaderIndex); + reader.index = input.length; + break ReadLoop; + case MatchResult.Failure: + // on a failure, roll back all remaining items in this generation + // and clear predictions, since they are no longer valid + output += this.expected.filter(p => p.gen === startingGen) + .map(({ p }) => p.rollback(buffer)) + .reverse() + .join(''); + this.expected = []; + this.cursor = undefined; + break ReadLoop; } } - output += reader.rest(); + emitPredictionOmitted(); + + // Extra data (like the result of running a command) should cause us to + // reset the cursor + if (!reader.eof) { + output += reader.rest; + this.expected = []; + this.cursor = undefined; + } // If we passed a generation boundary, apply the current generation's predictions if (this.expected.length && startingGen !== this.expected[0].gen) { @@ -317,6 +511,10 @@ class PredictionTimeline { } } + if (output.length === 0) { + return ''; + } + if (this.cursor) { output += setCursorCoordinate(buffer, this.cursor); } @@ -422,8 +620,8 @@ export class TypeAheadAddon implements ITerminalAddon { const buffer = terminal.buffer.active; const reader = new StringReader(data); while (reader.remaining > 0) { - if (reader.eatStr('\b')) { // backspace - this.timeline.addPrediction(buffer, new BackspacePrediction(this.typeheadStyle, '\b')); + if (reader.eatCharCode(127)) { // backspace + this.timeline.addPrediction(buffer, new BackspacePrediction()); continue; } @@ -440,7 +638,15 @@ export class TypeAheadAddon implements ITerminalAddon { const cursorMv = reader.eatRe(CSI_MOVE_RE); if (cursorMv) { this.timeline.addPrediction(buffer, new CursorMovePrediction( - cursorMv[2] as CursorMoveDirection, Number(cursorMv[1]) || 1)); + cursorMv[3] as CursorMoveDirection, + !!cursorMv[2], + Number(cursorMv[1]) || 1) + ); + continue; + } + + if (reader.eatChar('\r') && buffer.cursorY < terminal.rows - 1) { + this.timeline.addPrediction(buffer, new NewlinePrediction()); continue; } @@ -459,5 +665,14 @@ export class TypeAheadAddon implements ITerminalAddon { console.log('incoming data:', JSON.stringify(event.data)); event.data = this.timeline.beforeServerInput(event.data); console.log('emitted data:', JSON.stringify(event.data)); + + // If there's something that looks like a password prompt, omit giving + // input. This is approximate since there's no TTY "password here" code, + // but should be enough to cover common cases like sudo + if (PASSWORD_INPUT_RE.test(event.data)) { + const terminal = this.timeline.terminal; + this.timeline.addPrediction(terminal.buffer.active, new HardBoundary()); + this.timeline.addBoundary(); + } } } From 591c49a74b54e737bf4ac09a10ff9b502f81549d Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 19 Oct 2020 21:38:44 +0200 Subject: [PATCH 030/212] add dom.Dimension#with more modification, adopt class-vs-interface usage --- src/vs/base/browser/dom.ts | 16 ++++++++++++++++ src/vs/base/browser/ui/menu/menu.ts | 2 +- .../contrib/gotoSymbol/peek/referencesWidget.ts | 4 ++-- src/vs/workbench/browser/layout.ts | 2 +- src/vs/workbench/browser/parts/compositePart.ts | 2 +- .../workbench/browser/parts/editor/editorPart.ts | 2 +- .../browser/parts/editor/tabsTitleControl.ts | 2 +- .../parts/notifications/notificationsCenter.ts | 2 +- .../parts/notifications/notificationsToasts.ts | 2 +- .../callHierarchy/browser/callHierarchyPeek.ts | 4 ++-- .../extensions/browser/extensionsViewlet.ts | 2 +- .../contrib/output/browser/outputView.ts | 3 ++- .../preferences/browser/settingsEditor2.ts | 2 +- .../workbench/contrib/scm/browser/scmViewPane.ts | 5 +---- .../webviewView/browser/webviewViewPane.ts | 3 ++- 15 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 0c19deeb873..51a8f4cafac 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -500,6 +500,22 @@ export class Dimension implements IDimension { public readonly height: number, ) { } + with(width: number = this.width, height: number = this.height): Dimension { + if (width !== this.width || height !== this.height) { + return new Dimension(width, height); + } else { + return this; + } + } + + static lift(obj: IDimension): Dimension { + if (obj instanceof Dimension) { + return obj; + } else { + return new Dimension(obj.width, obj.height); + } + } + static equals(a: Dimension | undefined, b: Dimension | undefined): boolean { if (a === b) { return true; diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index e84d1e8b4d7..9dd4ff50457 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -868,7 +868,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { const viewBox = this.submenuContainer.getBoundingClientRect(); - const { top, left } = this.calculateSubmenuMenuLayout({ height: window.innerHeight, width: window.innerWidth }, viewBox, entryBoxUpdated, this.expandDirection); + const { top, left } = this.calculateSubmenuMenuLayout(new Dimension(window.innerWidth, window.innerHeight), Dimension.lift(viewBox), entryBoxUpdated, this.expandDirection); this.submenuContainer.style.left = `${left}px`; this.submenuContainer.style.top = `${top}px`; diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts index 7c21c932e17..ad343d031b4 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts @@ -209,7 +209,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { private _previewNotAvailableMessage!: TextModel; private _previewContainer!: HTMLElement; private _messageContainer!: HTMLElement; - private _dim: dom.Dimension = { height: 0, width: 0 }; + private _dim = new dom.Dimension(0, 0); constructor( editor: ICodeEditor, @@ -406,7 +406,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { protected _doLayoutBody(heightInPixel: number, widthInPixel: number): void { super._doLayoutBody(heightInPixel, widthInPixel); - this._dim = { height: heightInPixel, width: widthInPixel }; + this._dim = new dom.Dimension(widthInPixel, heightInPixel); this.layoutData.heightInLines = this._viewZone ? this._viewZone.heightInLines : this.layoutData.heightInLines; this._splitView.layout(widthInPixel); this._splitView.resizeView(0, widthInPixel * this.layoutData.ratio); diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 02e4ac93844..6618279096d 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -1095,7 +1095,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const availableWidth = this.dimension.width - takenWidth; const availableHeight = this.dimension.height - takenHeight; - return { width: availableWidth, height: availableHeight }; + return new Dimension(availableWidth, availableHeight); } getWorkbenchContainer(): HTMLElement { diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 84de9096251..ee234378560 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -483,7 +483,7 @@ export abstract class CompositePart extends Part { super.layout(width, height); // Layout contents - this.contentAreaSize = super.layoutContents(width, height).contentSize; + this.contentAreaSize = Dimension.lift(super.layoutContents(width, height).contentSize); // Layout composite if (this.activeComposite) { diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 8b577f47ef9..e6e216a5ed4 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -1075,7 +1075,7 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro const contentAreaSize = super.layoutContents(width, height).contentSize; // Layout editor container - this.doLayout(contentAreaSize); + this.doLayout(Dimension.lift(contentAreaSize)); } private doLayout(dimension: Dimension): void { diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 8a7695ce6bd..0c108f0c9d6 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -1283,7 +1283,7 @@ export class TabsTitleControl extends TitleControl { if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { const tabsScrollbar = assertIsDefined(this.tabsScrollbar); - this.breadcrumbsControl.layout({ width: dimension.width, height: BreadcrumbsControl.HEIGHT }); + this.breadcrumbsControl.layout(new Dimension(dimension.width, BreadcrumbsControl.HEIGHT)); tabsScrollbar.getDomNode().style.height = `${dimension.height - BreadcrumbsControl.HEIGHT}px`; } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index 7d7056fad8f..6e8b4e940f1 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -59,7 +59,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente private registerListeners(): void { this._register(this.model.onDidChangeNotification(e => this.onDidChangeNotification(e))); - this._register(this.layoutService.onLayout(dimension => this.layout(dimension))); + this._register(this.layoutService.onLayout(dimension => this.layout(Dimension.lift(dimension)))); } get isVisible(): boolean { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 3ed4c64890f..fc00d291436 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -90,7 +90,7 @@ export class NotificationsToasts extends Themable implements INotificationsToast private registerListeners(): void { // Layout - this._register(this.layoutService.onLayout(dimension => this.layout(dimension))); + this._register(this.layoutService.onLayout(dimension => this.layout(Dimension.lift(dimension)))); // Delay some tasks until after we can show notifications this.onCanShowNotifications().then(() => { diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts index e51a91dc96b..b6866acaaff 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts @@ -150,7 +150,7 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { protected _fillBody(parent: HTMLElement): void { this._layoutInfo = LayoutInfo.retrieve(this._storageService); - this._dim = { height: 0, width: 0 }; + this._dim = new Dimension(0, 0); this._parent = parent; parent.classList.add('call-hierarchy'); @@ -427,7 +427,7 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { protected _doLayoutBody(height: number, width: number): void { if (this._dim.height !== height || this._dim.width !== width) { super._doLayoutBody(height, width); - this._dim = { height, width }; + this._dim = new Dimension(width, height); this._layoutInfo.height = this._viewZone ? this._viewZone.heightInLines : this._layoutInfo.height; this._splitView.layout(width); this._splitView.resizeView(0, width * this._layoutInfo.ratio); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 13d923aefba..92f6e26bff7 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -499,7 +499,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.root.classList.toggle('narrow', dimension.width <= 300); } if (this.searchBox) { - this.searchBox.layout({ height: 20, width: dimension.width - 34 }); + this.searchBox.layout(new Dimension(dimension.width - 34, 20)); } super.layout(new Dimension(dimension.width, dimension.height - 41)); } diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 262c391623d..71baa088c19 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -36,6 +36,7 @@ import { groupBy } from 'vs/base/common/arrays'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { editorBackground, selectBorder } from 'vs/platform/theme/common/colorRegistry'; import { SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { Dimension } from 'vs/base/browser/dom'; export class OutputViewPane extends ViewPane { @@ -118,7 +119,7 @@ export class OutputViewPane extends ViewPane { layoutBody(height: number, width: number): void { super.layoutBody(height, width); - this.editor.layout({ height, width }); + this.editor.layout(new Dimension(width, height)); } getActionViewItem(action: IAction): IActionViewItem | undefined { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 068b01de440..3ca781e9eee 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -334,7 +334,7 @@ export class SettingsEditor2 extends EditorPane { const innerWidth = Math.min(1000, dimension.width) - 24 * 2; // 24px padding on left and right; // minus padding inside inputbox, countElement width, controls width, extra padding before countElement const monacoWidth = innerWidth - 10 - this.countElement.clientWidth - this.controlsElement.clientWidth - 12; - this.searchWidget.layout({ height: 20, width: monacoWidth }); + this.searchWidget.layout(new DOM.Dimension(monacoWidth, 20)); this.rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600); this.rootElement.classList.toggle('narrow-width', dimension.width < 600); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 89ae5f47db2..a8fa472a3bf 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1446,10 +1446,7 @@ class SCMInputWidget extends Disposable { layout(): void { const editorHeight = this.getContentHeight(); - const dimension: Dimension = { - width: this.element.clientWidth - 2, - height: editorHeight, - }; + const dimension = new Dimension(this.element.clientWidth - 2, editorHeight); this.inputEditor.layout(dimension); this.renderValidation(); diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts index 4c0849a8821..f154416d17d 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Dimension } from 'vs/base/browser/dom'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { toDisposable } from 'vs/base/common/lifecycle'; @@ -131,7 +132,7 @@ export class WebviewViewPane extends ViewPane { } if (this._container) { - this._webview.layoutWebviewOverElement(this._container, { width, height }); + this._webview.layoutWebviewOverElement(this._container, new Dimension(width, height)); } } From c748d1ca46a00b356bb576a17f6e378e028073c0 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 19 Oct 2020 21:41:33 +0200 Subject: [PATCH 031/212] add beforeRender and afterRender to IContentWidget --- src/vs/editor/browser/editorBrowser.ts | 12 ++++ .../contentWidgets/contentWidgets.ts | 59 +++++++++++++++---- src/vs/monaco.d.ts | 12 ++++ 3 files changed, 70 insertions(+), 13 deletions(-) diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 6bc4547463e..2b19fc90cff 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -156,6 +156,18 @@ export interface IContentWidget { * If null is returned, the content widget will be placed off screen. */ getPosition(): IContentWidgetPosition | null; + /** + * Optional function that is invoked before rendering + * the content widget. If a dimension is returned the editor will + * attempt to use it. + */ + beforeRender?(): editorCommon.IDimension | null; + /** + * Optional function that is invoked after rendering the content + * widget. The arguments are the actual dimensions and the selected + * position preference. + */ + afterRender?(position: ContentWidgetPositionPreference | null): void; } /** diff --git a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index a3a16436846..f6abaa7cae2 100644 --- a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -14,6 +14,7 @@ import { ViewContext } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IDimension } from 'vs/editor/common/editorCommon'; class Coordinate { @@ -171,6 +172,11 @@ interface IBoxLayoutResult { belowLeft: number; } +interface IRenderData { + coordinate: Coordinate, + position: ContentWidgetPositionPreference +} + class Widget { private readonly _context: ViewContext; private readonly _viewDomNode: FastDomNode; @@ -194,7 +200,7 @@ class Widget { private _maxWidth: number; private _isVisible: boolean; - private _renderData: Coordinate | null; + private _renderData: IRenderData | null; constructor(context: ViewContext, viewDomNode: FastDomNode, actual: IContentWidget) { this._context = context; @@ -428,16 +434,26 @@ class Widget { return [topLeft, bottomLeft]; } - private _prepareRenderWidget(ctx: RenderingContext): Coordinate | null { + private _prepareRenderWidget(ctx: RenderingContext): IRenderData | null { const [topLeft, bottomLeft] = this._getTopAndBottomLeft(ctx); if (!topLeft || !bottomLeft) { return null; } if (this._cachedDomNodeClientWidth === -1 || this._cachedDomNodeClientHeight === -1) { - const domNode = this.domNode.domNode; - this._cachedDomNodeClientWidth = domNode.clientWidth; - this._cachedDomNodeClientHeight = domNode.clientHeight; + + let preferredDimensions: IDimension | null = null; + if (typeof this._actual.beforeRender === 'function') { + preferredDimensions = safeInvoke(this._actual.beforeRender, this._actual); + } + if (preferredDimensions) { + this._cachedDomNodeClientWidth = preferredDimensions.width; + this._cachedDomNodeClientHeight = preferredDimensions.height; + } else { + const domNode = this.domNode.domNode; + this._cachedDomNodeClientWidth = domNode.clientWidth; + this._cachedDomNodeClientHeight = domNode.clientHeight; + } } let placement: IBoxLayoutResult | null; @@ -458,7 +474,7 @@ class Widget { return null; } if (pass === 2 || placement.fitsAbove) { - return new Coordinate(placement.aboveTop, placement.aboveLeft); + return { coordinate: new Coordinate(placement.aboveTop, placement.aboveLeft), position: ContentWidgetPositionPreference.ABOVE }; } } else if (pref === ContentWidgetPositionPreference.BELOW) { if (!placement) { @@ -466,13 +482,13 @@ class Widget { return null; } if (pass === 2 || placement.fitsBelow) { - return new Coordinate(placement.belowTop, placement.belowLeft); + return { coordinate: new Coordinate(placement.belowTop, placement.belowLeft), position: ContentWidgetPositionPreference.BELOW }; } } else { if (this.allowEditorOverflow) { - return this._prepareRenderWidgetAtExactPositionOverflowing(topLeft); + return { coordinate: this._prepareRenderWidgetAtExactPositionOverflowing(topLeft), position: ContentWidgetPositionPreference.EXACT }; } else { - return topLeft; + return { coordinate: topLeft, position: ContentWidgetPositionPreference.EXACT }; } } } @@ -509,16 +525,20 @@ class Widget { this._isVisible = false; this.domNode.setVisibility('hidden'); } + + if (typeof this._actual.afterRender === 'function') { + safeInvoke(this._actual.afterRender, this._actual, null); + } return; } // This widget should be visible if (this.allowEditorOverflow) { - this.domNode.setTop(this._renderData.top); - this.domNode.setLeft(this._renderData.left); + this.domNode.setTop(this._renderData.coordinate.top); + this.domNode.setLeft(this._renderData.coordinate.left); } else { - this.domNode.setTop(this._renderData.top + ctx.scrollTop - ctx.bigNumbersDelta); - this.domNode.setLeft(this._renderData.left); + this.domNode.setTop(this._renderData.coordinate.top + ctx.scrollTop - ctx.bigNumbersDelta); + this.domNode.setLeft(this._renderData.coordinate.left); } if (!this._isVisible) { @@ -526,5 +546,18 @@ class Widget { this.domNode.setAttribute('monaco-visible-content-widget', 'true'); this._isVisible = true; } + + if (typeof this._actual.afterRender === 'function') { + safeInvoke(this._actual.afterRender, this._actual, this._renderData.position); + } + } +} + +function safeInvoke any>(fn: T, thisArg: ThisParameterType, ...args: Parameters): ReturnType | null { + try { + return fn.call(thisArg, ...args); + } catch { + // ignore + return null; } } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index a44cb3bb307..1d51626cd4e 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4246,6 +4246,18 @@ declare namespace monaco.editor { * If null is returned, the content widget will be placed off screen. */ getPosition(): IContentWidgetPosition | null; + /** + * Optional function that is invoked before rendering + * the content widget. If a dimension is returned the editor will + * attempt to use it. + */ + beforeRender?(): IDimension | null; + /** + * Optional function that is invoked after rendering the content + * widget. The arguments are the actual dimensions and the selected + * position preference. + */ + afterRender?(position: ContentWidgetPositionPreference | null): void; } /** From 82df497acd08f78c2c550739d8164cb9ce46c60d Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 19 Oct 2020 13:00:50 -0700 Subject: [PATCH 032/212] cursor moves for nix --- .../terminal/browser/terminalTypeAheadAddon.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts index 62c39bf2830..f55b970a811 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts @@ -8,7 +8,8 @@ import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/term import { IBeforeProcessDataEvent, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; import type { IBuffer, IBufferCell, IDisposable, ITerminalAddon, Terminal } from 'xterm'; -const CSI = '\x1b['; +const ESC = '\x1b'; +const CSI = `${ESC}[`; const SHOW_CURSOR = `${CSI}?25h`; const HIDE_CURSOR = `${CSI}?25l`; const DELETE_CHAR = `${CSI}X`; @@ -645,6 +646,16 @@ export class TypeAheadAddon implements ITerminalAddon { continue; } + if (reader.eatStr(`${ESC}f`)) { + this.timeline.addPrediction(buffer, new CursorMovePrediction(CursorMoveDirection.Forwards, true, 1)); + continue; + } + + if (reader.eatStr(`${ESC}b`)) { + this.timeline.addPrediction(buffer, new CursorMovePrediction(CursorMoveDirection.Back, true, 1)); + continue; + } + if (reader.eatChar('\r') && buffer.cursorY < terminal.rows - 1) { this.timeline.addPrediction(buffer, new NewlinePrediction()); continue; From 4c520a11a682530b68b25c80b5509ddd1e93ca73 Mon Sep 17 00:00:00 2001 From: Miguel Solorio Date: Fri, 16 Oct 2020 09:31:47 -0700 Subject: [PATCH 033/212] update --- gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 8a5eff502f6..1d13cff608c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -40,4 +40,4 @@ process.on('unhandledRejection', (reason, p) => { // Load all the gulpfiles only if running tasks other than the editor tasks const build = path.join(__dirname, 'build'); require('glob').sync('gulpfile.*.js', { cwd: build }) - .forEach(f => require(`./build/${f}`)); \ No newline at end of file + .forEach(f => require(`./build/${f}`)); From 13081e6541cb1a6637b8b52a1a0e4ce83852ee1d Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 19 Oct 2020 16:29:56 -0700 Subject: [PATCH 034/212] get into a dogfooding state --- .../browser/terminalTypeAheadAddon.ts | 123 +++++++++++++----- .../terminal/browser/xterm-private.d.ts | 2 - .../terminal/common/terminalConfiguration.ts | 2 +- 3 files changed, 90 insertions(+), 37 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts index f55b970a811..f24e9a096e3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts @@ -14,9 +14,9 @@ const SHOW_CURSOR = `${CSI}?25h`; const HIDE_CURSOR = `${CSI}?25l`; const DELETE_CHAR = `${CSI}X`; const CSI_STYLE_RE = /^\x1b\[[0-9;]*m/; -const CSI_MOVE_RE = /^\x1b\[([0-9]*)(;5)?([DC])/; -const PASSWORD_INPUT_RE = /(password|passphrase|passwd).*: /i; -const WHITESPACE_RE = /\s/; +const CSI_MOVE_RE = /^\x1b\[([0-9]*)(;[35])?O?([DC])/; +const PASSWORD_INPUT_RE = /(password|passphrase|passwd).*:/i; +const NOT_WORD_RE = /\W/; /** * Codes that should be omitted from sending to the prediction engine and @@ -55,7 +55,7 @@ const moveToWordBoundary = (b: IBuffer, cursor: ICoordinate, direction: -1 | 1) } const chars = cell.getChars(); - if (WHITESPACE_RE.test(chars)) { + if (NOT_WORD_RE.test(chars)) { if (ateLeadingWhitespace) { break; } @@ -87,6 +87,8 @@ interface IPrediction { * Returns a sequence to apply the prediction. * @param buffer to write to * @param cursor position to write the data. Should advance the cursor. + * @returns a string to be written to the user terminal, or optionally a + * string for the user terminal and real pty. */ apply(buffer: IBuffer, cursor: ICoordinate): string; @@ -156,14 +158,14 @@ class StringReader { */ public eatGradually(substr: string): MatchResult { let prevIndex = this.index; - for (const char of substr) { - if (!this.eatChar(char)) { - this.index = prevIndex; - return MatchResult.Failure; + for (let i = 0; i < substr.length; i++) { + if (i > 0 && this.eof) { + return MatchResult.Buffer; } - if (this.eof) { - return MatchResult.Buffer; + if (!this.eatChar(substr[i])) { + this.index = prevIndex; + return MatchResult.Failure; } } @@ -215,6 +217,27 @@ class HardBoundary implements IPrediction { } } +/** + * Wraps another prediction. Does not apply the prediction, but will pass + * through its `matches` request. + */ +class TentativeBoundary implements IPrediction { + constructor(private readonly inner: IPrediction) { } + + public apply(buffer: IBuffer, cursor: ICoordinate) { + this.inner.apply(buffer, cursor); + return ''; + } + + public rollback() { + return ''; + } + + public matches(input: StringReader) { + return this.inner.matches(input); + } +} + /** * Prediction for a single alphanumeric character. */ @@ -289,7 +312,7 @@ class BackspacePrediction extends CharacterPrediction { } } - return input.eatChar('\b') ? MatchResult.Success : MatchResult.Failure; + return input.eatGradually('\b'); } } @@ -382,8 +405,9 @@ class CursorMovePrediction implements IPrediction { } // \b is the equivalent to moving one character back - if (direction === CursorMoveDirection.Back && input.eatChar('\b')) { - return MatchResult.Success; + const r2 = input.eatGradually(`\b`); + if (r2 !== MatchResult.Failure) { + return r2; } } @@ -534,15 +558,17 @@ class PredictionTimeline { this.expected.push({ gen: this.currentGen, p: prediction }); if (this.currentGen === this.expected[0].gen) { const text = prediction.apply(buffer, this.getCursor(buffer)); - console.log('prediction:', JSON.stringify(text)); this.terminal.write(text); } } /** - * Appends a boundary to the prediction. + * Appends a prediction followed by a boundary. The predictions applied + * after this one will only be displayed after the give prediction matches + * pty output/ */ - public addBoundary() { + public addBoundary(buffer: IBuffer, prediction: IPrediction) { + this.addPrediction(buffer, prediction); this.currentGen++; } @@ -594,6 +620,8 @@ const parseTypeheadStyle = (style: string | number) => { export class TypeAheadAddon implements ITerminalAddon { private disposables: IDisposable[] = []; private typeheadStyle = parseTypeheadStyle(this.config.config.typeaheadStyle); + private typeaheadThreshold = this.config.config.typeaheadThreshold; + private lastRow?: { y: number; startingX: number }; private timeline?: PredictionTimeline; constructor(private readonly _processManager: ITerminalProcessManager, private readonly config: TerminalConfigHelper) { @@ -602,27 +630,51 @@ export class TypeAheadAddon implements ITerminalAddon { public activate(terminal: Terminal): void { this.timeline = new PredictionTimeline(terminal); this.disposables.push(terminal.onData(e => this.onUserData(e))); - this.disposables.push(this.config.onConfigChanged(() => this.typeheadStyle = parseTypeheadStyle(this.config.config.typeaheadStyle))); + this.disposables.push(this.config.onConfigChanged(() => { + this.typeheadStyle = parseTypeheadStyle(this.config.config.typeaheadStyle); + this.typeaheadThreshold = this.config.config.typeaheadThreshold; + })); this.disposables.push(this._processManager.onBeforeProcessData(e => this.onBeforeProcessData(e))); } public dispose(): void { - // this.disposables.forEach(d => d.dispose()); + this.disposables.forEach(d => d.dispose()); } private onUserData(data: string): void { + if (this.typeaheadThreshold !== 0) { + return; + } + if (this.timeline?.terminal.buffer.active.type !== 'normal') { return; } - console.log('user data:', JSON.stringify(data)); + // console.log('user data:', JSON.stringify(data)); const terminal = this.timeline.terminal; const buffer = terminal.buffer.active; + + // the following code guards the terminal prompt to avoid being able to + // arrow or backspace-into the prompt. Record the lowest X value at which + // the user gave input, and mark all additions before that as tentative. + const actualY = buffer.baseY + buffer.cursorY; + if (actualY !== this.lastRow?.y) { + this.lastRow = { y: actualY, startingX: buffer.cursorX }; + } else { + this.lastRow.startingX = Math.min(this.lastRow.startingX, buffer.cursorX); + } + + const addLeftNavigating = (p: IPrediction) => + this.timeline!.getCursor(buffer).x <= this.lastRow!.startingX + ? this.timeline!.addBoundary(buffer, new TentativeBoundary(p)) + : this.timeline!.addPrediction(buffer, p); + + /** @see https://github.com/xtermjs/xterm.js/blob/1913e9512c048e3cf56bb5f5df51bfff6899c184/src/common/input/Keyboard.ts */ const reader = new StringReader(data); while (reader.remaining > 0) { if (reader.eatCharCode(127)) { // backspace - this.timeline.addPrediction(buffer, new BackspacePrediction()); + addLeftNavigating(new BackspacePrediction()); continue; } @@ -630,19 +682,20 @@ export class TypeAheadAddon implements ITerminalAddon { const char = data[reader.index - 1]; this.timeline.addPrediction(buffer, new CharacterPrediction(this.typeheadStyle, char)); if (this.timeline.getCursor(buffer).x === terminal.cols) { - this.timeline.addPrediction(buffer, new NewlinePrediction()); - this.timeline.addBoundary(); + this.timeline.addBoundary(buffer, new NewlinePrediction()); } continue; } const cursorMv = reader.eatRe(CSI_MOVE_RE); if (cursorMv) { - this.timeline.addPrediction(buffer, new CursorMovePrediction( - cursorMv[3] as CursorMoveDirection, - !!cursorMv[2], - Number(cursorMv[1]) || 1) - ); + const direction = cursorMv[3] as CursorMoveDirection; + const p = new CursorMovePrediction(direction, !!cursorMv[2], Number(cursorMv[1]) || 1); + if (direction === CursorMoveDirection.Back) { + addLeftNavigating(p); + } else { + this.timeline.addPrediction(buffer, p); + } continue; } @@ -652,7 +705,7 @@ export class TypeAheadAddon implements ITerminalAddon { } if (reader.eatStr(`${ESC}b`)) { - this.timeline.addPrediction(buffer, new CursorMovePrediction(CursorMoveDirection.Back, true, 1)); + addLeftNavigating(new CursorMovePrediction(CursorMoveDirection.Back, true, 1)); continue; } @@ -662,28 +715,30 @@ export class TypeAheadAddon implements ITerminalAddon { } // something else - this.timeline.addPrediction(buffer, new HardBoundary()); - this.timeline.addBoundary(); + this.timeline.addBoundary(buffer, new HardBoundary()); break; } } private onBeforeProcessData(event: IBeforeProcessDataEvent): void { + if (this.typeaheadThreshold !== 0) { + return; + } + if (!this.timeline) { return; } - console.log('incoming data:', JSON.stringify(event.data)); + // console.log('incoming data:', JSON.stringify(event.data)); event.data = this.timeline.beforeServerInput(event.data); - console.log('emitted data:', JSON.stringify(event.data)); + // console.log('emitted data:', JSON.stringify(event.data)); // If there's something that looks like a password prompt, omit giving // input. This is approximate since there's no TTY "password here" code, // but should be enough to cover common cases like sudo if (PASSWORD_INPUT_RE.test(event.data)) { const terminal = this.timeline.terminal; - this.timeline.addPrediction(terminal.buffer.active, new HardBoundary()); - this.timeline.addBoundary(); + this.timeline.addBoundary(terminal.buffer.active, new HardBoundary()); } } } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts b/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts index a46aa4477ff..c559ff7b8db 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts @@ -12,8 +12,6 @@ export interface XTermCore { height: number; }; - writeSync(input: Buffer | string): void; - _coreService: { triggerDataEvent(data: string, wasUserInput?: boolean): void; }; diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 2cce39b1a95..0f3ca52b6c2 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -353,7 +353,7 @@ export const terminalConfiguration: IConfigurationNode = { default: true }, 'terminal.integrated.typeaheadThreshold': { - description: localize('terminal.integrated.typeaheadThreshold', "Experimental: length of time, in milliseconds, where typeahead will active. If '0', typeahead will always be on, and if '-1' it will be disabled."), + description: localize('terminal.integrated.typeaheadThreshold', "Experimental: length of time, in milliseconds, where typeahead will active. If '0', typeahead will always be on, and if '-1' it will be disabled. Note: currently only -1 and 0 supported."), type: 'integer', minimum: -1, default: -1, From 6ad1da9f7958beb2368d51e9054e0f6d4e76ca74 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Sun, 18 Oct 2020 12:23:23 -0700 Subject: [PATCH 035/212] Pick up new TS for building VS Code --- build/package.json | 2 +- build/yarn.lock | 8 ++++---- package.json | 2 +- yarn.lock | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/build/package.json b/build/package.json index 7561ebc958c..9d86dbaf79e 100644 --- a/build/package.json +++ b/build/package.json @@ -45,7 +45,7 @@ "minimist": "^1.2.3", "request": "^2.85.0", "terser": "4.3.8", - "typescript": "^4.1.0-dev.20200924", + "typescript": "^4.1.0-dev.20201018", "vsce": "1.48.0", "vscode-telemetry-extractor": "^1.6.0", "xml2js": "^0.4.17" diff --git a/build/yarn.lock b/build/yarn.lock index 3cc284f20dc..09186eb00b0 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -2535,10 +2535,10 @@ typescript@^3.0.1: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== -typescript@^4.1.0-dev.20200924: - version "4.1.0-dev.20200924" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.0-dev.20200924.tgz#d8b2aaa6f94ec22725eafcadf0b9a17aae9c32b9" - integrity sha512-AXwqVrp2AeVZ3jaZ/gcvxb0nnvqEbDFuFFjvV5/9wfcyz7KZx5KvyJENUgGoJHywCvl1PHKasQKYjzjk1QixnQ== +typescript@^4.1.0-dev.20201018: + version "4.1.0-dev.20201018" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.0-dev.20201018.tgz#1a4b8e3f9b640218a44299773371354d75bcfa34" + integrity sha512-cOFYP1I+IrMWa6ZfefxcacZha1pQMxrq8DGMBLkvrl8k3CqIdD8APq9LXaMj/PWrB8IPgDprY6jHwqiHg0/oGA== typical@^4.0.0: version "4.0.0" diff --git a/package.json b/package.json index bda7354c003..eaef8f06d68 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,7 @@ "style-loader": "^1.0.0", "ts-loader": "^4.4.2", "tsec": "googleinterns/tsec", - "typescript": "^4.1.0-dev.20200924", + "typescript": "^4.1.0-dev.20201018", "typescript-formatter": "7.1.0", "underscore": "^1.8.2", "vinyl": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index b59bc29d438..6bbc9a10963 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9296,10 +9296,10 @@ typescript@^2.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= -typescript@^4.1.0-dev.20200924: - version "4.1.0-dev.20200924" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.0-dev.20200924.tgz#d8b2aaa6f94ec22725eafcadf0b9a17aae9c32b9" - integrity sha512-AXwqVrp2AeVZ3jaZ/gcvxb0nnvqEbDFuFFjvV5/9wfcyz7KZx5KvyJENUgGoJHywCvl1PHKasQKYjzjk1QixnQ== +typescript@^4.1.0-dev.20201018: + version "4.1.0-dev.20201018" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.0-dev.20201018.tgz#1a4b8e3f9b640218a44299773371354d75bcfa34" + integrity sha512-cOFYP1I+IrMWa6ZfefxcacZha1pQMxrq8DGMBLkvrl8k3CqIdD8APq9LXaMj/PWrB8IPgDprY6jHwqiHg0/oGA== uc.micro@^1.0.1, uc.micro@^1.0.3: version "1.0.3" From f1ffbb1f829557f646e9f9c463b76e02d4391a86 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 19 Oct 2020 16:56:13 -0700 Subject: [PATCH 036/212] Add title to commands in release notes Fixes #94712 This enables a title that displays the command id for text of the form: ``` kb(command.bla) ``` --- .../contrib/update/browser/releaseNotesEditor.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index 8c3430fbd32..0cf1e65ecba 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -133,7 +133,19 @@ export class ReleaseNotesManager { return resolvedKeybindings[0].getLabel() || unassigned; }; + const kbCode = (match: string, binding: string) => { + const resolved = kb(match, binding); + return resolved ? `${resolved}` : resolved; + }; + + const kbstyleCode = (match: string, binding: string) => { + const resolved = kbstyle(match, binding); + return resolved ? `${resolved}` : resolved; + }; + return text + .replace(/`kb\(([a-z.\d\-]+)\)`/gi, kbCode) + .replace(/`kbstyle\(([^\)]+)\)`/gi, kbstyleCode) .replace(/kb\(([a-z.\d\-]+)\)/gi, kb) .replace(/kbstyle\(([^\)]+)\)/gi, kbstyle); }; From 8f1117bf44db5d9e38e3c57a5116532537b8aa3a Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 19 Oct 2020 16:57:01 -0700 Subject: [PATCH 037/212] Extract some functions in markdown renderer --- src/vs/base/browser/markdownRenderer.ts | 59 ++++++++++++++++--------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 1f089e2542e..2d601e975df 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -222,11 +222,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende markedOptions.sanitize = true; markedOptions.renderer = renderer; - const allowedSchemes = [Schemas.http, Schemas.https, Schemas.mailto, Schemas.data, Schemas.file, Schemas.vscodeRemote, Schemas.vscodeRemoteResource]; - if (markdown.isTrusted) { - allowedSchemes.push(Schemas.command); - } - // values that are too long will freeze the UI let value = markdown.value ?? ''; if (value.length > 100_000) { @@ -239,9 +234,43 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende const renderedMarkdown = marked.parse(value, markedOptions); - // sanitize with insane - const insaneOptions = { + element.innerHTML = sanitizeRenderedMarkdown(markdown, renderedMarkdown); + + // signal that async code blocks can be now be inserted + signalInnerHTML!(); + + return element; +} + +function sanitizeRenderedMarkdown( + options: { isTrusted?: boolean }, + renderedMarkdown: string, +): string { + const insaneOptions = getInsaneOptions(options); + if (_ttpInsane) { + return _ttpInsane.createHTML(renderedMarkdown, insaneOptions) as unknown as string; + } else { + return insane(renderedMarkdown, insaneOptions); + } +} + +function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOptions { + const allowedSchemes = [ + Schemas.http, + Schemas.https, + Schemas.mailto, + Schemas.data, + Schemas.file, + Schemas.vscodeRemote, + Schemas.vscodeRemoteResource, + ]; + + if (options.isTrusted) { + allowedSchemes.push(Schemas.command); + } + + return { allowedSchemes, // allowedTags should included everything that markdown renders to. // Since we have our own sanitize function for marked, it's possible we missed some tag so let insane make sure. @@ -257,8 +286,8 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende 'th': ['align'], 'td': ['align'] }, - filter(token: { tag: string, attrs: { readonly [key: string]: string } }): boolean { - if (token.tag === 'span' && markdown.isTrusted && (Object.keys(token.attrs).length === 1)) { + filter(token: { tag: string; attrs: { readonly [key: string]: string; }; }): boolean { + if (token.tag === 'span' && options.isTrusted && (Object.keys(token.attrs).length === 1)) { if (token.attrs['style']) { return !!token.attrs['style'].match(/^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/); } else if (token.attrs['class']) { @@ -270,15 +299,5 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende return true; } }; - - if (_ttpInsane) { - element.innerHTML = _ttpInsane.createHTML(renderedMarkdown, insaneOptions) as unknown as string; - } else { - element.innerHTML = insane(renderedMarkdown, insaneOptions); - } - - // signal that async code blocks can be now be inserted - signalInnerHTML!(); - - return element; } + From 37c63d6ae551b5a2d4f1f6052ba645e02722f717 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 19 Oct 2020 17:18:49 -0700 Subject: [PATCH 038/212] Don't show loading and project loading status for in-memory JS/TS files Fixes #108454 --- .../src/languageFeatures/hover.ts | 5 +++-- .../src/typescriptServiceClient.ts | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/extensions/typescript-language-features/src/languageFeatures/hover.ts b/extensions/typescript-language-features/src/languageFeatures/hover.ts index a4de074897f..236ef694967 100644 --- a/extensions/typescript-language-features/src/languageFeatures/hover.ts +++ b/extensions/typescript-language-features/src/languageFeatures/hover.ts @@ -36,11 +36,12 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { } return new vscode.Hover( - this.getContents(response.body, response._serverType), + this.getContents(document.uri, response.body, response._serverType), typeConverters.Range.fromTextSpan(response.body)); } private getContents( + resource: vscode.Uri, data: Proto.QuickInfoResponseBody, source: ServerType | undefined, ) { @@ -49,7 +50,7 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { if (data.displayString) { const displayParts: string[] = []; - if (source === ServerType.Syntax && this.client.capabilities.has(ClientCapability.Semantic)) { + if (source === ServerType.Syntax && this.client.hasCapabilityForResource(resource, ClientCapability.Semantic)) { displayParts.push( localize({ key: 'loadingPrefix', diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 530e79c7fbd..3b4264f29ae 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -851,6 +851,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType break; } case EventName.projectsUpdatedInBackground: + this.loadingIndicator.reset(); + const body = (event as Proto.ProjectsUpdatedInBackgroundEvent).body; const resources = body.openFiles.map(file => this.toResource(file)); this.bufferSyncSupport.getErr(resources); From e2490d25b322c19e4959dcc98d1939f1986b82c8 Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Mon, 19 Oct 2020 17:25:57 -0700 Subject: [PATCH 039/212] fixes #99069 --- src/vs/workbench/common/theme.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index fb4a4d67db6..5193fd36e87 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -306,31 +306,31 @@ export const PANEL_SECTION_DRAG_AND_DROP_BACKGROUND = registerColor('panelSectio dark: EDITOR_DRAG_AND_DROP_BACKGROUND, light: EDITOR_DRAG_AND_DROP_BACKGROUND, hc: EDITOR_DRAG_AND_DROP_BACKGROUND, -}, nls.localize('panelSectionDragAndDropBackground', "Drag and drop feedback color for the panel sections. The color should have transparency so that the panel sections can still shine through. Panels are shown below the editor area and contain views like output and integrated terminal.")); +}, nls.localize('panelSectionDragAndDropBackground', "Drag and drop feedback color for the panel sections. The color should have transparency so that the panel sections can still shine through. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); export const PANEL_SECTION_HEADER_BACKGROUND = registerColor('panelSectionHeader.background', { dark: Color.fromHex('#808080').transparent(0.2), light: Color.fromHex('#808080').transparent(0.2), hc: null -}, nls.localize('panelSectionHeaderBackground', "Panel section header background color. Panels are shown below the editor area and contain views like output and integrated terminal.")); +}, nls.localize('panelSectionHeaderBackground', "Panel section header background color. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); export const PANEL_SECTION_HEADER_FOREGROUND = registerColor('panelSectionHeader.foreground', { dark: null, light: null, hc: null -}, nls.localize('panelSectionHeaderForeground', "Panel section header foreground color. Panels are shown below the editor area and contain views like output and integrated terminal.")); +}, nls.localize('panelSectionHeaderForeground', "Panel section header foreground color. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); export const PANEL_SECTION_HEADER_BORDER = registerColor('panelSectionHeader.border', { dark: contrastBorder, light: contrastBorder, hc: contrastBorder -}, nls.localize('panelSectionHeaderBorder', "Panel section header border color used when multiple views are stacked vertically in the panel. Panels are shown below the editor area and contain views like output and integrated terminal.")); +}, nls.localize('panelSectionHeaderBorder', "Panel section header border color used when multiple views are stacked vertically in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); export const PANEL_SECTION_BORDER = registerColor('panelSection.border', { dark: PANEL_BORDER, light: PANEL_BORDER, hc: PANEL_BORDER -}, nls.localize('panelSectionBorder', "Panel section border color used when multiple views are stacked horizontally in the panel. Panels are shown below the editor area and contain views like output and integrated terminal.")); +}, nls.localize('panelSectionBorder', "Panel section border color used when multiple views are stacked horizontally in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); // < --- Status --- > @@ -521,25 +521,25 @@ export const SIDE_BAR_DRAG_AND_DROP_BACKGROUND = registerColor('sideBar.dropBack dark: EDITOR_DRAG_AND_DROP_BACKGROUND, light: EDITOR_DRAG_AND_DROP_BACKGROUND, hc: EDITOR_DRAG_AND_DROP_BACKGROUND, -}, nls.localize('sideBarDragAndDropBackground', "Drag and drop feedback color for the side bar sections. The color should have transparency so that the side bar sections can still shine through. The side bar is the container for views like explorer and search.")); +}, nls.localize('sideBarDragAndDropBackground', "Drag and drop feedback color for the side bar sections. The color should have transparency so that the side bar sections can still shine through. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); export const SIDE_BAR_SECTION_HEADER_BACKGROUND = registerColor('sideBarSectionHeader.background', { dark: Color.fromHex('#808080').transparent(0.2), light: Color.fromHex('#808080').transparent(0.2), hc: null -}, nls.localize('sideBarSectionHeaderBackground', "Side bar section header background color. The side bar is the container for views like explorer and search.")); +}, nls.localize('sideBarSectionHeaderBackground', "Side bar section header background color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar")); export const SIDE_BAR_SECTION_HEADER_FOREGROUND = registerColor('sideBarSectionHeader.foreground', { dark: SIDE_BAR_FOREGROUND, light: SIDE_BAR_FOREGROUND, hc: SIDE_BAR_FOREGROUND -}, nls.localize('sideBarSectionHeaderForeground', "Side bar section header foreground color. The side bar is the container for views like explorer and search.")); +}, nls.localize('sideBarSectionHeaderForeground', "Side bar section header foreground color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar")); export const SIDE_BAR_SECTION_HEADER_BORDER = registerColor('sideBarSectionHeader.border', { dark: contrastBorder, light: contrastBorder, hc: contrastBorder -}, nls.localize('sideBarSectionHeaderBorder', "Side bar section header border color. The side bar is the container for views like explorer and search.")); +}, nls.localize('sideBarSectionHeaderBorder', "Side bar section header border color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar")); // < --- Title Bar --- > From 9745766e8e9f1a31a856e269cb42e0e6ef10aac2 Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Mon, 19 Oct 2020 17:27:00 -0700 Subject: [PATCH 040/212] add period --- src/vs/workbench/common/theme.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 5193fd36e87..f7451eda51d 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -527,19 +527,19 @@ export const SIDE_BAR_SECTION_HEADER_BACKGROUND = registerColor('sideBarSectionH dark: Color.fromHex('#808080').transparent(0.2), light: Color.fromHex('#808080').transparent(0.2), hc: null -}, nls.localize('sideBarSectionHeaderBackground', "Side bar section header background color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar")); +}, nls.localize('sideBarSectionHeaderBackground', "Side bar section header background color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); export const SIDE_BAR_SECTION_HEADER_FOREGROUND = registerColor('sideBarSectionHeader.foreground', { dark: SIDE_BAR_FOREGROUND, light: SIDE_BAR_FOREGROUND, hc: SIDE_BAR_FOREGROUND -}, nls.localize('sideBarSectionHeaderForeground', "Side bar section header foreground color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar")); +}, nls.localize('sideBarSectionHeaderForeground', "Side bar section header foreground color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); export const SIDE_BAR_SECTION_HEADER_BORDER = registerColor('sideBarSectionHeader.border', { dark: contrastBorder, light: contrastBorder, hc: contrastBorder -}, nls.localize('sideBarSectionHeaderBorder', "Side bar section header border color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar")); +}, nls.localize('sideBarSectionHeaderBorder', "Side bar section header border color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); // < --- Title Bar --- > From ff4f319e2954a49506d8142ae66cca9f20977c1b Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Mon, 19 Oct 2020 17:53:00 -0700 Subject: [PATCH 041/212] fixes #102345 --- src/vs/workbench/common/views.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index cb690ac93e7..24f43b1d740 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -184,7 +184,7 @@ class ViewContainersRegistryImpl extends Disposable implements IViewContainersRe } getViewContainerLocation(container: ViewContainer): ViewContainerLocation { - return [...this.viewContainers.keys()].filter(location => this.getViewContainers(location).filter(viewContainer => viewContainer.id === container.id).length > 0)[0]; + return [...this.viewContainers.keys()].filter(location => this.getViewContainers(location).filter(viewContainer => viewContainer?.id === container.id).length > 0)[0]; } getDefaultViewContainer(location: ViewContainerLocation): ViewContainer | undefined { From 5eb46b187cf508079c085989fd406ec1ac0f8f30 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 20 Oct 2020 03:36:08 +0200 Subject: [PATCH 042/212] link protection - read remotes only once on startup (#108938) * link protection - read remotes only once on startup * Reread on potential ocntent changes. Co-authored-by: Jackson Kearl --- .../contrib/url/browser/trustedDomains.ts | 50 ++++++++++++++----- .../url/browser/trustedDomainsValidator.ts | 33 ++++++++++-- 2 files changed, 65 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/url/browser/trustedDomains.ts b/src/vs/workbench/contrib/url/browser/trustedDomains.ts index 3e9d479633b..4d4b66c64f7 100644 --- a/src/vs/workbench/contrib/url/browser/trustedDomains.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomains.ts @@ -174,14 +174,47 @@ async function getRemotes(fileService: IFileService, textFileService: ITextFileS return [...set]; } -export async function readTrustedDomains(accessor: ServicesAccessor) { +export interface IStaticTrustedDomains { + readonly defaultTrustedDomains: string[]; + readonly trustedDomains: string[]; +} - const storageService = accessor.get(IStorageService); - const productService = accessor.get(IProductService); - const authenticationService = accessor.get(IAuthenticationService); +export interface ITrustedDomains extends IStaticTrustedDomains { + readonly userDomains: string[]; + readonly workspaceDomains: string[]; +} + +export async function readTrustedDomains(accessor: ServicesAccessor): Promise { + const { defaultTrustedDomains, trustedDomains } = readStaticTrustedDomains(accessor); + const [workspaceDomains, userDomains] = await Promise.all([readWorkspaceTrustedDomains(accessor), readAuthenticationTrustedDomains(accessor)]); + return { + workspaceDomains, + userDomains, + defaultTrustedDomains, + trustedDomains, + }; +} + +export async function readWorkspaceTrustedDomains(accessor: ServicesAccessor): Promise { + console.log('reading workspace domains'); const fileService = accessor.get(IFileService); const textFileService = accessor.get(ITextFileService); const workspaceContextService = accessor.get(IWorkspaceContextService); + return getRemotes(fileService, textFileService, workspaceContextService); +} + +export async function readAuthenticationTrustedDomains(accessor: ServicesAccessor): Promise { + console.log('reading auth domains'); + + const authenticationService = accessor.get(IAuthenticationService); + return authenticationService.isAuthenticationProviderRegistered('github') && ((await authenticationService.getSessions('github')) ?? []).length > 0 + ? [`https://github.com`] + : []; +} + +export function readStaticTrustedDomains(accessor: ServicesAccessor): IStaticTrustedDomains { + const storageService = accessor.get(IStorageService); + const productService = accessor.get(IProductService); const defaultTrustedDomains: string[] = productService.linkProtectionTrustedDomains ? [...productService.linkProtectionTrustedDomains] @@ -195,17 +228,8 @@ export async function readTrustedDomains(accessor: ServicesAccessor) { } } catch (err) { } - const userDomains = - authenticationService.isAuthenticationProviderRegistered('github') && ((await authenticationService.getSessions('github')) ?? []).length > 0 - ? [`https://github.com`] - : []; - - const workspaceDomains = await getRemotes(fileService, textFileService, workspaceContextService); - return { defaultTrustedDomains, trustedDomains, - userDomains, - workspaceDomains }; } diff --git a/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts index 235931c0287..2c8428a6300 100644 --- a/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts @@ -13,21 +13,25 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { - configureOpenerTrustedDomainsHandler, - readTrustedDomains -} from 'vs/workbench/contrib/url/browser/trustedDomains'; +import { configureOpenerTrustedDomainsHandler, readAuthenticationTrustedDomains, readStaticTrustedDomains, readWorkspaceTrustedDomains } from 'vs/workbench/contrib/url/browser/trustedDomains'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IdleValue } from 'vs/base/common/async'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; type TrustedDomainsDialogActionClassification = { action: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; }; export class OpenerValidatorContributions implements IWorkbenchContribution { + + private _readWorkspaceTrustedDomainsResult: IdleValue>; + private _readAuthenticationTrustedDomainsResult: IdleValue>; + constructor( @IOpenerService private readonly _openerService: IOpenerService, @IStorageService private readonly _storageService: IStorageService, @@ -39,8 +43,26 @@ export class OpenerValidatorContributions implements IWorkbenchContribution { @ITelemetryService private readonly _telemetryService: ITelemetryService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @INotificationService private readonly _notificationService: INotificationService, + @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, ) { this._openerService.registerValidator({ shouldOpen: r => this.validateLink(r) }); + + this._readAuthenticationTrustedDomainsResult = new IdleValue(() => + this._instantiationService.invokeFunction(readAuthenticationTrustedDomains)); + this._authenticationService.onDidRegisterAuthenticationProvider(() => { + this._readAuthenticationTrustedDomainsResult?.dispose(); + this._readAuthenticationTrustedDomainsResult = new IdleValue(() => + this._instantiationService.invokeFunction(readAuthenticationTrustedDomains)); + }); + + this._readWorkspaceTrustedDomainsResult = new IdleValue(() => + this._instantiationService.invokeFunction(readWorkspaceTrustedDomains)); + this._workspaceContextService.onDidChangeWorkspaceFolders(() => { + this._readWorkspaceTrustedDomainsResult?.dispose(); + this._readWorkspaceTrustedDomainsResult = new IdleValue(() => + this._instantiationService.invokeFunction(readWorkspaceTrustedDomains)); + }); } async validateLink(resource: URI | string): Promise { @@ -54,7 +76,8 @@ export class OpenerValidatorContributions implements IWorkbenchContribution { const { scheme, authority, path, query, fragment } = resource; const domainToOpen = `${scheme}://${authority}`; - const { defaultTrustedDomains, trustedDomains, userDomains, workspaceDomains } = await this._instantiationService.invokeFunction(readTrustedDomains); + const [workspaceDomains, userDomains] = await Promise.all([this._readWorkspaceTrustedDomainsResult.value, this._readAuthenticationTrustedDomainsResult.value]); + const { defaultTrustedDomains, trustedDomains, } = this._instantiationService.invokeFunction(readStaticTrustedDomains); const allTrustedDomains = [...defaultTrustedDomains, ...trustedDomains, ...userDomains, ...workspaceDomains]; if (isURLDomainTrusted(resource, allTrustedDomains)) { From a4a4cf5ace4472bc4f5176396bb290cafa15c518 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 19 Oct 2020 19:00:08 -0700 Subject: [PATCH 043/212] Re-resolve webview views when extension host restarts Fixes #108555 Previously webviews were left hanging when the extension host died. With this change, we now try to re-create them once the extension host restarts --- .../api/browser/mainThreadWebviewPanels.ts | 4 +- .../api/browser/mainThreadWebviewViews.ts | 15 +++++- .../webviewView/browser/webviewViewPane.ts | 53 ++++++++++++------- .../webviewView/browser/webviewViewService.ts | 20 ++++--- 4 files changed, 63 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts index c0dbac194c1..561e3c219eb 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts @@ -128,9 +128,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc dispose() { super.dispose(); - for (const disposable of this._editorProviders.values()) { - disposable.dispose(); - } + dispose(this._editorProviders.values()); this._editorProviders.clear(); } diff --git a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts index 346ca9957b6..e369b4f088d 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts @@ -5,7 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { MainThreadWebviews, reviveWebviewExtension } from 'vs/workbench/api/browser/mainThreadWebviews'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; import { IWebviewViewService, WebviewView } from 'vs/workbench/contrib/webviewView/browser/webviewViewService'; @@ -28,6 +28,15 @@ export class MainThreadWebviewsViews extends Disposable implements extHostProtoc this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviewViews); } + dispose() { + super.dispose(); + + dispose(this._webviewViewProviders.values()); + this._webviewViewProviders.clear(); + + dispose(this._webviewViews.values()); + } + public $setWebviewViewTitle(handle: extHostProtocol.WebviewHandle, value: string | undefined): void { const webviewView = this.getWebviewView(handle); webviewView.title = value; @@ -54,7 +63,7 @@ export class MainThreadWebviewsViews extends Disposable implements extHostProtoc const extension = reviveWebviewExtension(extensionData); - this._webviewViewService.register(viewType, { + const registration = this._webviewViewService.register(viewType, { resolve: async (webviewView: WebviewView, cancellation: CancellationToken) => { const handle = webviewView.webview.id; @@ -93,6 +102,8 @@ export class MainThreadWebviewsViews extends Disposable implements extHostProtoc } } }); + + this._webviewViewProviders.set(viewType, registration); } public $unregisterWebviewViewProvider(viewType: string): void { diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts index 4c0849a8821..5e877d47a2f 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -5,7 +5,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; -import { toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { setImmediate } from 'vs/base/common/platform'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -34,7 +34,8 @@ const storageKeys = { export class WebviewViewPane extends ViewPane { - private _webview?: WebviewOverlay; + private readonly _webview = this._register(new MutableDisposable()); + private readonly _webviewDisposables = this._register(new DisposableStore()); private _activated = false; private _container?: HTMLElement; @@ -71,6 +72,14 @@ export class WebviewViewPane extends ViewPane { this.viewState = this.memento.getMemento(StorageScope.WORKSPACE); this._register(this.onDidChangeBodyVisibility(() => this.updateTreeVisibility())); + + this._register(this.webviewViewService.onNewResolverRegistered(e => { + if (e.viewType === this.id) { + // Potentially re-activate if we have a new resolver + this.updateTreeVisibility(); + } + })); + this.updateTreeVisibility(); } @@ -83,14 +92,12 @@ export class WebviewViewPane extends ViewPane { dispose() { this._onDispose.fire(); - this._webview?.dispose(); - super.dispose(); } focus(): void { super.focus(); - this._webview?.focus(); + this._webview.value?.focus(); } renderBody(container: HTMLElement): void { @@ -102,7 +109,7 @@ export class WebviewViewPane extends ViewPane { this._resizeObserver = new ResizeObserver(() => { setImmediate(() => { if (this._container) { - this._webview?.layoutWebviewOverElement(this._container); + this._webview.value?.layoutWebviewOverElement(this._container); } }); }); @@ -115,8 +122,8 @@ export class WebviewViewPane extends ViewPane { } public saveState() { - if (this._webview) { - this.viewState[storageKeys.webviewState] = this._webview.state; + if (this._webview.value) { + this.viewState[storageKeys.webviewState] = this._webview.value.state; } this.memento.saveMemento(); @@ -126,21 +133,21 @@ export class WebviewViewPane extends ViewPane { protected layoutBody(height: number, width: number): void { super.layoutBody(height, width); - if (!this._webview) { + if (!this._webview.value) { return; } if (this._container) { - this._webview.layoutWebviewOverElement(this._container, { width, height }); + this._webview.value.layoutWebviewOverElement(this._container, { width, height }); } } private updateTreeVisibility() { if (this.isBodyVisible()) { this.activate(); - this._webview?.claim(this); + this._webview.value?.claim(this); } else { - this._webview?.release(this); + this._webview.value?.release(this); } } @@ -151,17 +158,20 @@ export class WebviewViewPane extends ViewPane { const webviewId = `webviewView-${this.id.replace(/[^a-z0-9]/gi, '-')}`.toLowerCase(); const webview = this.webviewService.createWebviewOverlay(webviewId, {}, {}, undefined); webview.state = this.viewState[storageKeys.webviewState]; - this._webview = webview; + this._webview.value = webview; - this._register(toDisposable(() => { - this._webview?.release(this); + if (this._container) { + this._webview.value?.layoutWebviewOverElement(this._container); + } + + this._webviewDisposables.add(toDisposable(() => { + this._webview.value?.release(this); })); - this._register(webview.onDidUpdateState(() => { + this._webviewDisposables.add(webview.onDidUpdateState(() => { this.viewState[storageKeys.webviewState] = webview.state; })); - - const source = this._register(new CancellationTokenSource()); + const source = this._webviewDisposables.add(new CancellationTokenSource()); this.withProgress(async () => { await this.extensionService.activateByEvent(`onView:${this.id}`); @@ -178,6 +188,13 @@ export class WebviewViewPane extends ViewPane { get description(): string | undefined { return self.titleDescription; }, set description(value: string | undefined) { self.updateTitleDescription(value); }, + dispose: () => { + // Only reset and clear the webview itself. Don't dispose of the view container + this._activated = false; + this._webview.clear(); + this._webviewDisposables.clear(); + }, + show: (preserveFocus) => { this.viewService.openView(this.id, !preserveFocus); } diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts index 68196b0f01f..663e359f7cf 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; @@ -20,6 +20,8 @@ export interface WebviewView { readonly onDidChangeVisibility: Event; readonly onDispose: Event; + dispose(): void; + show(preserveFocus: boolean): void; } @@ -31,6 +33,8 @@ export interface IWebviewViewService { readonly _serviceBrand: undefined; + readonly onNewResolverRegistered: Event<{ readonly viewType: string }>; + register(type: string, resolver: IWebviewViewResolver): IDisposable; resolve(viewType: string, webview: WebviewView, cancellation: CancellationToken): Promise; @@ -40,16 +44,20 @@ export class WebviewViewService extends Disposable implements IWebviewViewServic readonly _serviceBrand: undefined; - private readonly _views = new Map(); + private readonly _resolvers = new Map(); private readonly _awaitingRevival = new Map void }>(); + private readonly _onNewResolverRegistered = this._register(new Emitter<{ readonly viewType: string }>()); + public readonly onNewResolverRegistered = this._onNewResolverRegistered.event; + register(viewType: string, resolver: IWebviewViewResolver): IDisposable { - if (this._views.has(viewType)) { + if (this._resolvers.has(viewType)) { throw new Error(`View resolver already registered for ${viewType}`); } - this._views.set(viewType, resolver); + this._resolvers.set(viewType, resolver); + this._onNewResolverRegistered.fire({ viewType: viewType }); const pending = this._awaitingRevival.get(viewType); if (pending) { @@ -60,12 +68,12 @@ export class WebviewViewService extends Disposable implements IWebviewViewServic } return toDisposable(() => { - this._views.delete(viewType); + this._resolvers.delete(viewType); }); } resolve(viewType: string, webview: WebviewView, cancellation: CancellationToken): Promise { - const resolver = this._views.get(viewType); + const resolver = this._resolvers.get(viewType); if (!resolver) { if (this._awaitingRevival.has(viewType)) { throw new Error('View already awaiting revival'); From 13b3c937dc5e3816c79bdd2cdf2cdf6f9c727b75 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 19 Oct 2020 20:22:46 -0700 Subject: [PATCH 044/212] Use looser check for active element when re-focusing webview Fixes #108596 Always skip refocusing a webview if a new element has been focused after we call `.focus` on it --- .../contrib/webview/electron-browser/webviewElement.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index b7cd695aa46..6ac6a86d729 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -312,8 +312,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme if (!this.isFocused || !this.element) { return; } - - if (document.activeElement?.tagName === 'INPUT') { + if (document.activeElement && document.activeElement?.tagName !== 'BODY') { return; } try { From 50bfc596da23dc853bc3e04dc49957a27756e87d Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 20 Oct 2020 08:05:33 +0200 Subject: [PATCH 045/212] use try catch --- src/vs/platform/log/common/fileLogService.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/log/common/fileLogService.ts b/src/vs/platform/log/common/fileLogService.ts index 3f85535c791..a6318446b88 100644 --- a/src/vs/platform/log/common/fileLogService.ts +++ b/src/vs/platform/log/common/fileLogService.ts @@ -5,7 +5,7 @@ import { ILogService, LogLevel, AbstractLogService, ILoggerService, ILogger } from 'vs/platform/log/common/log'; import { URI } from 'vs/base/common/uri'; -import { IFileService, whenProviderRegistered } from 'vs/platform/files/common/files'; +import { FileOperationError, FileOperationResult, IFileService, whenProviderRegistered } from 'vs/platform/files/common/files'; import { Queue } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { dirname, joinPath, basename } from 'vs/base/common/resources'; @@ -87,8 +87,12 @@ export class FileLogService extends AbstractLogService implements ILogService { } private async initialize(): Promise { - if (!await this.fileService.exists(this.resource)) { + try { await this.fileService.createFile(this.resource); + } catch (error) { + if ((error).fileOperationResult !== FileOperationResult.FILE_MODIFIED_SINCE) { + throw error; + } } } From 60d96d72a560f0f02d67ef9d438a8a7ed534ec4a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 20 Oct 2020 09:10:44 +0200 Subject: [PATCH 046/212] explorer - tweaks to upload/download --- .../contrib/files/browser/fileActions.ts | 11 +- .../files/browser/views/explorerViewer.ts | 118 +++++++++++------- 2 files changed, 78 insertions(+), 51 deletions(-) diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index f91217259bd..c7f63c11d7a 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -1044,8 +1044,8 @@ const downloadFileHandler = (accessor: ServicesAccessor) => { filesTotal: number; filesDownloaded: number; - totalBytesDownloaded: 0 - fileBytesDownloaded: 0 + totalBytesDownloaded: number; + fileBytesDownloaded: number; } async function pipeContents(name: string, source: IFileStreamContent, target: WebFileSystemAccess.FileSystemWritableFileStream, operation: IDownloadOperation): Promise { @@ -1142,7 +1142,7 @@ const downloadFileHandler = (accessor: ServicesAccessor) => { } try { - const targetFolder: WebFileSystemAccess.FileSystemDirectoryHandle = await window.showDirectoryPicker(); + const parentFolder: WebFileSystemAccess.FileSystemDirectoryHandle = await window.showDirectoryPicker(); const operation: IDownloadOperation = { startTime: Date.now(), @@ -1154,12 +1154,13 @@ const downloadFileHandler = (accessor: ServicesAccessor) => { }; if (stat.isDirectory) { + const targetFolder = await parentFolder.getDirectoryHandle(stat.name, { create: true }); await downloadFolder(stat, targetFolder, operation); } else { - await downloadFile(targetFolder, stat.name, stat.resource, operation); + await downloadFile(parentFolder, stat.name, stat.resource, operation); } } catch (error) { - logService.trace(error); + logService.warn(error); cts.cancel(); // `showDirectoryPicker` will throw an error when the user cancels } } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 01b3dbc05a5..17a38497e9e 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -999,22 +999,43 @@ export class FileDragAndDrop implements ITreeDragAndDrop { if (target.isReadonly) { return; } + const resolvedTarget = target; + if (!resolvedTarget) { + return; + } // Desktop DND (Import file) if (data instanceof NativeDragAndDropData) { - if (isWeb) { - this.handleWebExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e)); - } else { - this.handleExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e)); - } + const cts = new CancellationTokenSource(); + + // Indicate progress globally + const dropPromise = this.progressService.withProgress({ + location: ProgressLocation.Window, + delay: 800, + cancellable: true, + title: isWeb ? localize('uploadingFiles', "Uploading") : localize('copyingFiles', "Copying") + }, async progress => { + try { + if (isWeb) { + await this.handleWebExternalDrop(data, resolvedTarget, originalEvent, progress, cts.token); + } else { + await this.handleExternalDrop(data, resolvedTarget, originalEvent, progress, cts.token); + } + } catch (error) { + this.notificationService.warn(error); + } + }, () => cts.dispose(true)); + + // Also indicate progress in the files view + this.progressService.withProgress({ location: VIEW_ID, delay: 800 }, () => dropPromise); } // In-Explorer DND (Move/Copy file) else { - this.handleExplorerDrop(data as ElementsDragAndDropData, target, originalEvent).then(undefined, e => this.notificationService.warn(e)); + this.handleExplorerDrop(data as ElementsDragAndDropData, resolvedTarget, originalEvent).then(undefined, e => this.notificationService.warn(e)); } } - private async handleWebExternalDrop(data: NativeDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { + private async handleWebExternalDrop(data: NativeDragAndDropData, target: ExplorerItem, originalEvent: DragEvent, progress: IProgress, token: CancellationToken): Promise { const items = (originalEvent.dataTransfer as unknown as IWebkitDataTransfer).items; // Somehow the items thing is being modified at random, maybe as a security @@ -1026,48 +1047,38 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } const results: { isFile: boolean, resource: URI }[] = []; - const cts = new CancellationTokenSource(); const operation: IUploadOperation = { filesTotal: entries.length, filesUploaded: 0, startTime: Date.now(), bytesUploaded: 0 }; - // Start upload and report progress globally - const uploadPromise = this.progressService.withProgress({ - location: ProgressLocation.Window, - delay: 800, - cancellable: true, - title: localize('uploadingFiles', "Uploading") - }, async progress => { - for (let entry of entries) { - if (cts.token.isCancellationRequested) { + for (let entry of entries) { + if (token.isCancellationRequested) { + break; + } + + // Confirm overwrite as needed + if (target && entry.name && target.getChild(entry.name)) { + const { confirmed } = await this.dialogService.confirm(getFileOverwriteConfirm(entry.name)); + if (!confirmed) { + continue; + } + + await this.workingCopyFileService.delete([joinPath(target.resource, entry.name)], { recursive: true }); + + if (token.isCancellationRequested) { break; } - - // Confirm overwrite as needed - if (target && entry.name && target.getChild(entry.name)) { - const { confirmed } = await this.dialogService.confirm(getFileOverwriteConfirm(entry.name)); - if (!confirmed) { - continue; - } - - await this.workingCopyFileService.delete([joinPath(target.resource, entry.name)], { recursive: true }); - } - - // Upload entry - const result = await this.doUploadWebFileEntry(entry, target.resource, target, progress, operation, cts.token); - if (result) { - results.push(result); - } } - }, () => cts.dispose(true)); - // Also indicate progress in the files view - this.progressService.withProgress({ location: VIEW_ID, delay: 800 }, () => uploadPromise); - - // Wait until upload is done - await uploadPromise; + // Upload entry + const result = await this.doUploadWebFileEntry(entry, target.resource, target, progress, operation, token); + if (result) { + results.push(result); + } + } // Open uploaded file in editor only if we upload just one - if (!cts.token.isCancellationRequested && results.length === 1 && results[0].isFile) { - await this.editorService.openEditor({ resource: results[0].resource, options: { pinned: true } }); + const firstUploadedFile = results[0]; + if (!token.isCancellationRequested && firstUploadedFile?.isFile) { + await this.editorService.openEditor({ resource: firstUploadedFile.resource, options: { pinned: true } }); } } @@ -1234,12 +1245,16 @@ export class FileDragAndDrop implements ITreeDragAndDrop { }); } - private async handleExternalDrop(data: NativeDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { + private async handleExternalDrop(data: NativeDragAndDropData, target: ExplorerItem, originalEvent: DragEvent, progress: IProgress, token: CancellationToken): Promise { // Check for dropped external files to be folders const droppedResources = extractResources(originalEvent, true); const result = await this.fileService.resolveAll(droppedResources.map(droppedResource => ({ resource: droppedResource.resource }))); + if (token.isCancellationRequested) { + return; + } + // Pass focus to window this.hostService.focus(); @@ -1264,7 +1279,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { return this.workspaceEditingService.addFolders(folders); } if (choice === buttons.length - 2) { - return this.addResources(target, droppedResources.map(res => res.resource)); + return this.addResources(target, droppedResources.map(res => res.resource), progress, token); } return undefined; @@ -1272,16 +1287,20 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // Handle dropped files (only support FileStat as target) else if (target instanceof ExplorerItem) { - return this.addResources(target, droppedResources.map(res => res.resource)); + return this.addResources(target, droppedResources.map(res => res.resource), progress, token); } } - private async addResources(target: ExplorerItem, resources: URI[]): Promise { + private async addResources(target: ExplorerItem, resources: URI[], progress: IProgress, token: CancellationToken): Promise { if (resources && resources.length > 0) { // Resolve target to check for name collisions and ask user const targetStat = await this.fileService.resolve(target.resource); + if (token.isCancellationRequested) { + return; + } + // Check for name collisions const targetNames = new Set(); const caseSensitive = this.fileService.hasCapability(target.resource, FileSystemProviderCapabilities.PathCaseSensitive); @@ -1302,8 +1321,15 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } addPromisesFactory.push(async () => { + if (token.isCancellationRequested) { + return; + } + const sourceFile = resource; - const targetFile = joinPath(target.resource, basename(sourceFile)); + const sourceFileName = basename(sourceFile); + const targetFile = joinPath(target.resource, sourceFileName); + + progress.report({ message: sourceFileName }); const stat = (await this.workingCopyFileService.copy([{ source: sourceFile, target: targetFile }], { overwrite: true }))[0]; // if we only add one file, just open it directly From 73cd1f193fc45f5fbe330a29d365196a78516bce Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 20 Oct 2020 09:10:55 +0200 Subject: [PATCH 047/212] trusted domains - remove console.logs //cc @JacksonKearl --- src/vs/workbench/contrib/url/browser/trustedDomains.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/vs/workbench/contrib/url/browser/trustedDomains.ts b/src/vs/workbench/contrib/url/browser/trustedDomains.ts index 4d4b66c64f7..81ec10aea95 100644 --- a/src/vs/workbench/contrib/url/browser/trustedDomains.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomains.ts @@ -196,7 +196,6 @@ export async function readTrustedDomains(accessor: ServicesAccessor): Promise { - console.log('reading workspace domains'); const fileService = accessor.get(IFileService); const textFileService = accessor.get(ITextFileService); const workspaceContextService = accessor.get(IWorkspaceContextService); @@ -204,8 +203,6 @@ export async function readWorkspaceTrustedDomains(accessor: ServicesAccessor): P } export async function readAuthenticationTrustedDomains(accessor: ServicesAccessor): Promise { - console.log('reading auth domains'); - const authenticationService = accessor.get(IAuthenticationService); return authenticationService.isAuthenticationProviderRegistered('github') && ((await authenticationService.getSessions('github')) ?? []).length > 0 ? [`https://github.com`] From 3c463a2076ef0f2c8ef838bfc4e31fa7e3ca119a Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 20 Oct 2020 09:46:48 +0200 Subject: [PATCH 048/212] Fixes #108880 --- .../services/keybinding/common/macLinuxKeyboardMapper.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts b/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts index 86f63f04b56..7e2d9d6d17c 100644 --- a/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts +++ b/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts @@ -956,6 +956,12 @@ export class MacLinuxKeyboardMapper implements IKeyboardMapper { } } + // See https://github.com/microsoft/vscode/issues/108880 + if (binding.ctrlKey && !binding.metaKey && !binding.altKey && constantKeyCode === KeyCode.US_MINUS) { + // ctrl+- and ctrl+shift+- render very similarly in native macOS menus, leading to confusion + return null; + } + if (constantKeyCode !== -1) { return this._getElectronLabelForKeyCode(constantKeyCode); } From 4af42491069dcbf0881b7993ee18a180b790f861 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 20 Oct 2020 04:04:33 -0400 Subject: [PATCH 049/212] SCM: Support past commit message navigation (#107619) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * logging prior commit messages * Logging prior commit messages * Logging prior commit messages * now works in forward and backward directions * reset index on empty input * cleaning up code * introduce historyNavigator, working but not persisting on window reload * introduce historyNavigator, working but not persisting on window reload * add history * saves search entries on window reload but doesn't save last typed message in input box * save input * change where the save occurs * working * add remove last method * now replaces most recent input * remove check for null value * now at least lets you see most recent commit * before adding objects * add scmi value class * add scmi value class * new commit * fix removal / insertion order * change function modifiers * working correctly * change conditional * undo inadvertant changes * Update README.md * fix tricky bug * working and removed unnecessary conditional * fix another bug * make elements private again * change order of save * now working as expected, about to add context keys * hook up context keys * save on shutdown * improve variable name * disable show prior/next commit when there's no history and ensure that input is last in history * formatting * add new history navigator * fix bad == * rename scm input history methods * adopt HistoryNavigator2 in SCMInput * remove unnecessary method * revert history.ts * :lipstick: * change size of history required to be valid * revert change * on reload, display latest input * remove rogue console.log * fix issue with saving uncommitted message Co-authored-by: João Moreno --- src/vs/base/common/history.ts | 92 +++++++++++++++++++ src/vs/workbench/api/browser/mainThreadSCM.ts | 2 +- .../contrib/scm/browser/scm.contribution.ts | 29 +++++- .../contrib/scm/browser/scmViewPane.ts | 16 +++- src/vs/workbench/contrib/scm/common/scm.ts | 6 +- .../contrib/scm/common/scmService.ts | 81 ++++++++++++---- 6 files changed, 202 insertions(+), 24 deletions(-) diff --git a/src/vs/base/common/history.ts b/src/vs/base/common/history.ts index f7b8d5ed64d..781067fe863 100644 --- a/src/vs/base/common/history.ts +++ b/src/vs/base/common/history.ts @@ -97,3 +97,95 @@ export class HistoryNavigator implements INavigator { return elements; } } + +interface HistoryNode { + value: T; + previous: HistoryNode | undefined; + next: HistoryNode | undefined; +} + +export class HistoryNavigator2 { + + private head: HistoryNode; + private tail: HistoryNode; + private cursor: HistoryNode; + private size: number; + + constructor(history: readonly T[], private capacity: number = 10) { + if (history.length < 1) { + throw new Error('not supported'); + } + + this.size = 1; + this.head = this.tail = this.cursor = { + value: history[0], + previous: undefined, + next: undefined + }; + + for (let i = 1; i < history.length; i++) { + this.add(history[i]); + } + } + + add(value: T): void { + const node: HistoryNode = { + value, + previous: this.tail, + next: undefined + }; + + this.tail.next = node; + this.tail = node; + this.cursor = this.tail; + this.size++; + + while (this.size > this.capacity) { + this.head = this.head.next!; + this.head.previous = undefined; + this.size--; + } + } + + replaceLast(value: T): void { + this.tail.value = value; + } + + isAtEnd(): boolean { + return this.cursor === this.tail; + } + + current(): T { + return this.cursor.value; + } + + previous(): T { + if (this.cursor.previous) { + this.cursor = this.cursor.previous; + } + + return this.cursor.value; + } + + next(): T { + if (this.cursor.next) { + this.cursor = this.cursor.next; + } + + return this.cursor.value; + } + + resetCursor(): T { + this.cursor = this.tail; + return this.cursor.value; + } + + *[Symbol.iterator](): Iterator { + let node: HistoryNode | undefined = this.head; + + while (node) { + yield node.value; + node = node.next; + } + } +} diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 9594c9137d6..0b70be0ca26 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -397,7 +397,7 @@ export class MainThreadSCM implements MainThreadSCMShape { return; } - repository.input.value = value; + repository.input.setValue(value, false); } $setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): void { diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index bf905250784..fd9bffda2f2 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -221,7 +221,6 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ if (!repository || !repository.provider.acceptInputCommand) { return Promise.resolve(null); } - const id = repository.provider.acceptInputCommand.id; const args = repository.provider.acceptInputCommand.arguments; @@ -230,6 +229,34 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'scm.viewNextCommit', + description: { description: localize('scm view next commit', "SCM: View Next Commit"), args: [] }, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.has('scmInputIsInLastLine'), + primary: KeyCode.DownArrow, + handler: accessor => { + const contextKeyService = accessor.get(IContextKeyService); + const context = contextKeyService.getContext(document.activeElement); + const repository = context.getValue('scmRepository'); + repository?.input.showNextHistoryValue(); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'scm.viewPriorCommit', + description: { description: localize('scm view prior commit', "SCM: View Prior Commit"), args: [] }, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.has('scmInputIsInFirstLine'), + primary: KeyCode.UpArrow, + handler: accessor => { + const contextKeyService = accessor.get(IContextKeyService); + const context = contextKeyService.getContext(document.activeElement); + const repository = context.getValue('scmRepository'); + repository?.input.showPreviousHistoryValue(); + } +}); + CommandsRegistry.registerCommand('scm.openInTerminal', async (accessor, provider: ISCMProvider) => { if (!provider || !provider.rootUri) { return; diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 89ae5f47db2..f175011ae4f 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1307,14 +1307,14 @@ class SCMInputWidget extends Disposable { if (value === textModel.getValue()) { // circuit breaker return; } - textModel.setValue(input.value); + textModel.setValue(value); this.inputEditor.setPosition(textModel.getFullModelRange().getEndPosition()); })); // Keep API in sync with model, update placeholder visibility and validate const updatePlaceholderVisibility = () => this.placeholderTextContainer.classList.toggle('hidden', textModel.getValueLength() > 0); this.repositoryDisposables.add(textModel.onDidChangeContent(() => { - input.value = textModel.getValue(); + input.setValue(textModel.getValue(), true); updatePlaceholderVisibility(); triggerValidation(); })); @@ -1433,6 +1433,18 @@ class SCMInputWidget extends Disposable { this.validationDisposable.dispose(); })); + const firstLineKey = contextKeyService2.createKey('scmInputIsInFirstLine', false); + const lastLineKey = contextKeyService2.createKey('scmInputIsInLastLine', false); + + this._register(this.inputEditor.onDidChangeCursorPosition(({ position }) => { + const viewModel = this.inputEditor._getViewModel()!; + const lastLineNumber = viewModel.getLineCount(); + const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(position); + + firstLineKey.set(viewPosition.lineNumber === 1); + lastLineKey.set(viewPosition.lineNumber === lastLineNumber); + })); + const onInputFontFamilyChanged = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.inputFontFamily')); this._register(onInputFontFamilyChanged(() => this.inputEditor.updateOptions({ fontFamily: this.getInputEditorFontFamily() }))); diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index aa3f57f7849..0f4640b971b 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -86,7 +86,8 @@ export interface IInputValidator { export interface ISCMInput { readonly repository: ISCMRepository; - value: string; + readonly value: string; + setValue(value: string, fromKeyboard: boolean): void; readonly onDidChange: Event; placeholder: string; @@ -97,6 +98,9 @@ export interface ISCMInput { visible: boolean; readonly onDidChangeVisibility: Event; + + showNextHistoryValue(): void; + showPreviousHistoryValue(): void; } export interface ISCMRepository extends IDisposable { diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index 50282ded800..b6dcb82722f 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -8,7 +8,8 @@ import { Event, Emitter } from 'vs/base/common/event'; import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository, IInputValidator } from './scm'; import { ILogService } from 'vs/platform/log/common/log'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, WillSaveStateReason } from 'vs/platform/storage/common/storage'; +import { HistoryNavigator2 } from 'vs/base/common/history'; class SCMInput implements ISCMInput { @@ -18,21 +19,6 @@ class SCMInput implements ISCMInput { return this._value; } - set value(value: string) { - if (value === this._value) { - return; - } - - this._value = value; - - if (this.repository.provider.rootUri) { - const key = `scm/input:${this.repository.provider.label}:${this.repository.provider.rootUri.path}`; - this.storageService.store(key, value, StorageScope.WORKSPACE); - } - - this._onDidChange.fire(value); - } - private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; @@ -79,14 +65,71 @@ class SCMInput implements ISCMInput { private readonly _onDidChangeValidateInput = new Emitter(); readonly onDidChangeValidateInput: Event = this._onDidChangeValidateInput.event; + private historyNavigator: HistoryNavigator2; + constructor( readonly repository: ISCMRepository, @IStorageService private storageService: IStorageService ) { - if (this.repository.provider.rootUri) { - const key = `scm/input:${this.repository.provider.label}:${this.repository.provider.rootUri.path}`; - this._value = this.storageService.get(key, StorageScope.WORKSPACE, ''); + const historyKey = `scm/input:${this.repository.provider.label}:${this.repository.provider.rootUri?.path}`; + let history: string[] | undefined; + let rawHistory = this.storageService.get(historyKey, StorageScope.WORKSPACE, ''); + + if (rawHistory) { + try { + history = JSON.parse(rawHistory); + } catch { + // noop + } } + + if (!history || history.length === 0) { + history = [this._value]; + } else { + this._value = history[history.length - 1]; + } + + this.historyNavigator = new HistoryNavigator2(history, 50); + + this.storageService.onWillSaveState(e => { + if (e.reason === WillSaveStateReason.SHUTDOWN) { + if (this.historyNavigator.isAtEnd()) { + this.historyNavigator.replaceLast(this._value); + } + + if (this.repository.provider.rootUri) { + this.storageService.store(historyKey, JSON.stringify([...this.historyNavigator]), StorageScope.WORKSPACE); + } + } + }); + } + + setValue(value: string, transient: boolean) { + if (value === this._value) { + return; + } + + if (!transient) { + this.historyNavigator.replaceLast(this._value); + this.historyNavigator.add(value); + } + + this._value = value; + this._onDidChange.fire(value); + } + + showNextHistoryValue(): void { + const value = this.historyNavigator.next(); + this.setValue(value, true); + } + + showPreviousHistoryValue(): void { + if (this.historyNavigator.isAtEnd()) { + this.historyNavigator.replaceLast(this._value); + } + + const value = this.historyNavigator.previous(); + this.setValue(value, true); } } From 0ccd7a95a3852713f70a1c89ddc9da78ee3e1d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Tue, 20 Oct 2020 10:29:36 +0200 Subject: [PATCH 050/212] remove deprecated code usages --- .../editor/contrib/parameterHints/parameterHintsWidget.ts | 6 +++--- src/vs/workbench/contrib/views/browser/treeView.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index e7ef8cad81a..2d0d4bcd349 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -192,7 +192,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { } const multiple = hints.signatures.length > 1; - dom.toggleClass(this.domNodes.element, 'multiple', multiple); + this.domNodes.element.classList.toggle('multiple', multiple); this.keyMultipleSignatures.set(multiple); this.domNodes.signature.innerText = ''; @@ -243,8 +243,8 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { const hasDocs = this.hasDocs(signature, activeParameter); - dom.toggleClass(this.domNodes.signature, 'has-docs', hasDocs); - dom.toggleClass(this.domNodes.docs, 'empty', !hasDocs); + this.domNodes.signature.classList.toggle('has-docs', hasDocs); + this.domNodes.docs.classList.toggle('empty', !hasDocs); this.domNodes.overloads.textContent = String(hints.activeSignature + 1).padStart(hints.signatures.length.toString().length, '0') + '/' + hints.signatures.length; diff --git a/src/vs/workbench/contrib/views/browser/treeView.ts b/src/vs/workbench/contrib/views/browser/treeView.ts index f11e9d412a5..2df022d0558 100644 --- a/src/vs/workbench/contrib/views/browser/treeView.ts +++ b/src/vs/workbench/contrib/views/browser/treeView.ts @@ -896,7 +896,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer Date: Tue, 20 Oct 2020 10:36:51 +0200 Subject: [PATCH 051/212] wip - resizable list and details --- .../editor/contrib/suggest/media/suggest.css | 138 +++---- src/vs/editor/contrib/suggest/resizable.ts | 100 +++-- .../editor/contrib/suggest/suggestWidget.ts | 341 +++++++----------- .../contrib/suggest/suggestWidgetDetails.ts | 121 +++++-- 4 files changed, 347 insertions(+), 353 deletions(-) diff --git a/src/vs/editor/contrib/suggest/media/suggest.css b/src/vs/editor/contrib/suggest/media/suggest.css index 4908df1a1d7..f9d229d006a 100644 --- a/src/vs/editor/contrib/suggest/media/suggest.css +++ b/src/vs/editor/contrib/suggest/media/suggest.css @@ -13,81 +13,19 @@ flex-direction: row; } -.monaco-editor .suggest-widget.docs-side { - flex-direction: row; -} - -.monaco-editor .suggest-widget.docs-side.reverse { - flex-direction: row-reverse; -} - -.monaco-editor .suggest-widget.docs-below { - flex-direction: column; -} - -.monaco-editor .suggest-widget.docs-below.reverse { - flex-direction: column-reverse; -} - -/* --- fiddle with details margin so that borders merge/overlap */ -.monaco-editor .suggest-widget.docs-side>.details { - margin: 0 0 0 -1px; -} -.monaco-editor .suggest-widget.docs-side.reverse>.details { - margin: 0 -1px 0 0; -} -.monaco-editor.hc-black .suggest-widget.docs-side>.details { - margin: 0 0 0 -2px; -} -.monaco-editor.hc-black .suggest-widget.docs-side.reverse>.details { - margin: 0 -2px 0 0; -} - -.monaco-editor .suggest-widget>.message, -.monaco-editor .suggest-widget>.tree, -.monaco-editor .suggest-widget>.details { +.monaco-editor .suggest-widget, +.monaco-editor .suggest-details { flex: 0 1 auto; width: 100%; border-style: solid; border-width: 1px; } -.monaco-editor .suggest-widget>.tree.docs-higher { - align-self: flex-end; -} - -.monaco-editor.hc-black .suggest-widget>.message, -.monaco-editor.hc-black .suggest-widget>.tree, -.monaco-editor.hc-black .suggest-widget>.details { +.monaco-editor.hc-black .suggest-widget, +.monaco-editor.hc-black .suggest-details { border-width: 2px; } -/** Adjust width when docs are expanded to the side **/ - -.monaco-editor .suggest-widget.docs-side { - width: 660px; -} - -.monaco-editor .suggest-widget.docs-side>.tree, -.monaco-editor .suggest-widget.docs-side>.details { - width: 50%; -} - -/* MarkupContent Layout */ - -.monaco-editor .suggest-widget>.details ul { - padding-left: 20px; -} - -.monaco-editor .suggest-widget>.details ol { - padding-left: 20px; -} - -.monaco-editor .suggest-widget>.details p code { - font-family: var(--monaco-monospace-font); -} - - /* Styles for status bar part */ @@ -146,6 +84,7 @@ .monaco-editor .suggest-widget>.tree { height: 100%; + width: 100%; } .monaco-editor .suggest-widget .monaco-list { @@ -193,7 +132,7 @@ /** ReadMore Icon styles **/ -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.header>.codicon-close, +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.header>.codicon-close, .monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.readMore::before { color: inherit; opacity: 1; @@ -201,13 +140,14 @@ cursor: pointer; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.header>.codicon-close { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.header>.codicon-close { position: absolute; top: 6px; right: 2px; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.header>.codicon-close:hover, .monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.readMore:hover { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.header>.codicon-close:hover, +.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.readMore:hover { opacity: 1; } @@ -248,11 +188,11 @@ /** Details: if using CompletionItem#details, show on focus **/ -.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.details-label, .monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row.focused>.contents>.main>.right>.details-label { +.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.details-label { display: none; } -.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused>.contents>.main>.right>.details-label { +.monaco-editor .suggest-widget:not(.shows-details) .monaco-list .monaco-list-row.focused>.contents>.main>.right>.details-label { display: inline; } @@ -319,7 +259,8 @@ display: inline-block; } -.monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row>.contents>.main>.right>.readMore, .monaco-editor .suggest-widget.docs-below .monaco-list .monaco-list-row>.contents>.main>.right>.readMore { +.monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row>.contents>.main>.right>.readMore, +.monaco-editor .suggest-widget.docs-below .monaco-list .monaco-list-row>.contents>.main>.right>.readMore { display: none; } @@ -376,39 +317,29 @@ /** Styles for the docs of the completion item in focus **/ -.monaco-editor .suggest-widget .details { +.monaco-editor .suggest-details { display: flex; flex-direction: column; cursor: default; + z-index: 41; } -.monaco-editor .suggest-widget .details.no-docs { +.monaco-editor .suggest-details.no-docs { display: none; } -.monaco-editor .suggest-widget.docs-below .details { - border-top-width: 0; - border-bottom-width: 1px; -} - -.monaco-editor .suggest-widget.docs-below.reverse .details { - border-bottom-width: 0; - border-top-width: 1px; -} - -.monaco-editor .suggest-widget .details>.monaco-scrollable-element { +.monaco-editor .suggest-details>.monaco-scrollable-element { flex: 1; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body { - position: absolute; +.monaco-editor .suggest-details>.monaco-scrollable-element>.body { box-sizing: border-box; height: 100%; width: 100%; padding-right: 22px; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.header>.type { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.header>.type { flex: 2; overflow: hidden; text-overflow: ellipsis; @@ -418,44 +349,57 @@ padding: 4px 0 12px 5px; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs { margin: 0; padding: 4px 5px; white-space: pre-wrap; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs { padding: 0; white-space: initial; min-height: calc(1rem + 8px); } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs>div, .monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs>span:not(:empty) { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs>div, +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs>span:not(:empty) { padding: 4px 5px; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs>div>p:first-child { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs>div>p:first-child { margin-top: 0; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs>div>p:last-child { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs>div>p:last-child { margin-bottom: 0; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs .code { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs .code { white-space: pre-wrap; word-wrap: break-word; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>.docs.markdown-docs .codicon { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs .codicon { vertical-align: sub; } -.monaco-editor .suggest-widget .details>.monaco-scrollable-element>.body>p:empty { +.monaco-editor .suggest-details>.monaco-scrollable-element>.body>p:empty { display: none; } -.monaco-editor .suggest-widget .details code { +.monaco-editor .suggest-details code { border-radius: 3px; padding: 0 0.4em; } + +.monaco-editor .suggest-details ul { + padding-left: 20px; +} + +.monaco-editor .suggest-details ol { + padding-left: 20px; +} + +.monaco-editor .suggest-details p code { + font-family: var(--monaco-monospace-font); +} diff --git a/src/vs/editor/contrib/suggest/resizable.ts b/src/vs/editor/contrib/suggest/resizable.ts index 0d81215d2e4..84e3efb4756 100644 --- a/src/vs/editor/contrib/suggest/resizable.ts +++ b/src/vs/editor/contrib/suggest/resizable.ts @@ -28,12 +28,15 @@ export class ResizableHTMLElement { private readonly _southSash: Sash; private readonly _sashListener = new DisposableStore(); - private _size?: Dimension; + private _size = new Dimension(0, 0); + private _minSize = new Dimension(0, 0); + private _maxSize = new Dimension(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); + private _preferredSize?: Dimension; constructor() { this.domNode = document.createElement('div'); - this._eastSash = new Sash(this.domNode, { getVerticalSashLeft: () => this._size?.width ?? 0 }, { orientation: Orientation.VERTICAL }); - this._southSash = new Sash(this.domNode, { getHorizontalSashTop: () => this._size?.height ?? 0 }, { orientation: Orientation.HORIZONTAL }); + this._eastSash = new Sash(this.domNode, { getVerticalSashLeft: () => this._size.width }, { orientation: Orientation.VERTICAL }); + this._southSash = new Sash(this.domNode, { getHorizontalSashTop: () => this._size.height }, { orientation: Orientation.HORIZONTAL }); this._eastSash.orthogonalEndSash = this._southSash; this._southSash.orthogonalEndSash = this._eastSash; @@ -43,30 +46,47 @@ export class ResizableHTMLElement { let deltaX = 0; this._sashListener.add(Event.any(this._eastSash.onDidStart, this._southSash.onDidStart)(() => { - this._onDidWillResize.fire(); - currentSize = this._size; - deltaY = 0; - deltaX = 0; - })); - this._sashListener.add(Event.any(this._eastSash.onDidEnd, this._southSash.onDidEnd)(() => { - currentSize = undefined; - deltaY = 0; - deltaX = 0; - this._onDidResize.fire({ dimenion: this._size!, done: false }); - })); - - this._sashListener.add(this._southSash.onDidChange(e => { - if (currentSize) { - deltaY = e.currentY - e.startY; - this.layout(currentSize.height + deltaY, currentSize.width + deltaX); - this._onDidResize.fire({ dimenion: this._size!, done: false }); + if (currentSize === undefined) { + this._onDidWillResize.fire(); + currentSize = this._size; + deltaY = 0; + deltaX = 0; } })); + this._sashListener.add(Event.any(this._eastSash.onDidEnd, this._southSash.onDidEnd)(() => { + if (currentSize !== undefined) { + currentSize = undefined; + deltaY = 0; + deltaX = 0; + this._onDidResize.fire({ dimenion: this._size, done: true }); + } + })); + this._sashListener.add(this._eastSash.onDidChange(e => { if (currentSize) { deltaX = e.currentX - e.startX; this.layout(currentSize.height + deltaY, currentSize.width + deltaX); - this._onDidResize.fire({ dimenion: this._size!, done: false }); + this._onDidResize.fire({ dimenion: this._size, done: false }); + } + })); + this._sashListener.add(this._southSash.onDidChange(e => { + if (currentSize) { + deltaY = e.currentY - e.startY; + this.layout(currentSize.height + deltaY, currentSize.width + deltaX); + this._onDidResize.fire({ dimenion: this._size, done: false }); + } + })); + + this._sashListener.add(this._eastSash.onDidReset(e => { + if (this._preferredSize) { + this.layout(this._size.height, this._preferredSize.width); + this._onDidResize.fire({ dimenion: this._size, done: true }); + } + })); + this._sashListener.add(this._southSash.onDidReset(e => { + if (this._preferredSize) { + this.layout(this._preferredSize.height, this._size.width); + this._onDidResize.fire({ dimenion: this._size, done: true }); } })); } @@ -78,7 +98,14 @@ export class ResizableHTMLElement { this.domNode.remove(); } - layout(height: number, width: number): void { + layout(height: number = this.size.height, width: number = this.size.width): void { + + const { height: minHeight, width: minWidth } = this._minSize; + const { height: maxHeight, width: maxWidth } = this._maxSize; + + height = Math.max(minHeight, Math.min(maxHeight, height)); + width = Math.max(minWidth, Math.min(maxWidth, width)); + const newSize = new Dimension(width, height); if (!Dimension.equals(newSize, this._size)) { this.domNode.style.height = height + 'px'; @@ -88,4 +115,33 @@ export class ResizableHTMLElement { this._eastSash.layout(); } } + + get size() { + return this._size; + } + + set maxSize(value: Dimension) { + this._maxSize = value; + } + + get maxSize() { + return this._maxSize; + } + + set minSize(value: Dimension) { + this._minSize = value; + } + + get minSize() { + return this._minSize; + } + + set preferredSize(value: Dimension | undefined) { + this._preferredSize = value; + } + + get preferredSize() { + return this._preferredSize; + } + } diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index f958ff0f669..abc02b281a3 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -8,11 +8,11 @@ import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles import 'vs/editor/contrib/documentSymbols/outlineTree'; // The codicon symbol colors are defined here and must be loaded import * as nls from 'vs/nls'; import * as strings from 'vs/base/common/strings'; +import * as dom from 'vs/base/browser/dom'; import { Event, Emitter } from 'vs/base/common/event'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; -import { append, $, hide, show, getDomNodePagePosition, addDisposableListener, addStandardDisposableListener } from 'vs/base/browser/dom'; -import { IListVirtualDelegate, IListEvent, IListMouseEvent, IListGestureEvent } from 'vs/base/browser/ui/list/list'; +import { IListEvent, IListMouseEvent, IListGestureEvent } from 'vs/base/browser/ui/list/list'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -31,7 +31,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { TimeoutTimer, CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { SuggestDetailsWidget, canExpandCompletionItem } from './suggestWidgetDetails'; +import { SuggestDetailsWidget, canExpandCompletionItem, SuggestDetailsOverlay } from './suggestWidgetDetails'; import { SuggestWidgetStatus } from 'vs/editor/contrib/suggest/suggestWidgetStatus'; import { getAriaId, ItemRenderer } from './suggestWidgetRenderer'; import { ResizableHTMLElement } from './resizable'; @@ -45,7 +45,6 @@ export const editorSuggestWidgetForeground = registerColor('editorSuggestWidget. export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', { dark: listFocusBackground, light: listFocusBackground, hc: listFocusBackground }, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.')); export const editorSuggestWidgetHighlightForeground = registerColor('editorSuggestWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hc: listHighlightForeground }, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.')); - const enum State { Hidden, Loading, @@ -55,19 +54,18 @@ const enum State { Details } - export interface ISelectedSuggestion { item: CompletionItem; index: number; model: CompletionModel; } -export class SuggestWidget implements IContentWidget, IListVirtualDelegate, IDisposable { +export class SuggestWidget implements IContentWidget, IDisposable { private static readonly ID: string = 'editor.widget.suggestWidget'; - static LOADING_MESSAGE: string = nls.localize('suggestWidget.loading', "Loading..."); - static NO_SUGGESTIONS_MESSAGE: string = nls.localize('suggestWidget.noSuggestions', "No suggestions."); + private static LOADING_MESSAGE: string = nls.localize('suggestWidget.loading', "Loading..."); + private static NO_SUGGESTIONS_MESSAGE: string = nls.localize('suggestWidget.noSuggestions', "No suggestions."); // Editor.IContentWidget.allowEditorOverflow readonly allowEditorOverflow = true; @@ -88,7 +86,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate; private status: SuggestWidgetStatus; - private details: SuggestDetailsWidget; + private _details: SuggestDetailsOverlay; private readonly ctxSuggestWidgetVisible: IContextKey; private readonly ctxSuggestWidgetDetailsVisible: IContextKey; @@ -107,15 +105,10 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate = this.onDidHideEmitter.event; readonly onDidShow: Event = this.onDidShowEmitter.event; - private readonly maxWidgetWidth = 660; - private readonly listWidth = 330; + private detailsFocusBorderColor?: string; private detailsBorderColor?: string; - private firstFocusInCurrentList: boolean = false; - - private preferDocPositionTop: boolean = false; - private docsPositionPreviousWidgetY?: number; private explainMode: boolean = false; private readonly _onDetailsKeydown = new Emitter(); @@ -137,32 +130,38 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate { - this.layout(e.dimenion.height, e.dimenion.width); - })); - this._disposables.add(addDisposableListener(this.element.domNode, 'click', e => { - if (e.target === this.element.domNode) { - this.hideWidget(); - } - })); - this.messageElement = append(this.element.domNode, $('.message')); - this.mainElement = append(this.element.domNode, $('.tree')); + this._disposables.add(this.element.onDidWillResize(() => { + // update min/max sizes + const bodyBox = dom.getClientArea(document.body); + const pagePos = dom.getDomNodePagePosition(this.element.domNode); + const maxHeight = bodyBox.height - pagePos.top; + const maxWidth = bodyBox.width - pagePos.left; + this.element.maxSize = new dom.Dimension(maxWidth, maxHeight); + this.element.minSize = new dom.Dimension(220, this._itemHeight); + })); + this._disposables.add(this.element.onDidResize(e => this._layout(e.dimenion.height, e.dimenion.width))); - this.details = instantiationService.createInstance(SuggestDetailsWidget, this.element.domNode, this.editor, markdownRenderer, kbToggleDetails); - this.details.onDidClose(this.toggleDetails, this, this._disposables); - hide(this.details.element); + this.messageElement = dom.append(this.element.domNode, dom.$('.message')); + this.mainElement = dom.append(this.element.domNode, dom.$('.tree')); + + const details = instantiationService.createInstance(SuggestDetailsWidget, this.editor, markdownRenderer, kbToggleDetails); + details.onDidClose(this.toggleDetails, this, this._disposables); + this._details = new SuggestDetailsOverlay(details, this.editor); const applyIconStyle = () => this.element.domNode.classList.toggle('no-icons', !this.editor.getOption(EditorOption.suggest).showIcons); applyIconStyle(); - this.listContainer = append(this.mainElement, $('.list-container')); + this.listContainer = dom.append(this.mainElement, dom.$('.list-container')); const renderer = instantiationService.createInstance(ItemRenderer, this.editor, kbToggleDetails); this._disposables.add(renderer); this._disposables.add(renderer.onDidToggleDetails(() => this.toggleDetails())); - this.list = new List('SuggestWidget', this.listContainer, this, [renderer], { + this.list = new List('SuggestWidget', this.listContainer, { + getHeight: (_element: CompletionItem): number => this._itemHeight, + getTemplateId: (_element: CompletionItem): string => 'suggestion' + }, [renderer], { useShadows: false, mouseSupport: false, accessibilityProvider: { @@ -195,7 +194,8 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate this.onThemeChange(t))); - this._disposables.add(editor.onDidLayoutChange(() => this.onEditorLayoutChange())); + this.onThemeChange(themeService.getColorTheme()); + this._disposables.add(this.list.onMouseDown(e => this.onListMouseDownOrTap(e))); this._disposables.add(this.list.onTap(e => this.onListMouseDownOrTap(e))); this._disposables.add(this.list.onDidChangeSelection(e => this.onListSelection(e))); @@ -212,9 +212,8 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate { + this._disposables.add(dom.addStandardDisposableListener(this._details.widget.domNode, 'keydown', e => { this._onDetailsKeydown.fire(e); })); @@ -222,7 +221,8 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate | IListGestureEvent): void { if (typeof e.element === 'undefined' || typeof e.index === 'undefined') { return; @@ -291,23 +285,23 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate): void { @@ -333,7 +327,6 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate 1); @@ -509,7 +501,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate { @@ -696,6 +670,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate