Skip to content

Adding preparePasteEdits method to check if smart copy/paste should be applied #60053

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 26, 2024
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
10 changes: 10 additions & 0 deletions src/harness/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,16 @@ export class SessionClient implements LanguageService {
return getSupportedCodeFixes();
}

preparePasteEditsForFile(copiedFromFile: string, copiedTextSpan: TextRange[]): boolean {
const args: protocol.PreparePasteEditsRequestArgs = {
file: copiedFromFile,
copiedTextSpan: copiedTextSpan.map(span => ({ start: this.positionToOneBasedLineOffset(copiedFromFile, span.pos), end: this.positionToOneBasedLineOffset(copiedFromFile, span.end) })),
};
const request = this.processRequest<protocol.PreparePasteEditsRequest>(protocol.CommandTypes.PreparePasteEdits, args);
const response = this.processResponse<protocol.PreparePasteEditsResponse>(request);
return response.body;
}

getPasteEdits(
{ targetFile, pastedText, pasteLocations, copiedFrom }: PasteEditsArgs,
formatOptions: FormatCodeSettings,
Expand Down
7 changes: 7 additions & 0 deletions src/harness/fourslashImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3630,6 +3630,13 @@ export class TestState {
assert.deepEqual(actualModuleSpecifiers, moduleSpecifiers);
}

public verifyPreparePasteEdits(options: FourSlashInterface.PreparePasteEditsOptions): void {
const providePasteEdits = this.languageService.preparePasteEditsForFile(options.copiedFromFile, options.copiedTextRange);
if (providePasteEdits !== options.providePasteEdits) {
this.raiseError(`preparePasteEdits failed - Expected prepare paste edits to return ${options.providePasteEdits}, but got ${providePasteEdits}.`);
}
}

public verifyPasteEdits(options: FourSlashInterface.PasteEditsOptions): void {
const editInfo = this.languageService.getPasteEdits({ targetFile: this.activeFile.fileName, pastedText: options.args.pastedText, pasteLocations: options.args.pasteLocations, copiedFrom: options.args.copiedFrom, preferences: options.args.preferences }, this.formatCodeSettings);
this.verifyNewContent({ newFileContent: options.newFileContents }, editInfo.edits);
Expand Down
8 changes: 8 additions & 0 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,9 @@ export class Verify extends VerifyNegatable {
this.state.verifyOrganizeImports(newContent, mode, preferences);
}

public preparePasteEdits(options: PreparePasteEditsOptions): void {
this.state.verifyPreparePasteEdits(options);
}
public pasteEdits(options: PasteEditsOptions): void {
this.state.verifyPasteEdits(options);
}
Expand Down Expand Up @@ -2017,6 +2020,11 @@ export interface MoveToFileOptions {
readonly preferences?: ts.UserPreferences;
}

export interface PreparePasteEditsOptions {
readonly providePasteEdits: boolean;
readonly copiedTextRange: ts.TextRange[];
readonly copiedFromFile: string;
}
export interface PasteEditsOptions {
readonly newFileContents: { readonly [fileName: string]: string; };
args: ts.PasteEditsArgs;
Expand Down
15 changes: 15 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export const enum CommandTypes {
GetApplicableRefactors = "getApplicableRefactors",
GetEditsForRefactor = "getEditsForRefactor",
GetMoveToRefactoringFileSuggestions = "getMoveToRefactoringFileSuggestions",
PreparePasteEdits = "preparePasteEdits",
GetPasteEdits = "getPasteEdits",
/** @internal */
GetEditsForRefactorFull = "getEditsForRefactor-full",
Expand Down Expand Up @@ -671,6 +672,20 @@ export interface GetMoveToRefactoringFileSuggestions extends Response {
};
}

/**
* Request to check if `pasteEdits` should be provided for a given location post copying text from that location.
*/
export interface PreparePasteEditsRequest extends FileRequest {
command: CommandTypes.PreparePasteEdits;
arguments: PreparePasteEditsRequestArgs;
}
export interface PreparePasteEditsRequestArgs extends FileRequestArgs {
copiedTextSpan: TextSpan[];
}
export interface PreparePasteEditsResponse extends Response {
body: boolean;
}

/**
* Request refactorings at a given position post pasting text from some other location.
*/
Expand Down
8 changes: 8 additions & 0 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,7 @@ const invalidSyntacticModeCommands: readonly protocol.CommandTypes[] = [
protocol.CommandTypes.NavtoFull,
protocol.CommandTypes.DocumentHighlights,
protocol.CommandTypes.DocumentHighlightsFull,
protocol.CommandTypes.PreparePasteEdits,
];

export interface SessionOptions {
Expand Down Expand Up @@ -2966,6 +2967,10 @@ export class Session<TMessage = string> implements EventSender {
return project.getLanguageService().getMoveToRefactoringFileSuggestions(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file));
}

private preparePasteEdits(args: protocol.PreparePasteEditsRequestArgs): boolean {
const { file, project } = this.getFileAndProject(args);
return project.getLanguageService().preparePasteEditsForFile(file, args.copiedTextSpan.map(copies => this.getRange({ file, startLine: copies.start.line, startOffset: copies.start.offset, endLine: copies.end.line, endOffset: copies.end.offset }, this.projectService.getScriptInfoForNormalizedPath(file)!)));
}
private getPasteEdits(args: protocol.GetPasteEditsRequestArgs): protocol.PasteEditsAction | undefined {
const { file, project } = this.getFileAndProject(args);
const copiedFrom = args.copiedFrom
Expand Down Expand Up @@ -3716,6 +3721,9 @@ export class Session<TMessage = string> implements EventSender {
[protocol.CommandTypes.GetMoveToRefactoringFileSuggestions]: (request: protocol.GetMoveToRefactoringFileSuggestionsRequest) => {
return this.requiredResponse(this.getMoveToRefactoringFileSuggestions(request.arguments));
},
[protocol.CommandTypes.PreparePasteEdits]: (request: protocol.PreparePasteEditsRequest) => {
return this.requiredResponse(this.preparePasteEdits(request.arguments));
},
[protocol.CommandTypes.GetPasteEdits]: (request: protocol.GetPasteEditsRequest) => {
return this.requiredResponse(this.getPasteEdits(request.arguments));
},
Expand Down
1 change: 1 addition & 0 deletions src/services/_namespaces/ts.preparePasteEdits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "../preparePasteEdits.js";
2 changes: 2 additions & 0 deletions src/services/_namespaces/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,7 @@ import * as textChanges from "./ts.textChanges.js";
export { textChanges };
import * as formatting from "./ts.formatting.js";
export { formatting };
import * as PreparePasteEdits from "./ts.preparePasteEdits.js";
export { PreparePasteEdits };
import * as pasteEdits from "./ts.PasteEdits.js";
export { pasteEdits };
46 changes: 46 additions & 0 deletions src/services/preparePasteEdits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
findAncestor,
forEachChild,
getTokenAtPosition,
isIdentifier,
rangeContainsPosition,
rangeContainsRange,
SourceFile,
SymbolFlags,
TextRange,
TypeChecker,
} from "./_namespaces/ts.js";
import { isInImport } from "./refactors/moveToFile.js";

/** @internal */
export function preparePasteEdits(
sourceFile: SourceFile,
copiedFromRange: TextRange[],
checker: TypeChecker,
): boolean {
let shouldProvidePasteEdits = false;
copiedFromRange.forEach(range => {
const enclosingNode = findAncestor(
getTokenAtPosition(sourceFile, range.pos),
ancestorNode => rangeContainsRange(ancestorNode, range),
);
if (!enclosingNode) return;
forEachChild(enclosingNode, function checkNameResolution(node) {
if (shouldProvidePasteEdits) return;
if (isIdentifier(node) && rangeContainsPosition(range, node.getStart(sourceFile))) {
const resolvedSymbol = checker.resolveName(node.text, node, SymbolFlags.All, /*excludeGlobals*/ false);
if (resolvedSymbol && resolvedSymbol.declarations) {
for (const decl of resolvedSymbol.declarations) {
if (isInImport(decl) || !!(node.text && sourceFile.symbol && sourceFile.symbol.exports?.has(node.escapedText))) {
shouldProvidePasteEdits = true;
return;
}
}
}
}
node.forEachChild(checkNameResolution);
});
if (shouldProvidePasteEdits) return;
});
return shouldProvidePasteEdits;
}
4 changes: 2 additions & 2 deletions src/services/refactors/moveToFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1000,8 +1000,8 @@ function forEachTopLevelDeclaration<T>(statement: Statement, cb: (node: TopLevel
}
}
}

function isInImport(decl: Declaration) {
/** @internal */
export function isInImport(decl: Declaration): boolean {
switch (decl.kind) {
case SyntaxKind.ImportEqualsDeclaration:
case SyntaxKind.ImportSpecifier:
Expand Down
12 changes: 12 additions & 0 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ import {
positionIsSynthesized,
PossibleProgramFileInfo,
PragmaMap,
PreparePasteEdits,
PrivateIdentifier,
Program,
PropertyName,
Expand Down Expand Up @@ -1613,6 +1614,7 @@ const invalidOperationsInSyntacticMode: readonly (keyof LanguageService)[] = [
"getRenameInfo",
"findRenameLocations",
"getApplicableRefactors",
"preparePasteEditsForFile",
];
export function createLanguageService(
host: LanguageServiceHost,
Expand Down Expand Up @@ -2300,6 +2302,15 @@ export function createLanguageService(
};
}

function preparePasteEditsForFile(fileName: string, copiedTextRange: TextRange[]): boolean {
synchronizeHostData();
return PreparePasteEdits.preparePasteEdits(
getValidSourceFile(fileName),
copiedTextRange,
program.getTypeChecker(),
);
}

function getPasteEdits(
args: PasteEditsArgs,
formatOptions: FormatCodeSettings,
Expand Down Expand Up @@ -3416,6 +3427,7 @@ export function createLanguageService(
uncommentSelection,
provideInlayHints,
getSupportedCodeFixes,
preparePasteEditsForFile,
getPasteEdits,
mapCode,
};
Expand Down
1 change: 1 addition & 0 deletions src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,7 @@ export interface LanguageService {
/** @internal */ mapCode(fileName: string, contents: string[], focusLocations: TextSpan[][] | undefined, formatOptions: FormatCodeSettings, preferences: UserPreferences): readonly FileTextChanges[];

dispose(): void;
preparePasteEditsForFile(fileName: string, copiedTextRanges: TextRange[]): boolean;
getPasteEdits(
args: PasteEditsArgs,
formatOptions: FormatCodeSettings,
Expand Down
16 changes: 16 additions & 0 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ declare namespace ts {
GetApplicableRefactors = "getApplicableRefactors",
GetEditsForRefactor = "getEditsForRefactor",
GetMoveToRefactoringFileSuggestions = "getMoveToRefactoringFileSuggestions",
PreparePasteEdits = "preparePasteEdits",
GetPasteEdits = "getPasteEdits",
OrganizeImports = "organizeImports",
GetEditsForFileRename = "getEditsForFileRename",
Expand Down Expand Up @@ -514,6 +515,19 @@ declare namespace ts {
files: string[];
};
}
/**
* Request to check if `pasteEdits` should be provided for a given location post copying text from that location.
*/
export interface PreparePasteEditsRequest extends FileRequest {
command: CommandTypes.PreparePasteEdits;
arguments: PreparePasteEditsRequestArgs;
}
export interface PreparePasteEditsRequestArgs extends FileRequestArgs {
copiedTextSpan: TextSpan[];
}
export interface PreparePasteEditsResponse extends Response {
body: boolean;
}
/**
* Request refactorings at a given position post pasting text from some other location.
*/
Expand Down Expand Up @@ -3556,6 +3570,7 @@ declare namespace ts {
private getApplicableRefactors;
private getEditsForRefactor;
private getMoveToRefactoringFileSuggestions;
private preparePasteEdits;
private getPasteEdits;
private organizeImports;
private getEditsForFileRename;
Expand Down Expand Up @@ -10211,6 +10226,7 @@ declare namespace ts {
uncommentSelection(fileName: string, textRange: TextRange): TextChange[];
getSupportedCodeFixes(fileName?: string): readonly string[];
dispose(): void;
preparePasteEditsForFile(fileName: string, copiedTextRanges: TextRange[]): boolean;
getPasteEdits(args: PasteEditsArgs, formatOptions: FormatCodeSettings): PasteEdits;
}
interface JsxClosingTagInfo {
Expand Down
5 changes: 5 additions & 0 deletions tests/cases/fourslash/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ declare namespace FourSlashInterface {
toggleMultilineComment(newFileContent: string): void;
commentSelection(newFileContent: string): void;
uncommentSelection(newFileContent: string): void;
preparePasteEdits(options: {
copiedFromFile: string,
copiedTextRange: { pos: number, end: number }[],
providePasteEdits: boolean
}): void;
pasteEdits(options: {
newFileContents: { readonly [fileName: string]: string };
args: {
Expand Down
22 changes: 22 additions & 0 deletions tests/cases/fourslash/preparePasteEdits_multipleLocations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// <reference path='fourslash.ts' />

// @module: commonjs
// @allowJs: true

// @Filename: /file1.js
//// import { aa, bb } = require("./other");
//// [|const r = 10;|]
//// export const s = 12;
//// [|export const t = aa + bb + r + s;
//// const u = 1;|]

// @Filename: /other.js
//// export const aa = 1;
//// export const bb = 2;
//// module.exports = { aa, bb };

verify.preparePasteEdits({
copiedFromFile: "/file1.js",
copiedTextRange: test.ranges(),
providePasteEdits: true,
})
20 changes: 20 additions & 0 deletions tests/cases/fourslash/preparePasteEdits_resolvedIdentifiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// <reference path='./fourslash.ts' />

// @Filename: /file2.ts
////import { b } from './file1';
////export const a = 1;
//// [|function MyFunction() {}
//// namespace MyFunction {
//// export const value = b;
//// }|]
////const c = a + 20;
////const t = 9;

// @Filename: /file1.ts
////export const b = 2;

verify.preparePasteEdits({
copiedFromFile: "/file2.ts",
copiedTextRange: test.ranges(),
providePasteEdits: true,
})
19 changes: 19 additions & 0 deletions tests/cases/fourslash/preparePasteEdits_resolvedTypeParameters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// <reference path='./fourslash.ts' />

// @Filename: /file2.ts
//// import { T } from './file1';
////
//// [|function MyFunction(param: T): T {
//// type U = { value: T }
//// const localVariable: U = { value: param };
//// return localVariable.value;
//// }|]

// @Filename: /file1.ts
//// export type T = string;

verify.preparePasteEdits({
copiedFromFile: "/file2.ts",
copiedTextRange: test.ranges(),
providePasteEdits: true
})
13 changes: 13 additions & 0 deletions tests/cases/fourslash/preparePasteEdits_returnFalse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference path='./fourslash.ts' />

// @Filename: /file1.ts
//// [|const a = 1;|]
//// [|function foo() {
//// console.log("testing");}|]
//// [|//This is a comment|]

verify.preparePasteEdits({
copiedFromFile: "/file1.ts",
copiedTextRange: test.ranges(),
providePasteEdits: false,
})