Skip to content

Commit 8499803

Browse files
authored
Adding preparePasteEdits method to check if smart copy/paste should be applied (#60053)
1 parent 3ad0f75 commit 8499803

17 files changed

+207
-2
lines changed

src/harness/client.ts

+10
Original file line numberDiff line numberDiff line change
@@ -1020,6 +1020,16 @@ export class SessionClient implements LanguageService {
10201020
return getSupportedCodeFixes();
10211021
}
10221022

1023+
preparePasteEditsForFile(copiedFromFile: string, copiedTextSpan: TextRange[]): boolean {
1024+
const args: protocol.PreparePasteEditsRequestArgs = {
1025+
file: copiedFromFile,
1026+
copiedTextSpan: copiedTextSpan.map(span => ({ start: this.positionToOneBasedLineOffset(copiedFromFile, span.pos), end: this.positionToOneBasedLineOffset(copiedFromFile, span.end) })),
1027+
};
1028+
const request = this.processRequest<protocol.PreparePasteEditsRequest>(protocol.CommandTypes.PreparePasteEdits, args);
1029+
const response = this.processResponse<protocol.PreparePasteEditsResponse>(request);
1030+
return response.body;
1031+
}
1032+
10231033
getPasteEdits(
10241034
{ targetFile, pastedText, pasteLocations, copiedFrom }: PasteEditsArgs,
10251035
formatOptions: FormatCodeSettings,

src/harness/fourslashImpl.ts

+7
Original file line numberDiff line numberDiff line change
@@ -3630,6 +3630,13 @@ export class TestState {
36303630
assert.deepEqual(actualModuleSpecifiers, moduleSpecifiers);
36313631
}
36323632

3633+
public verifyPreparePasteEdits(options: FourSlashInterface.PreparePasteEditsOptions): void {
3634+
const providePasteEdits = this.languageService.preparePasteEditsForFile(options.copiedFromFile, options.copiedTextRange);
3635+
if (providePasteEdits !== options.providePasteEdits) {
3636+
this.raiseError(`preparePasteEdits failed - Expected prepare paste edits to return ${options.providePasteEdits}, but got ${providePasteEdits}.`);
3637+
}
3638+
}
3639+
36333640
public verifyPasteEdits(options: FourSlashInterface.PasteEditsOptions): void {
36343641
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);
36353642
this.verifyNewContent({ newFileContent: options.newFileContents }, editInfo.edits);

src/harness/fourslashInterfaceImpl.ts

+8
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,9 @@ export class Verify extends VerifyNegatable {
657657
this.state.verifyOrganizeImports(newContent, mode, preferences);
658658
}
659659

660+
public preparePasteEdits(options: PreparePasteEditsOptions): void {
661+
this.state.verifyPreparePasteEdits(options);
662+
}
660663
public pasteEdits(options: PasteEditsOptions): void {
661664
this.state.verifyPasteEdits(options);
662665
}
@@ -2017,6 +2020,11 @@ export interface MoveToFileOptions {
20172020
readonly preferences?: ts.UserPreferences;
20182021
}
20192022

2023+
export interface PreparePasteEditsOptions {
2024+
readonly providePasteEdits: boolean;
2025+
readonly copiedTextRange: ts.TextRange[];
2026+
readonly copiedFromFile: string;
2027+
}
20202028
export interface PasteEditsOptions {
20212029
readonly newFileContents: { readonly [fileName: string]: string; };
20222030
args: ts.PasteEditsArgs;

src/server/protocol.ts

+15
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export const enum CommandTypes {
169169
GetApplicableRefactors = "getApplicableRefactors",
170170
GetEditsForRefactor = "getEditsForRefactor",
171171
GetMoveToRefactoringFileSuggestions = "getMoveToRefactoringFileSuggestions",
172+
PreparePasteEdits = "preparePasteEdits",
172173
GetPasteEdits = "getPasteEdits",
173174
/** @internal */
174175
GetEditsForRefactorFull = "getEditsForRefactor-full",
@@ -671,6 +672,20 @@ export interface GetMoveToRefactoringFileSuggestions extends Response {
671672
};
672673
}
673674

675+
/**
676+
* Request to check if `pasteEdits` should be provided for a given location post copying text from that location.
677+
*/
678+
export interface PreparePasteEditsRequest extends FileRequest {
679+
command: CommandTypes.PreparePasteEdits;
680+
arguments: PreparePasteEditsRequestArgs;
681+
}
682+
export interface PreparePasteEditsRequestArgs extends FileRequestArgs {
683+
copiedTextSpan: TextSpan[];
684+
}
685+
export interface PreparePasteEditsResponse extends Response {
686+
body: boolean;
687+
}
688+
674689
/**
675690
* Request refactorings at a given position post pasting text from some other location.
676691
*/

src/server/session.ts

+8
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,7 @@ const invalidSyntacticModeCommands: readonly protocol.CommandTypes[] = [
966966
protocol.CommandTypes.NavtoFull,
967967
protocol.CommandTypes.DocumentHighlights,
968968
protocol.CommandTypes.DocumentHighlightsFull,
969+
protocol.CommandTypes.PreparePasteEdits,
969970
];
970971

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

2970+
private preparePasteEdits(args: protocol.PreparePasteEditsRequestArgs): boolean {
2971+
const { file, project } = this.getFileAndProject(args);
2972+
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)!)));
2973+
}
29692974
private getPasteEdits(args: protocol.GetPasteEditsRequestArgs): protocol.PasteEditsAction | undefined {
29702975
const { file, project } = this.getFileAndProject(args);
29712976
const copiedFrom = args.copiedFrom
@@ -3716,6 +3721,9 @@ export class Session<TMessage = string> implements EventSender {
37163721
[protocol.CommandTypes.GetMoveToRefactoringFileSuggestions]: (request: protocol.GetMoveToRefactoringFileSuggestionsRequest) => {
37173722
return this.requiredResponse(this.getMoveToRefactoringFileSuggestions(request.arguments));
37183723
},
3724+
[protocol.CommandTypes.PreparePasteEdits]: (request: protocol.PreparePasteEditsRequest) => {
3725+
return this.requiredResponse(this.preparePasteEdits(request.arguments));
3726+
},
37193727
[protocol.CommandTypes.GetPasteEdits]: (request: protocol.GetPasteEditsRequest) => {
37203728
return this.requiredResponse(this.getPasteEdits(request.arguments));
37213729
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "../preparePasteEdits.js";

src/services/_namespaces/ts.ts

+2
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,7 @@ import * as textChanges from "./ts.textChanges.js";
5858
export { textChanges };
5959
import * as formatting from "./ts.formatting.js";
6060
export { formatting };
61+
import * as PreparePasteEdits from "./ts.preparePasteEdits.js";
62+
export { PreparePasteEdits };
6163
import * as pasteEdits from "./ts.PasteEdits.js";
6264
export { pasteEdits };

src/services/preparePasteEdits.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {
2+
findAncestor,
3+
forEachChild,
4+
getTokenAtPosition,
5+
isIdentifier,
6+
rangeContainsPosition,
7+
rangeContainsRange,
8+
SourceFile,
9+
SymbolFlags,
10+
TextRange,
11+
TypeChecker,
12+
} from "./_namespaces/ts.js";
13+
import { isInImport } from "./refactors/moveToFile.js";
14+
15+
/** @internal */
16+
export function preparePasteEdits(
17+
sourceFile: SourceFile,
18+
copiedFromRange: TextRange[],
19+
checker: TypeChecker,
20+
): boolean {
21+
let shouldProvidePasteEdits = false;
22+
copiedFromRange.forEach(range => {
23+
const enclosingNode = findAncestor(
24+
getTokenAtPosition(sourceFile, range.pos),
25+
ancestorNode => rangeContainsRange(ancestorNode, range),
26+
);
27+
if (!enclosingNode) return;
28+
forEachChild(enclosingNode, function checkNameResolution(node) {
29+
if (shouldProvidePasteEdits) return;
30+
if (isIdentifier(node) && rangeContainsPosition(range, node.getStart(sourceFile))) {
31+
const resolvedSymbol = checker.resolveName(node.text, node, SymbolFlags.All, /*excludeGlobals*/ false);
32+
if (resolvedSymbol && resolvedSymbol.declarations) {
33+
for (const decl of resolvedSymbol.declarations) {
34+
if (isInImport(decl) || !!(node.text && sourceFile.symbol && sourceFile.symbol.exports?.has(node.escapedText))) {
35+
shouldProvidePasteEdits = true;
36+
return;
37+
}
38+
}
39+
}
40+
}
41+
node.forEachChild(checkNameResolution);
42+
});
43+
if (shouldProvidePasteEdits) return;
44+
});
45+
return shouldProvidePasteEdits;
46+
}

src/services/refactors/moveToFile.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1000,8 +1000,8 @@ function forEachTopLevelDeclaration<T>(statement: Statement, cb: (node: TopLevel
10001000
}
10011001
}
10021002
}
1003-
1004-
function isInImport(decl: Declaration) {
1003+
/** @internal */
1004+
export function isInImport(decl: Declaration): boolean {
10051005
switch (decl.kind) {
10061006
case SyntaxKind.ImportEqualsDeclaration:
10071007
case SyntaxKind.ImportSpecifier:

src/services/services.ts

+12
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ import {
258258
positionIsSynthesized,
259259
PossibleProgramFileInfo,
260260
PragmaMap,
261+
PreparePasteEdits,
261262
PrivateIdentifier,
262263
Program,
263264
PropertyName,
@@ -1621,6 +1622,7 @@ const invalidOperationsInSyntacticMode: readonly (keyof LanguageService)[] = [
16211622
"getRenameInfo",
16221623
"findRenameLocations",
16231624
"getApplicableRefactors",
1625+
"preparePasteEditsForFile",
16241626
];
16251627
export function createLanguageService(
16261628
host: LanguageServiceHost,
@@ -2308,6 +2310,15 @@ export function createLanguageService(
23082310
};
23092311
}
23102312

2313+
function preparePasteEditsForFile(fileName: string, copiedTextRange: TextRange[]): boolean {
2314+
synchronizeHostData();
2315+
return PreparePasteEdits.preparePasteEdits(
2316+
getValidSourceFile(fileName),
2317+
copiedTextRange,
2318+
program.getTypeChecker(),
2319+
);
2320+
}
2321+
23112322
function getPasteEdits(
23122323
args: PasteEditsArgs,
23132324
formatOptions: FormatCodeSettings,
@@ -3424,6 +3435,7 @@ export function createLanguageService(
34243435
uncommentSelection,
34253436
provideInlayHints,
34263437
getSupportedCodeFixes,
3438+
preparePasteEditsForFile,
34273439
getPasteEdits,
34283440
mapCode,
34293441
};

src/services/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@ export interface LanguageService {
699699
/** @internal */ mapCode(fileName: string, contents: string[], focusLocations: TextSpan[][] | undefined, formatOptions: FormatCodeSettings, preferences: UserPreferences): readonly FileTextChanges[];
700700

701701
dispose(): void;
702+
preparePasteEditsForFile(fileName: string, copiedTextRanges: TextRange[]): boolean;
702703
getPasteEdits(
703704
args: PasteEditsArgs,
704705
formatOptions: FormatCodeSettings,

tests/baselines/reference/api/typescript.d.ts

+16
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ declare namespace ts {
107107
GetApplicableRefactors = "getApplicableRefactors",
108108
GetEditsForRefactor = "getEditsForRefactor",
109109
GetMoveToRefactoringFileSuggestions = "getMoveToRefactoringFileSuggestions",
110+
PreparePasteEdits = "preparePasteEdits",
110111
GetPasteEdits = "getPasteEdits",
111112
OrganizeImports = "organizeImports",
112113
GetEditsForFileRename = "getEditsForFileRename",
@@ -514,6 +515,19 @@ declare namespace ts {
514515
files: string[];
515516
};
516517
}
518+
/**
519+
* Request to check if `pasteEdits` should be provided for a given location post copying text from that location.
520+
*/
521+
export interface PreparePasteEditsRequest extends FileRequest {
522+
command: CommandTypes.PreparePasteEdits;
523+
arguments: PreparePasteEditsRequestArgs;
524+
}
525+
export interface PreparePasteEditsRequestArgs extends FileRequestArgs {
526+
copiedTextSpan: TextSpan[];
527+
}
528+
export interface PreparePasteEditsResponse extends Response {
529+
body: boolean;
530+
}
517531
/**
518532
* Request refactorings at a given position post pasting text from some other location.
519533
*/
@@ -3556,6 +3570,7 @@ declare namespace ts {
35563570
private getApplicableRefactors;
35573571
private getEditsForRefactor;
35583572
private getMoveToRefactoringFileSuggestions;
3573+
private preparePasteEdits;
35593574
private getPasteEdits;
35603575
private organizeImports;
35613576
private getEditsForFileRename;
@@ -10211,6 +10226,7 @@ declare namespace ts {
1021110226
uncommentSelection(fileName: string, textRange: TextRange): TextChange[];
1021210227
getSupportedCodeFixes(fileName?: string): readonly string[];
1021310228
dispose(): void;
10229+
preparePasteEditsForFile(fileName: string, copiedTextRanges: TextRange[]): boolean;
1021410230
getPasteEdits(args: PasteEditsArgs, formatOptions: FormatCodeSettings): PasteEdits;
1021510231
}
1021610232
interface JsxClosingTagInfo {

tests/cases/fourslash/fourslash.ts

+5
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,11 @@ declare namespace FourSlashInterface {
458458
toggleMultilineComment(newFileContent: string): void;
459459
commentSelection(newFileContent: string): void;
460460
uncommentSelection(newFileContent: string): void;
461+
preparePasteEdits(options: {
462+
copiedFromFile: string,
463+
copiedTextRange: { pos: number, end: number }[],
464+
providePasteEdits: boolean
465+
}): void;
461466
pasteEdits(options: {
462467
newFileContents: { readonly [fileName: string]: string };
463468
args: {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @module: commonjs
4+
// @allowJs: true
5+
6+
// @Filename: /file1.js
7+
//// import { aa, bb } = require("./other");
8+
//// [|const r = 10;|]
9+
//// export const s = 12;
10+
//// [|export const t = aa + bb + r + s;
11+
//// const u = 1;|]
12+
13+
// @Filename: /other.js
14+
//// export const aa = 1;
15+
//// export const bb = 2;
16+
//// module.exports = { aa, bb };
17+
18+
verify.preparePasteEdits({
19+
copiedFromFile: "/file1.js",
20+
copiedTextRange: test.ranges(),
21+
providePasteEdits: true,
22+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// <reference path='./fourslash.ts' />
2+
3+
// @Filename: /file2.ts
4+
////import { b } from './file1';
5+
////export const a = 1;
6+
//// [|function MyFunction() {}
7+
//// namespace MyFunction {
8+
//// export const value = b;
9+
//// }|]
10+
////const c = a + 20;
11+
////const t = 9;
12+
13+
// @Filename: /file1.ts
14+
////export const b = 2;
15+
16+
verify.preparePasteEdits({
17+
copiedFromFile: "/file2.ts",
18+
copiedTextRange: test.ranges(),
19+
providePasteEdits: true,
20+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// <reference path='./fourslash.ts' />
2+
3+
// @Filename: /file2.ts
4+
//// import { T } from './file1';
5+
////
6+
//// [|function MyFunction(param: T): T {
7+
//// type U = { value: T }
8+
//// const localVariable: U = { value: param };
9+
//// return localVariable.value;
10+
//// }|]
11+
12+
// @Filename: /file1.ts
13+
//// export type T = string;
14+
15+
verify.preparePasteEdits({
16+
copiedFromFile: "/file2.ts",
17+
copiedTextRange: test.ranges(),
18+
providePasteEdits: true
19+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path='./fourslash.ts' />
2+
3+
// @Filename: /file1.ts
4+
//// [|const a = 1;|]
5+
//// [|function foo() {
6+
//// console.log("testing");}|]
7+
//// [|//This is a comment|]
8+
9+
verify.preparePasteEdits({
10+
copiedFromFile: "/file1.ts",
11+
copiedTextRange: test.ranges(),
12+
providePasteEdits: false,
13+
})

0 commit comments

Comments
 (0)