Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
]
}