Skip to content

Commit

Permalink
Add paste as default settings and enable js/ts paste with imports by …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
mjbvz authored Nov 5, 2024
1 parent 49907c0 commit 54d81cd
Show file tree
Hide file tree
Showing 11 changed files with 277 additions and 96 deletions.
18 changes: 6 additions & 12 deletions extensions/typescript-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion extensions/typescript-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class CopyMetadata {
}
}

const settingId = 'experimental.updateImportsOnPaste';
const enabledSettingId = 'updateImportsOnPaste.enabled';

class DocumentPasteProvider implements vscode.DocumentPasteEditProvider {

Expand Down Expand Up @@ -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));
Expand All @@ -146,15 +148,15 @@ 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);
}
}

export function register(selector: DocumentSelector, language: LanguageDescription, client: ITypeScriptServiceClient) {
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],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<IConfigurationRegistry>(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' } }
]
}
},
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<boolean>('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;
Expand Down Expand Up @@ -73,6 +79,12 @@ export class CopyPasteController extends Disposable implements IEditorContributi
return editor.getContribution<CopyPasteController>(CopyPasteController.ID);
}

public static setConfigureDefaultAction(action: IAction) {
CopyPasteController._configureDefaultAction = action;
}

private static _configureDefaultAction?: IAction;

/**
* Global tracking the last copy operation.
*
Expand All @@ -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,
Expand All @@ -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() {
Expand Down Expand Up @@ -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<PasteEditWithProvider>((resolve, reject) => {
(async () => {
try {
Expand Down Expand Up @@ -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<ItemWithEdit>(
[
...editSession.edits.map((edit): ItemWithEdit => ({
label: edit.title,
description: edit.kind?.value,
edit,
})),
...(CopyPasteController._configureDefaultAction ? [
upcast<IQuickPickSeparator>({ type: 'separator' }),
{
label: CopyPasteController._configureDefaultAction.label,
edit: undefined,
}
] : [])
], {
placeHolder: localize('pasteAsPickerPlaceholder', "Select Paste Action"),
});

if (selected === configureDefaultItem) {
CopyPasteController._configureDefaultAction?.run();
return;
}

pickedEdit = selected?.edit;
}

Expand Down Expand Up @@ -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<PreferredPasteConfiguration>(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;
}
}
Loading

0 comments on commit 54d81cd

Please sign in to comment.