From 54d81cd618fbff092bfdf030f0116d1d82b69366 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 4 Nov 2024 16:30:09 -0800 Subject: [PATCH] Add paste as default settings and enable js/ts paste with imports by default (#233031) Fixes #184871 For #30066 Adds new settings that let you configure the default way to paste/drop. Also enables js/ts paste with imports by default for 5.7+. However will not apply by default. Instead it will be shown as an option after pasting. You can then use the `editor.pasteAs.preferences` setting to make it apply automatically or use the `javascript.updateImportsOnPaste.enabled` settings to disable the feature entirely --- .../typescript-language-features/package.json | 18 ++-- .../package.nls.json | 2 +- .../src/languageFeatures/copyPaste.ts | 8 +- .../browser/copyPasteContribution.ts | 40 +++++++- .../browser/copyPasteController.ts | 92 +++++++++++++++---- .../browser/dropIntoEditorContribution.ts | 40 +++++--- .../browser/dropIntoEditorController.ts | 52 +++++++---- .../dropOrPasteInto/browser/postEditWidget.ts | 64 +++++++------ .../actionWidget/browser/actionList.ts | 7 +- .../browser/dropOrPasteInto.contribution.ts | 46 ++++++++++ src/vs/workbench/workbench.common.main.ts | 4 + 11 files changed, 277 insertions(+), 96 deletions(-) create mode 100644 src/vs/workbench/contrib/dropOrPasteInto/browser/dropOrPasteInto.contribution.ts diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index f21c9227e273b..c6363dfc48e16 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -1506,23 +1506,17 @@ "description": "%typescript.tsserver.enableRegionDiagnostics%", "scope": "window" }, - "javascript.experimental.updateImportsOnPaste": { + "javascript.updateImportsOnPaste.enabled": { "scope": "window", "type": "boolean", - "default": false, - "description": "%configuration.updateImportsOnPaste%", - "tags": [ - "experimental" - ] + "default": true, + "markdownDescription": "%configuration.updateImportsOnPaste%" }, - "typescript.experimental.updateImportsOnPaste": { + "typescript.updateImportsOnPaste.enabled": { "scope": "window", "type": "boolean", - "default": false, - "description": "%configuration.updateImportsOnPaste%", - "tags": [ - "experimental" - ] + "default": true, + "markdownDescription": "%configuration.updateImportsOnPaste%" }, "typescript.experimental.expandableHover": { "type": "boolean", diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index adf2fe31566a2..22fa2fc8345bc 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -224,7 +224,7 @@ "configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors": "Suppresses semantic errors on web even when project wide IntelliSense is enabled. This is always on when project wide IntelliSense is not enabled or available. See `#typescript.tsserver.web.projectWideIntellisense.enabled#`", "configuration.tsserver.web.typeAcquisition.enabled": "Enable/disable package acquisition on the web. This enables IntelliSense for imported packages. Requires `#typescript.tsserver.web.projectWideIntellisense.enabled#`. Currently not supported for Safari.", "configuration.tsserver.nodePath": "Run TS Server on a custom Node installation. This can be a path to a Node executable, or 'node' if you want VS Code to detect a Node installation.", - "configuration.updateImportsOnPaste": "Automatically update imports when pasting code. Requires TypeScript 5.7+.", + "configuration.updateImportsOnPaste": "Enable updating imports when pasting code. Requires TypeScript 5.7+.\n\nBy default this shows a option to update imports after pasting. You can use the `#editor.pasteAs.preferences#` setting to update imports automatically when pasting: `\"editor.pasteAs.preferences\": [{ \"kind\": \"text.jsts.pasteWithImports\" }]`.", "configuration.expandableHover": "Enable/disable expanding on hover.", "walkthroughs.nodejsWelcome.title": "Get started with JavaScript and Node.js", "walkthroughs.nodejsWelcome.description": "Make the most of Visual Studio Code's first-class JavaScript experience.", diff --git a/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts index ca3f7708397d9..76f9d6a5130f4 100644 --- a/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts +++ b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts @@ -38,7 +38,7 @@ class CopyMetadata { } } -const settingId = 'experimental.updateImportsOnPaste'; +const enabledSettingId = 'updateImportsOnPaste.enabled'; class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { @@ -127,6 +127,8 @@ class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { } const edit = new vscode.DocumentPasteEdit('', vscode.l10n.t("Paste with imports"), DocumentPasteProvider.kind); + edit.yieldTo = [vscode.DocumentDropOrPasteEditKind.Empty.append('text', 'plain')]; + const additionalEdit = new vscode.WorkspaceEdit(); for (const edit of response.body.edits) { additionalEdit.set(this._client.toResource(edit.fileName), edit.textChanges.map(typeConverters.TextEdit.fromCodeEdit)); @@ -146,7 +148,7 @@ class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { private isEnabled(document: vscode.TextDocument) { const config = vscode.workspace.getConfiguration(this._modeId, document.uri); - return config.get(settingId, false); + return config.get(enabledSettingId, false); } } @@ -154,7 +156,7 @@ export function register(selector: DocumentSelector, language: LanguageDescripti return conditionalRegistration([ requireSomeCapability(client, ClientCapability.Semantic), requireMinVersion(client, API.v570), - requireGlobalConfiguration(language.id, settingId), + requireGlobalConfiguration(language.id, enabledSettingId), ], () => { return vscode.languages.registerDocumentPasteEditProvider(selector.semantic, new DocumentPasteProvider(language.id, client), { providedPasteEditKinds: [DocumentPasteProvider.kind], diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.ts index f4f13d510a85e..7e3b4f45e581b 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteContribution.ts @@ -6,14 +6,17 @@ import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; import { IJSONSchema, SchemaToType } from '../../../../base/common/jsonSchema.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; +import * as nls from '../../../../nls.js'; +import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { EditorAction, EditorCommand, EditorContributionInstantiation, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution } from '../../../browser/editorExtensions.js'; +import { editorConfigurationBaseNode } from '../../../common/config/editorConfigurationSchema.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; import { registerEditorFeature } from '../../../common/editorFeatures.js'; -import { CopyPasteController, changePasteTypeCommandId, pasteWidgetVisibleCtx } from './copyPasteController.js'; +import { CopyPasteController, changePasteTypeCommandId, pasteWidgetVisibleCtx, pasteAsPreferenceConfig } from './copyPasteController.js'; import { DefaultPasteProvidersFeature, DefaultTextPasteOrDropEditProvider } from './defaultProviders.js'; -import * as nls from '../../../../nls.js'; -import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; registerEditorContribution(CopyPasteController.ID, CopyPasteController, EditorContributionInstantiation.Eager); // eager because it listens to events on the container dom node of the editor registerEditorFeature(DefaultPasteProvidersFeature); @@ -105,3 +108,34 @@ registerEditorAction(class extends EditorAction { return CopyPasteController.get(editor)?.pasteAs({ providerId: DefaultTextPasteOrDropEditProvider.id }); } }); + +export type PreferredPasteConfiguration = ReadonlyArray<{ readonly kind: string; readonly mimeType?: string }>; + +Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + ...editorConfigurationBaseNode, + properties: { + [pasteAsPreferenceConfig]: { + type: 'array', + scope: ConfigurationScope.LANGUAGE_OVERRIDABLE, + description: nls.localize('preferredDescription', "Configures the preferred type of edit to use when pasting content.\n\nThis is an ordered list of edit kinds with optional mime types for the content being pasted. The first available edit of a preferred kind will be used."), + default: [], + items: { + type: 'object', + required: ['kind'], + properties: { + mimeType: { + type: 'string', + description: nls.localize('mimeType', "The optional mime type that this preference applies to. If not provided, the preference will be used for all mime types."), + }, + kind: { + type: 'string', + description: nls.localize('kind', "The kind identifier of the paste edit."), + } + }, + defaultSnippets: [ + { body: { kind: '$1' } } + ] + } + }, + } +}); diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index 5f10ec82e0c25..27bb02fc347a5 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -4,15 +4,26 @@ *--------------------------------------------------------------------------------------------*/ import { addDisposableListener, getActiveDocument } from '../../../../base/browser/dom.js'; +import { IAction } from '../../../../base/common/actions.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { CancelablePromise, createCancelablePromise, DeferredPromise, raceCancellation } from '../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { UriList, VSDataTransfer, createStringDataTransferItem, matchesMimeType } from '../../../../base/common/dataTransfer.js'; +import { createStringDataTransferItem, matchesMimeType, UriList, VSDataTransfer } from '../../../../base/common/dataTransfer.js'; +import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'; import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { Mimes } from '../../../../base/common/mime.js'; import * as platform from '../../../../base/common/platform.js'; +import { upcast } from '../../../../base/common/types.js'; import { generateUuid } from '../../../../base/common/uuid.js'; +import { localize } from '../../../../nls.js'; +import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; +import { ClipboardEventUtils } from '../../../browser/controller/editContext/textArea/textAreaEditContextInput.js'; import { toExternalVSDataTransfer, toVSDataTransfer } from '../../../browser/dnd.js'; import { ICodeEditor, PastePayload } from '../../../browser/editorBrowser.js'; import { IBulkEditService } from '../../../browser/services/bulkEditService.js'; @@ -23,26 +34,21 @@ import { Handler, IEditorContribution } from '../../../common/editorCommon.js'; import { DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteTriggerKind } from '../../../common/languages.js'; import { ITextModel } from '../../../common/model.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; -import { DefaultTextPasteOrDropEditProvider } from './defaultProviders.js'; -import { createCombinedWorkspaceEdit, sortEditsByYieldTo } from './edit.js'; import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from '../../editorState/browser/editorState.js'; import { InlineProgressManager } from '../../inlineProgress/browser/inlineProgress.js'; import { MessageController } from '../../message/browser/messageController.js'; -import { localize } from '../../../../nls.js'; -import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; -import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; -import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; +import { PreferredPasteConfiguration } from './copyPasteContribution.js'; +import { DefaultTextPasteOrDropEditProvider } from './defaultProviders.js'; +import { createCombinedWorkspaceEdit, sortEditsByYieldTo } from './edit.js'; import { PostEditWidgetManager } from './postEditWidget.js'; -import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'; -import { ClipboardEventUtils } from '../../../browser/controller/editContext/textArea/textAreaEditContextInput.js'; export const changePasteTypeCommandId = 'editor.changePasteType'; +export const pasteAsPreferenceConfig = 'editor.pasteAs.preferences'; + export const pasteWidgetVisibleCtx = new RawContextKey('pasteWidgetVisible', false, localize('pasteWidgetVisible', "Whether the paste widget is showing")); -const vscodeClipboardMime = 'application/vnd.code.copyMetadata'; +const vscodeClipboardMime = 'application/vnd.code.copymetadata'; interface CopyMetadata { readonly id?: string; @@ -73,6 +79,12 @@ export class CopyPasteController extends Disposable implements IEditorContributi return editor.getContribution(CopyPasteController.ID); } + public static setConfigureDefaultAction(action: IAction) { + CopyPasteController._configureDefaultAction = action; + } + + private static _configureDefaultAction?: IAction; + /** * Global tracking the last copy operation. * @@ -98,6 +110,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi @IInstantiationService instantiationService: IInstantiationService, @IBulkEditService private readonly _bulkEditService: IBulkEditService, @IClipboardService private readonly _clipboardService: IClipboardService, + @IConfigurationService private readonly _configService: IConfigurationService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @IProgressService private readonly _progressService: IProgressService, @@ -113,7 +126,10 @@ export class CopyPasteController extends Disposable implements IEditorContributi this._pasteProgressManager = this._register(new InlineProgressManager('pasteIntoEditor', editor, instantiationService)); - this._postPasteWidgetManager = this._register(instantiationService.createInstance(PostEditWidgetManager, 'pasteIntoEditor', editor, pasteWidgetVisibleCtx, { id: changePasteTypeCommandId, label: localize('postPasteWidgetTitle', "Show paste options...") })); + this._postPasteWidgetManager = this._register(instantiationService.createInstance(PostEditWidgetManager, 'pasteIntoEditor', editor, pasteWidgetVisibleCtx, + { id: changePasteTypeCommandId, label: localize('postPasteWidgetTitle', "Show paste options...") }, + () => CopyPasteController._configureDefaultAction ? [CopyPasteController._configureDefaultAction] : [] + )); } public changePasteType() { @@ -354,7 +370,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi if (editSession.edits.length) { const canShowWidget = editor.getOption(EditorOption.pasteAs).showPasteSelector === 'afterPaste'; - return this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections, { activeEditIndex: 0, allEdits: editSession.edits }, canShowWidget, (edit, token) => { + return this._postPasteWidgetManager.applyEditAndShowIfNeeded(selections, { activeEditIndex: this.getInitialActiveEditIndex(model, editSession.edits), allEdits: editSession.edits }, canShowWidget, (edit, token) => { return new Promise((resolve, reject) => { (async () => { try { @@ -464,14 +480,36 @@ export class CopyPasteController extends Disposable implements IEditorContributi if (preference) { pickedEdit = editSession.edits.at(0); } else { - const selected = await this._quickInputService.pick( - editSession.edits.map((edit): IQuickPickItem & { edit: DocumentPasteEdit } => ({ - label: edit.title, - description: edit.kind?.value, - edit, - })), { + type ItemWithEdit = IQuickPickItem & { edit?: DocumentPasteEdit }; + const configureDefaultItem: ItemWithEdit = { + id: 'editor.pasteAs.default', + label: localize('pasteAsDefault', "Configure default paste action"), + edit: undefined, + }; + + const selected = await this._quickInputService.pick( + [ + ...editSession.edits.map((edit): ItemWithEdit => ({ + label: edit.title, + description: edit.kind?.value, + edit, + })), + ...(CopyPasteController._configureDefaultAction ? [ + upcast({ type: 'separator' }), + { + label: CopyPasteController._configureDefaultAction.label, + edit: undefined, + } + ] : []) + ], { placeHolder: localize('pasteAsPickerPlaceholder', "Select Paste Action"), }); + + if (selected === configureDefaultItem) { + CopyPasteController._configureDefaultAction?.run(); + return; + } + pickedEdit = selected?.edit; } @@ -621,4 +659,18 @@ export class CopyPasteController extends Disposable implements IEditorContributi return provider.id === preference.providerId; } } + + private getInitialActiveEditIndex(model: ITextModel, edits: readonly DocumentPasteEdit[]) { + const preferredProviders = this._configService.getValue(pasteAsPreferenceConfig, { resource: model.uri }); + for (const config of Array.isArray(preferredProviders) ? preferredProviders : []) { + const desiredKind = new HierarchicalKind(config.kind); + const editIndex = edits.findIndex(edit => + desiredKind.contains(edit.kind) + && (!config.mimeType || (edit.handledMimeType && matchesMimeType(config.mimeType, [edit.handledMimeType])))); + if (editIndex >= 0) { + return editIndex; + } + } + return 0; + } } diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution.ts index 7af24cf81e831..9133698f812e0 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution.ts @@ -4,16 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; +import * as nls from '../../../../nls.js'; +import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { EditorCommand, EditorContributionInstantiation, ServicesAccessor, registerEditorCommand, registerEditorContribution } from '../../../browser/editorExtensions.js'; import { editorConfigurationBaseNode } from '../../../common/config/editorConfigurationSchema.js'; import { registerEditorFeature } from '../../../common/editorFeatures.js'; import { DefaultDropProvidersFeature } from './defaultProviders.js'; -import * as nls from '../../../../nls.js'; -import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; -import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { DropIntoEditorController, changeDropTypeCommandId, defaultProviderConfig, dropWidgetVisibleCtx } from './dropIntoEditorController.js'; +import { DropIntoEditorController, changeDropTypeCommandId, dropAsPreferenceConfig, dropWidgetVisibleCtx } from './dropIntoEditorController.js'; registerEditorContribution(DropIntoEditorController.ID, DropIntoEditorController, EditorContributionInstantiation.BeforeFirstInteraction); registerEditorFeature(DefaultDropProvidersFeature); @@ -52,17 +52,33 @@ registerEditorCommand(new class extends EditorCommand { } }); +export type PreferredDropConfiguration = ReadonlyArray<{ readonly kind: string; readonly mimeType?: string }>; + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ ...editorConfigurationBaseNode, properties: { - [defaultProviderConfig]: { - type: 'object', + [dropAsPreferenceConfig]: { + type: 'array', scope: ConfigurationScope.LANGUAGE_OVERRIDABLE, - description: nls.localize('defaultProviderDescription', "Configures the default drop provider to use for content of a given mime type."), - default: {}, - additionalProperties: { - type: 'string', - }, + description: nls.localize('preferredDescription', "Configures the preferred type of edit to use when dropping content.\n\nThis is an ordered list of edit kinds with optional mime types for the content being dropped. The first available edit of a preferred kind will be used."), + default: [], + items: { + type: 'object', + required: ['kind'], + properties: { + mimeType: { + type: 'string', + description: nls.localize('mimeType', "The optional mime type that this preference applies to. If not provided, the preference will be used for all mime types."), + }, + kind: { + type: 'string', + description: nls.localize('kind', "The kind identifier of the drop edit."), + } + }, + defaultSnippets: [ + { body: { kind: '$1' } } + ] + } }, } }); diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts index da8ef9147c79a..e6285447a14c3 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts @@ -28,8 +28,12 @@ import { LocalSelectionTransfer } from '../../../../platform/dnd/browser/dnd.js' import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { sortEditsByYieldTo } from './edit.js'; import { PostEditWidgetManager } from './postEditWidget.js'; +import { isCancellationError } from '../../../../base/common/errors.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { PreferredDropConfiguration } from './dropIntoEditorContribution.js'; +import { IAction } from '../../../../base/common/actions.js'; -export const defaultProviderConfig = 'editor.experimental.dropIntoEditor.defaultProvider'; +export const dropAsPreferenceConfig = 'editor.dropIntoEditor.preferences'; export const changeDropTypeCommandId = 'editor.changeDropType'; @@ -43,7 +47,18 @@ export class DropIntoEditorController extends Disposable implements IEditorContr return editor.getContribution(DropIntoEditorController.ID); } - private _currentOperation?: CancelablePromise; + public static setConfigureDefaultAction(action: IAction) { + this._configureDefaultAction = action; + } + + private static _configureDefaultAction?: IAction; + + /** + * Global tracking the current drop operation. + * + * TODO: figure out how to make this work with multiple windows + */ + private static _currentDropOperation?: CancelablePromise; private readonly _dropProgressManager: InlineProgressManager; private readonly _postDropWidgetManager: PostEditWidgetManager; @@ -60,7 +75,9 @@ export class DropIntoEditorController extends Disposable implements IEditorContr super(); this._dropProgressManager = this._register(instantiationService.createInstance(InlineProgressManager, 'dropIntoEditor', editor)); - this._postDropWidgetManager = this._register(instantiationService.createInstance(PostEditWidgetManager, 'dropIntoEditor', editor, dropWidgetVisibleCtx, { id: changeDropTypeCommandId, label: localize('postDropWidgetTitle', "Show drop options...") })); + this._postDropWidgetManager = this._register(instantiationService.createInstance(PostEditWidgetManager, 'dropIntoEditor', editor, dropWidgetVisibleCtx, + { id: changeDropTypeCommandId, label: localize('postDropWidgetTitle', "Show drop options...") }, + () => DropIntoEditorController._configureDefaultAction ? [DropIntoEditorController._configureDefaultAction] : [])); this._register(editor.onDropIntoEditor(e => this.onDropIntoEditor(editor, e.position, e.event))); } @@ -78,7 +95,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr return; } - this._currentOperation?.cancel(); + DropIntoEditorController._currentDropOperation?.cancel(); editor.focus(); editor.setPosition(position); @@ -108,7 +125,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr return provider.dropMimeTypes.some(mime => ourDataTransfer.matches(mime)); }); - const editSession = disposables.add(await this.getDropEdits(providers, model, position, ourDataTransfer, tokenSource)); + const editSession = disposables.add(await this.getDropEdits(providers, model, position, ourDataTransfer, tokenSource.token)); if (tokenSource.token.isCancellationRequested) { return; } @@ -121,31 +138,34 @@ export class DropIntoEditorController extends Disposable implements IEditorContr } } finally { disposables.dispose(); - if (this._currentOperation === p) { - this._currentOperation = undefined; + if (DropIntoEditorController._currentDropOperation === p) { + DropIntoEditorController._currentDropOperation = undefined; } } }); this._dropProgressManager.showWhile(position, localize('dropIntoEditorProgress', "Running drop handlers. Click to cancel"), p, { cancel: () => p.cancel() }); - this._currentOperation = p; + DropIntoEditorController._currentDropOperation = p; } - private async getDropEdits(providers: readonly DocumentDropEditProvider[], model: ITextModel, position: IPosition, dataTransfer: VSDataTransfer, tokenSource: EditorStateCancellationTokenSource) { + private async getDropEdits(providers: readonly DocumentDropEditProvider[], model: ITextModel, position: IPosition, dataTransfer: VSDataTransfer, token: CancellationToken) { const disposables = new DisposableStore(); const results = await raceCancellation(Promise.all(providers.map(async provider => { try { - const edits = await provider.provideDocumentDropEdits(model, position, dataTransfer, tokenSource.token); + const edits = await provider.provideDocumentDropEdits(model, position, dataTransfer, token); if (edits) { disposables.add(edits); } return edits?.edits.map(edit => ({ ...edit, providerId: provider.id })); } catch (err) { + if (!isCancellationError(err)) { + console.error(err); + } console.error(err); } return undefined; - })), tokenSource.token); + })), token); const edits = coalesce(results ?? []).flat(); return { @@ -155,12 +175,12 @@ export class DropIntoEditorController extends Disposable implements IEditorContr } private getInitialActiveEditIndex(model: ITextModel, edits: ReadonlyArray) { - const preferredProviders = this._configService.getValue>(defaultProviderConfig, { resource: model.uri }); - for (const [configMime, desiredKindStr] of Object.entries(preferredProviders)) { - const desiredKind = new HierarchicalKind(desiredKindStr); + const preferredProviders = this._configService.getValue(dropAsPreferenceConfig, { resource: model.uri }); + for (const config of Array.isArray(preferredProviders) ? preferredProviders : []) { + const desiredKind = new HierarchicalKind(config.kind); const editIndex = edits.findIndex(edit => - desiredKind.value === edit.providerId - && edit.handledMimeType && matchesMimeType(configMime, [edit.handledMimeType])); + (edit.kind && desiredKind.contains(edit.kind)) + && (!config.mimeType || (edit.handledMimeType && matchesMimeType(config.mimeType, [edit.handledMimeType])))); if (editIndex >= 0) { return editIndex; } diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts index 6be92c50daa25..868cd81a0db85 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts @@ -5,25 +5,26 @@ import * as dom from '../../../../base/browser/dom.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; -import { toAction } from '../../../../base/common/actions.js'; +import { IAction } from '../../../../base/common/actions.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { Event } from '../../../../base/common/event.js'; import { Disposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import './postEditWidget.css'; +import { localize } from '../../../../nls.js'; +import { ActionListItemKind, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js'; +import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js'; +import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../browser/editorBrowser.js'; import { IBulkEditResult, IBulkEditService } from '../../../browser/services/bulkEditService.js'; import { Range } from '../../../common/core/range.js'; import { DocumentDropEdit, DocumentPasteEdit } from '../../../common/languages.js'; import { TrackedRangeStickiness } from '../../../common/model.js'; import { createCombinedWorkspaceEdit } from './edit.js'; -import { localize } from '../../../../nls.js'; -import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import './postEditWidget.css'; interface EditSet { @@ -55,9 +56,10 @@ class PostEditWidget extends Dis private readonly range: Range, private readonly edits: EditSet, private readonly onSelectNewEdit: (editIndex: number) => void, - @IContextMenuService private readonly _contextMenuService: IContextMenuService, + private readonly additionalActions: readonly IAction[], @IContextKeyService contextKeyService: IContextKeyService, @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IActionWidgetService private readonly _actionWidgetService: IActionWidgetService, ) { super(); @@ -115,24 +117,31 @@ class PostEditWidget extends Dis } showSelector() { - this._contextMenuService.showContextMenu({ - getAnchor: () => { - const pos = dom.getDomNodePagePosition(this.button.element); - return { x: pos.left + pos.width, y: pos.top + pos.height }; - }, - getActions: () => { - return this.edits.allEdits.map((edit, i) => toAction({ - id: '', + const pos = dom.getDomNodePagePosition(this.button.element); + const anchor = { x: pos.left + pos.width, y: pos.top + pos.height }; + + this._actionWidgetService.show('postEditWidget', false, [ + ...this.edits.allEdits.map((edit, i): IActionListItem => { + return { + item: edit, + kind: ActionListItemKind.Action, label: edit.title, - checked: i === this.edits.activeEditIndex, - run: () => { - if (i !== this.edits.activeEditIndex) { - return this.onSelectNewEdit(i); - } - }, - })); - } - }); + disabled: false, + canPreview: false, + hideIcon: true + }; + }) + ], { + onHide: () => { }, + onSelect: (item) => { + this._actionWidgetService.hide(false); + + const i = this.edits.allEdits.findIndex(edit => edit === item); + if (i !== this.edits.activeEditIndex) { + return this.onSelectNewEdit(i); + } + }, + }, anchor, this.editor.getDomNode() ?? undefined, this.additionalActions); } } @@ -145,6 +154,7 @@ export class PostEditWidgetManager, private readonly _showCommand: ShowCommand, + private readonly _getAdditionalActions: () => readonly IAction[], @IInstantiationService private readonly _instantiationService: IInstantiationService, @IBulkEditService private readonly _bulkEditService: IBulkEditService, @INotificationService private readonly _notificationService: INotificationService, @@ -234,7 +244,7 @@ export class PostEditWidgetManager, this._id, this._editor, this._visibleContext, this._showCommand, range, edits, onDidSelectEdit); + this._currentWidget.value = this._instantiationService.createInstance(PostEditWidget, this._id, this._editor, this._visibleContext, this._showCommand, range, edits, onDidSelectEdit, this._getAdditionalActions()); } } diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 9a48bf706a31e..bebab4ec9a4b5 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -37,6 +37,7 @@ export interface IActionListItem { readonly label?: string; readonly keybinding?: ResolvedKeybinding; canPreview?: boolean | undefined; + readonly hideIcon?: boolean; } interface IActionMenuTemplateData { @@ -118,6 +119,8 @@ class ActionItemRenderer implements IListRenderer, IAction return; } + dom.setVisibility(!element.hideIcon, data.icon); + data.text.textContent = stripNewlines(element.label); data.keybinding.set(element.keybinding); @@ -139,8 +142,8 @@ class ActionItemRenderer implements IListRenderer, IAction } } - disposeTemplate(_templateData: IActionMenuTemplateData): void { - _templateData.keybinding.dispose(); + disposeTemplate(templateData: IActionMenuTemplateData): void { + templateData.keybinding.dispose(); } } diff --git a/src/vs/workbench/contrib/dropOrPasteInto/browser/dropOrPasteInto.contribution.ts b/src/vs/workbench/contrib/dropOrPasteInto/browser/dropOrPasteInto.contribution.ts new file mode 100644 index 0000000000000..f7104095ca88f --- /dev/null +++ b/src/vs/workbench/contrib/dropOrPasteInto/browser/dropOrPasteInto.contribution.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { toAction } from '../../../../base/common/actions.js'; +import { CopyPasteController, pasteAsPreferenceConfig } from '../../../../editor/contrib/dropOrPasteInto/browser/copyPasteController.js'; +import { dropAsPreferenceConfig, DropIntoEditorController } from '../../../../editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.js'; +import { localize } from '../../../../nls.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; + +class DropOrPasteInto implements IWorkbenchContribution { + public static ID = 'workbench.contrib.dropOrPasteInto'; + + constructor( + @IPreferencesService private readonly _preferencesService: IPreferencesService + ) { + CopyPasteController.setConfigureDefaultAction(toAction({ + id: 'workbench.action.configurePreferredPasteAction', + label: localize('configureDefaultPaste.label', 'Configure preferred paste action...'), + run: () => this.configurePreferredPasteAction() + })); + + DropIntoEditorController.setConfigureDefaultAction(toAction({ + id: 'workbench.action.configurePreferredDropAction', + label: localize('configureDefaultDrop.label', 'Configure preferred drop action...'), + run: () => this.configurePreferredDropAction() + })); + } + + private configurePreferredPasteAction() { + return this._preferencesService.openUserSettings({ + jsonEditor: true, + revealSetting: { key: pasteAsPreferenceConfig, edit: true } + }); + } + + private configurePreferredDropAction() { + return this._preferencesService.openUserSettings({ + jsonEditor: true, + revealSetting: { key: dropAsPreferenceConfig, edit: true } + }); + } +} + +registerWorkbenchContribution2(DropOrPasteInto.ID, DropOrPasteInto, WorkbenchPhase.Eventually); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index c50af76d0bbaa..c33b094cae184 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -398,4 +398,8 @@ import './contrib/scrollLocking/browser/scrollLocking.contribution.js'; // Inline Completions import './contrib/inlineCompletions/browser/inlineCompletions.contribution.js'; +// Drop or paste into +import './contrib/dropOrPasteInto/browser/dropOrPasteInto.contribution.js'; + + //#endregion