Skip to content

Commit

Permalink
Hook up prototype paste with imports for JS/TS (microsoft#204665)
Browse files Browse the repository at this point in the history
* Hook up prototype paste with imports for JS/TS

For microsoft/TypeScript#57262 but with proposed changes to ts protocol

* Support new api

* Update
  • Loading branch information
mjbvz authored and andremmsilva committed May 26, 2024
1 parent 68f3c6a commit ec43930
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 2 deletions.
17 changes: 16 additions & 1 deletion extensions/typescript-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"multiDocumentHighlightProvider",
"mappedEditsProvider",
"codeActionAI",
"codeActionRanges"
"codeActionRanges",
"documentPaste"
],
"capabilities": {
"virtualWorkspaces": {
Expand Down Expand Up @@ -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"]
}
}
},
Expand Down
1 change: 1 addition & 0 deletions extensions/typescript-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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'],
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions extensions/typescript-language-features/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
}

0 comments on commit ec43930

Please sign in to comment.