From 033705cd7aefe044a620afca08a1a131c502ddf0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 7 Dec 2023 08:36:33 -0800 Subject: [PATCH] Move to new module, add tests --- .../terminal/browser/terminalInstance.ts | 98 ++----------------- .../terminal/common/terminalClipboard.ts | 87 ++++++++++++++++ .../test/common/terminalClipboard.test.ts | 80 +++++++++++++++ 3 files changed, 176 insertions(+), 89 deletions(-) create mode 100644 src/vs/workbench/contrib/terminal/common/terminalClipboard.ts create mode 100644 src/vs/workbench/contrib/terminal/test/common/terminalClipboard.test.ts diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 59a7e0e0297..2ecff99d865 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -31,7 +31,6 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { CodeDataTransfers, containsDragType } from 'vs/platform/dnd/browser/dnd'; import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -87,6 +86,7 @@ import { importAMDNodeModule } from 'vs/amdX'; import type { IMarker, Terminal as XTermTerminal } from '@xterm/xterm'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; +import { shouldPasteTerminalText } from 'vs/workbench/contrib/terminal/common/terminalClipboard'; const enum Constants { /** @@ -346,7 +346,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { @IThemeService private readonly _themeService: IThemeService, @IConfigurationService private readonly _configurationService: IConfigurationService, @ITerminalLogService private readonly _logService: ITerminalLogService, - @IDialogService private readonly _dialogService: IDialogService, @IStorageService private readonly _storageService: IStorageService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IProductService private readonly _productService: IProductService, @@ -1094,80 +1093,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._terminalAltBufferActiveContextKey.set(!!(this.xterm && this.xterm.raw.buffer.active === this.xterm.raw.buffer.alternate)); } - private async _shouldPasteText(text: string): Promise { - // If the clipboard has only one line, a warning should never show - const textForLines = text.split(/\r?\n/); - if (textForLines.length === 1) { - return true; - } - - // Get config value - function parseConfigValue(value: unknown): 'auto' | 'always' | 'never' { - // Valid value - if (typeof value === 'string') { - if (value === 'auto' || value === 'always' || value === 'never') { - return value; - } - } - // Legacy backwards compatibility - if (typeof value === 'boolean') { - return value ? 'auto' : 'never'; - } - // Invalid value fallback - return 'auto'; - } - const configValue = parseConfigValue(this._configurationService.getValue(TerminalSettingId.EnableMultiLinePasteWarning)); - - // Never show it - if (configValue === 'never') { - return true; - } - - // Special edge cases to not show for auto - if (configValue === 'auto') { - // Ignore check if the shell is in bracketed paste mode (ie. the shell can handle multi-line - // text). - if (this.xterm?.raw.modes.bracketedPasteMode) { - return true; - } - - const textForLines = text.split(/\r?\n/); - // Ignore check when a command is copied with a trailing new line - if (textForLines.length === 2 && textForLines[1].trim().length === 0) { - return true; - } - } - - const displayItemsCount = 3; - const maxPreviewLineLength = 30; - - let detail = nls.localize('preview', "Preview:"); - for (let i = 0; i < Math.min(textForLines.length, displayItemsCount); i++) { - const line = textForLines[i]; - const cleanedLine = line.length > maxPreviewLineLength ? `${line.slice(0, maxPreviewLineLength)}…` : line; - detail += `\n${cleanedLine}`; - } - - if (textForLines.length > displayItemsCount) { - detail += `\n…`; - } - - const { confirmed, checkboxChecked } = await this._dialogService.confirm({ - message: nls.localize('confirmMoveTrashMessageFilesAndDirectories', "Are you sure you want to paste {0} lines of text into the terminal?", textForLines.length), - detail, - primaryButton: nls.localize({ key: 'multiLinePasteButton', comment: ['&& denotes a mnemonic'] }, "&&Paste"), - checkbox: { - label: nls.localize('doNotAskAgain', "Do not ask me again") - } - }); - - if (confirmed && checkboxChecked) { - await this._configurationService.updateValue(TerminalSettingId.EnableMultiLinePasteWarning, false); - } - - return confirmed; - } - override dispose(reason?: TerminalExitReason): void { if (this.isDisposed) { return; @@ -1246,26 +1171,21 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } async paste(): Promise { - if (!this.xterm) { - return; - } - - const currentText: string = await this._clipboardService.readText(); - if (!await this._shouldPasteText(currentText)) { - return; - } - - this.focus(); - this.xterm.raw.paste(currentText); + await this._paste(await this._clipboardService.readText()); } async pasteSelection(): Promise { + await this._paste(await this._clipboardService.readText('selection')); + } + + private async _paste(value: string): Promise { if (!this.xterm) { return; } - const currentText: string = await this._clipboardService.readText('selection'); - if (!await this._shouldPasteText(currentText)) { + const currentText: string = value; + const shouldPasteText = await this._scopedInstantiationService.invokeFunction(shouldPasteTerminalText, currentText, this.xterm?.raw.modes.bracketedPasteMode); + if (!shouldPasteText) { return; } diff --git a/src/vs/workbench/contrib/terminal/common/terminalClipboard.ts b/src/vs/workbench/contrib/terminal/common/terminalClipboard.ts new file mode 100644 index 00000000000..2e529b6e637 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/common/terminalClipboard.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; + +export async function shouldPasteTerminalText(accessor: ServicesAccessor, text: string, bracketedPasteMode: boolean | undefined): Promise { + const configurationService = accessor.get(IConfigurationService); + const dialogService = accessor.get(IDialogService); + + // If the clipboard has only one line, a warning should never show + const textForLines = text.split(/\r?\n/); + if (textForLines.length === 1) { + return true; + } + + // Get config value + function parseConfigValue(value: unknown): 'auto' | 'always' | 'never' { + // Valid value + if (typeof value === 'string') { + if (value === 'auto' || value === 'always' || value === 'never') { + return value; + } + } + // Legacy backwards compatibility + if (typeof value === 'boolean') { + return value ? 'auto' : 'never'; + } + // Invalid value fallback + return 'auto'; + } + const configValue = parseConfigValue(configurationService.getValue(TerminalSettingId.EnableMultiLinePasteWarning)); + + // Never show it + if (configValue === 'never') { + return true; + } + + // Special edge cases to not show for auto + if (configValue === 'auto') { + // Ignore check if the shell is in bracketed paste mode (ie. the shell can handle multi-line + // text). + if (bracketedPasteMode) { + return true; + } + + const textForLines = text.split(/\r?\n/); + // Ignore check when a command is copied with a trailing new line + if (textForLines.length === 2 && textForLines[1].trim().length === 0) { + return true; + } + } + + const displayItemsCount = 3; + const maxPreviewLineLength = 30; + + let detail = localize('preview', "Preview:"); + for (let i = 0; i < Math.min(textForLines.length, displayItemsCount); i++) { + const line = textForLines[i]; + const cleanedLine = line.length > maxPreviewLineLength ? `${line.slice(0, maxPreviewLineLength)}…` : line; + detail += `\n${cleanedLine}`; + } + + if (textForLines.length > displayItemsCount) { + detail += `\n…`; + } + + const { confirmed, checkboxChecked } = await dialogService.confirm({ + message: localize('confirmMoveTrashMessageFilesAndDirectories', "Are you sure you want to paste {0} lines of text into the terminal?", textForLines.length), + detail, + primaryButton: localize({ key: 'multiLinePasteButton', comment: ['&& denotes a mnemonic'] }, "&&Paste"), + checkbox: { + label: localize('doNotAskAgain', "Do not ask me again") + } + }); + + if (confirmed && checkboxChecked) { + await configurationService.updateValue(TerminalSettingId.EnableMultiLinePasteWarning, false); + } + + return confirmed; +} diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalClipboard.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalClipboard.test.ts new file mode 100644 index 00000000000..55d4f94ade4 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/test/common/terminalClipboard.test.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { strictEqual } from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { shouldPasteTerminalText } from 'vs/workbench/contrib/terminal/common/terminalClipboard'; + +suite('TerminalClipboard', function () { + + suite('shouldPasteTerminalText', () => { + let instantiationService: TestInstantiationService; + let configurationService: TestConfigurationService; + let dialogService: TestDialogService; + + setup(async () => { + instantiationService = new TestInstantiationService(); + configurationService = new TestConfigurationService({ + [TerminalSettingId.EnableMultiLinePasteWarning]: 'auto' + }); + dialogService = new TestDialogService({ confirmed: false }); + + instantiationService.stub(IConfigurationService, configurationService); + instantiationService.stub(IDialogService, dialogService); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + function setConfigValue(value: unknown) { + configurationService = new TestConfigurationService({ + [TerminalSettingId.EnableMultiLinePasteWarning]: value + }); + instantiationService.stub(IConfigurationService, configurationService); + } + + test('Single line string', async () => { + strictEqual(await instantiationService.invokeFunction(shouldPasteTerminalText, 'foo', undefined), true); + + setConfigValue('always'); + strictEqual(await instantiationService.invokeFunction(shouldPasteTerminalText, 'foo', undefined), true); + + setConfigValue('never'); + strictEqual(await instantiationService.invokeFunction(shouldPasteTerminalText, 'foo', undefined), true); + }); + test('Single line string with trailing new line', async () => { + strictEqual(await instantiationService.invokeFunction(shouldPasteTerminalText, 'foo\n', undefined), true); + + setConfigValue('always'); + strictEqual(await instantiationService.invokeFunction(shouldPasteTerminalText, 'foo\n', undefined), false); + + setConfigValue('never'); + strictEqual(await instantiationService.invokeFunction(shouldPasteTerminalText, 'foo\n', undefined), true); + }); + test('Multi-line string', async () => { + strictEqual(await instantiationService.invokeFunction(shouldPasteTerminalText, 'foo\nbar', undefined), false); + + setConfigValue('always'); + strictEqual(await instantiationService.invokeFunction(shouldPasteTerminalText, 'foo\nbar', undefined), false); + + setConfigValue('never'); + strictEqual(await instantiationService.invokeFunction(shouldPasteTerminalText, 'foo\nbar', undefined), true); + }); + test('Bracketed paste mode', async () => { + strictEqual(await instantiationService.invokeFunction(shouldPasteTerminalText, 'foo\nbar', true), true); + + setConfigValue('always'); + strictEqual(await instantiationService.invokeFunction(shouldPasteTerminalText, 'foo\nbar', true), false); + + setConfigValue('never'); + strictEqual(await instantiationService.invokeFunction(shouldPasteTerminalText, 'foo\nbar', true), true); + }); + }); +});