diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index e01c9c96527d5..25cfda4b0b96e 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -13,7 +13,8 @@ "multiDocumentHighlightProvider", "mappedEditsProvider", "codeActionAI", - "codeActionRanges" + "codeActionRanges", + "documentPaste" ], "capabilities": { "virtualWorkspaces": { @@ -1316,6 +1317,20 @@ "default": true, "markdownDescription": "%typescript.workspaceSymbols.excludeLibrarySymbols%", "scope": "window" + }, + "javascript.experimental.updateImportsOnPaste": { + "scope": "resource", + "type": "boolean", + "default": false, + "description": "%configuration.updateImportsOnPaste%", + "tags": ["experimental"] + }, + "typescript.experimental.updateImportsOnPaste": { + "scope": "resource", + "type": "boolean", + "default": false, + "description": "%configuration.updateImportsOnPaste%", + "tags": ["experimental"] } } }, diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 7fb5bae6ad12a..4f276905d967a 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -219,6 +219,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.5+.", "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.", "walkthroughs.nodejsWelcome.downloadNode.forMacOrWindows.title": "Install Node.js", diff --git a/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts new file mode 100644 index 0000000000000..643c77ac35767 --- /dev/null +++ b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { DocumentSelector } from '../configuration/documentSelector'; +import * as typeConverters from '../typeConverters'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; +import { conditionalRegistration, requireMinVersion, requireSomeCapability } from './util/dependentRegistration'; +import protocol from '../tsServer/protocol/protocol'; +import { API } from '../tsServer/api'; +import { LanguageDescription } from '../configuration/languageDescription'; + +class CopyMetadata { + constructor( + readonly resource: vscode.Uri, + readonly ranges: readonly vscode.Range[], + ) { } + + toJSON() { + return JSON.stringify({ + resource: this.resource.toJSON(), + ranges: this.ranges, + }); + } + + static fromJSON(str: string): CopyMetadata | undefined { + try { + const parsed = JSON.parse(str); + return new CopyMetadata( + vscode.Uri.from(parsed.resource), + parsed.ranges.map((r: any) => new vscode.Range(r[0].line, r[0].character, r[1].line, r[1].character))); + } catch { + // ignore + } + return undefined; + } +} + +class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { + + static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('text', 'jsts', 'pasteWithImports'); + static readonly metadataMimeType = 'application/vnd.code.jsts.metadata'; + + constructor( + private readonly _modeId: string, + private readonly _client: ITypeScriptServiceClient, + ) { } + + prepareDocumentPaste(document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken) { + dataTransfer.set(DocumentPasteProvider.metadataMimeType, + new vscode.DataTransferItem(new CopyMetadata(document.uri, ranges).toJSON())); + } + + async provideDocumentPasteEdits( + document: vscode.TextDocument, + ranges: readonly vscode.Range[], + dataTransfer: vscode.DataTransfer, + _context: vscode.DocumentPasteEditContext, + token: vscode.CancellationToken, + ): Promise { + const config = vscode.workspace.getConfiguration(this._modeId, document.uri); + if (!config.get('experimental.updateImportsOnPaste')) { + return; + } + + const file = this._client.toOpenTsFilePath(document); + if (!file) { + return; + } + + const text = await dataTransfer.get('text/plain')?.asString(); + if (!text || token.isCancellationRequested) { + return; + } + + // Get optional metadata + const metadata = await this.extractMetadata(dataTransfer, token); + if (token.isCancellationRequested) { + return; + } + + let copiedFrom: { + file: string; + spans: protocol.TextSpan[]; + } | undefined; + if (metadata) { + const spans = metadata.ranges.map(typeConverters.Range.toTextSpan); + const copyFile = this._client.toTsFilePath(metadata.resource); + if (copyFile) { + copiedFrom = { file: copyFile, spans }; + } + } + + const response = await this._client.execute('getPasteEdits', { + file, + // TODO: only supports a single paste for now + pastedText: [text], + pasteLocations: ranges.map(typeConverters.Range.toTextSpan), + copiedFrom + }, token); + if (response.type !== 'response' || !response.body || token.isCancellationRequested) { + return; + } + + const edit = new vscode.DocumentPasteEdit('', vscode.l10n.t("Paste with imports"), DocumentPasteProvider.kind); + 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)); + } + edit.additionalEdit = additionalEdit; + return [edit]; + } + + private async extractMetadata(dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { + const metadata = await dataTransfer.get(DocumentPasteProvider.metadataMimeType)?.asString(); + if (token.isCancellationRequested) { + return undefined; + } + + return metadata ? CopyMetadata.fromJSON(metadata) : undefined; + } +} + +export function register(selector: DocumentSelector, language: LanguageDescription, client: ITypeScriptServiceClient) { + return conditionalRegistration([ + requireSomeCapability(client, ClientCapability.Semantic), + requireMinVersion(client, API.v550), + ], () => { + return vscode.languages.registerDocumentPasteEditProvider(selector.semantic, new DocumentPasteProvider(language.id, client), { + providedPasteEditKinds: [DocumentPasteProvider.kind], + copyMimeTypes: [DocumentPasteProvider.metadataMimeType], + pasteMimeTypes: ['text/plain'], + }); + }); +} diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index b09df40561b36..a192740986918 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -65,6 +65,7 @@ export default class LanguageProvider extends Disposable { import('./languageFeatures/codeLens/implementationsCodeLens').then(provider => this._register(provider.register(selector, this.description, this.client, cachedNavTreeResponse))), import('./languageFeatures/codeLens/referencesCodeLens').then(provider => this._register(provider.register(selector, this.description, this.client, cachedNavTreeResponse))), import('./languageFeatures/completions').then(provider => this._register(provider.register(selector, this.description, this.client, this.typingsStatus, this.fileConfigurationManager, this.commandManager, this.telemetryReporter, this.onCompletionAccepted))), + import('./languageFeatures/copyPaste').then(provider => this._register(provider.register(selector, this.description, this.client))), import('./languageFeatures/definitions').then(provider => this._register(provider.register(selector, this.client))), import('./languageFeatures/directiveCommentCompletions').then(provider => this._register(provider.register(selector, this.client))), import('./languageFeatures/documentHighlight').then(provider => this._register(provider.register(selector, this.client))), diff --git a/extensions/typescript-language-features/src/tsServer/api.ts b/extensions/typescript-language-features/src/tsServer/api.ts index 2378bfb53f0f2..4f26db47513a4 100644 --- a/extensions/typescript-language-features/src/tsServer/api.ts +++ b/extensions/typescript-language-features/src/tsServer/api.ts @@ -37,6 +37,7 @@ export class API { public static readonly v520 = API.fromSimpleString('5.2.0'); public static readonly v544 = API.fromSimpleString('5.4.4'); public static readonly v540 = API.fromSimpleString('5.4.0'); + public static readonly v550 = API.fromSimpleString('5.5.0'); public static fromVersionString(versionString: string): API { let version = semver.valid(versionString); diff --git a/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts b/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts index aa9b0589d2da6..900d66f37abba 100644 --- a/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts +++ b/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts @@ -20,6 +20,7 @@ declare module '../../../../node_modules/typescript/lib/typescript' { readonly _serverType?: ServerType; } + //#region MapCode export interface MapCodeRequestArgs extends FileRequestArgs { /** * The files and changes to try and apply/map. @@ -49,7 +50,39 @@ declare module '../../../../node_modules/typescript/lib/typescript' { } export interface MapCodeResponse extends Response { - body: readonly FileCodeEdits[]; + body: FileCodeEdits[] } + //#endregion + + //#region Paste + export interface GetPasteEditsRequest extends Request { + command: 'getPasteEdits'; + arguments: GetPasteEditsRequestArgs; + } + + export interface GetPasteEditsRequestArgs extends FileRequestArgs { + /** The text that gets pasted in a file. */ + pastedText: string[]; + /** Locations of where the `pastedText` gets added in a file. If the length of the `pastedText` and `pastedLocations` are not the same, + * then the `pastedText` is combined into one and added at all the `pastedLocations`. + */ + pasteLocations: TextSpan[]; + /** The source location of each `pastedText`. If present, the length of `spans` must be equal to the length of `pastedText`. */ + copiedFrom?: { + file: string; + spans: TextSpan[]; + }; + } + + export interface GetPasteEditsResponse extends Response { + body: PasteEditsAction; + } + export interface PasteEditsAction { + edits: FileCodeEdits[]; + fixId?: {}; + } + //#endregion } } + + diff --git a/extensions/typescript-language-features/src/typescriptService.ts b/extensions/typescript-language-features/src/typescriptService.ts index 147324911187b..931b287df03e1 100644 --- a/extensions/typescript-language-features/src/typescriptService.ts +++ b/extensions/typescript-language-features/src/typescriptService.ts @@ -77,6 +77,7 @@ interface StandardTsServerRequests { 'getMoveToRefactoringFileSuggestions': [Proto.GetMoveToRefactoringFileSuggestionsRequestArgs, Proto.GetMoveToRefactoringFileSuggestions]; 'linkedEditingRange': [Proto.FileLocationRequestArgs, Proto.LinkedEditingRangeResponse]; 'mapCode': [Proto.MapCodeRequestArgs, Proto.MapCodeResponse]; + 'getPasteEdits': [Proto.GetPasteEditsRequestArgs, Proto.GetPasteEditsResponse]; } interface NoResponseTsServerRequests { diff --git a/extensions/typescript-language-features/tsconfig.json b/extensions/typescript-language-features/tsconfig.json index 1da85cd17cd4b..65557839ba60d 100644 --- a/extensions/typescript-language-features/tsconfig.json +++ b/extensions/typescript-language-features/tsconfig.json @@ -17,5 +17,6 @@ "../../src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts", "../../src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts", "../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts", + "../../src/vscode-dts/vscode.proposed.documentPaste.d.ts", ] }