Move to new module, add tests

This commit is contained in:
Daniel Imms
2023-12-07 08:36:33 -08:00
parent 5fc9540a47
commit 033705cd7a
3 changed files with 176 additions and 89 deletions
@@ -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<boolean> {
// 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<void> {
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<void> {
await this._paste(await this._clipboardService.readText('selection'));
}
private async _paste(value: string): Promise<void> {
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;
}
@@ -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<boolean> {
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;
}
@@ -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);
});
});
});