Hook up prototype paste with imports for JS/TS (#204665)

* Hook up prototype paste with imports for JS/TS

For https://github.com/microsoft/TypeScript/pull/57262 but with proposed changes to ts protocol

* Support new api

* Update
This commit is contained in:
Matt Bierner 2024-05-24 12:57:30 -07:00 committed by GitHub
parent fb7f5a9a83
commit 934af755c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 192 additions and 2 deletions

View file

@ -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"]
}
}
},

View file

@ -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",

View file

@ -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<vscode.DocumentPasteEdit[] | undefined> {
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<CopyMetadata | undefined> {
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'],
});
});
}

View file

@ -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))),

View file

@ -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);

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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",
]
}