From 8499803ae02cb657c3219aa193648968c9805851 Mon Sep 17 00:00:00 2001 From: navya9singh <108360753+navya9singh@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:50:23 -0700 Subject: [PATCH] Adding preparePasteEdits method to check if smart copy/paste should be applied (#60053) --- src/harness/client.ts | 10 ++++ src/harness/fourslashImpl.ts | 7 +++ src/harness/fourslashInterfaceImpl.ts | 8 ++++ src/server/protocol.ts | 15 ++++++ src/server/session.ts | 8 ++++ .../_namespaces/ts.preparePasteEdits.ts | 1 + src/services/_namespaces/ts.ts | 2 + src/services/preparePasteEdits.ts | 46 +++++++++++++++++++ src/services/refactors/moveToFile.ts | 4 +- src/services/services.ts | 12 +++++ src/services/types.ts | 1 + tests/baselines/reference/api/typescript.d.ts | 16 +++++++ tests/cases/fourslash/fourslash.ts | 5 ++ .../preparePasteEdits_multipleLocations.ts | 22 +++++++++ .../preparePasteEdits_resolvedIdentifiers.ts | 20 ++++++++ ...reparePasteEdits_resolvedTypeParameters.ts | 19 ++++++++ .../preparePasteEdits_returnFalse.ts | 13 ++++++ 17 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 src/services/_namespaces/ts.preparePasteEdits.ts create mode 100644 src/services/preparePasteEdits.ts create mode 100644 tests/cases/fourslash/preparePasteEdits_multipleLocations.ts create mode 100644 tests/cases/fourslash/preparePasteEdits_resolvedIdentifiers.ts create mode 100644 tests/cases/fourslash/preparePasteEdits_resolvedTypeParameters.ts create mode 100644 tests/cases/fourslash/preparePasteEdits_returnFalse.ts diff --git a/src/harness/client.ts b/src/harness/client.ts index 612b006f8a6..ec659d738c0 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -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.CommandTypes.PreparePasteEdits, args); + const response = this.processResponse(request); + return response.body; + } + getPasteEdits( { targetFile, pastedText, pasteLocations, copiedFrom }: PasteEditsArgs, formatOptions: FormatCodeSettings, diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 4e8695d78b7..43db92bf7e7 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -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); diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index 8af5de8cdc5..ec23609de6d 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -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); } @@ -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; diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 0f9233f772a..98e9ee2b95a 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -169,6 +169,7 @@ export const enum CommandTypes { GetApplicableRefactors = "getApplicableRefactors", GetEditsForRefactor = "getEditsForRefactor", GetMoveToRefactoringFileSuggestions = "getMoveToRefactoringFileSuggestions", + PreparePasteEdits = "preparePasteEdits", GetPasteEdits = "getPasteEdits", /** @internal */ GetEditsForRefactorFull = "getEditsForRefactor-full", @@ -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. */ diff --git a/src/server/session.ts b/src/server/session.ts index 0ce5c8c3fa4..0f66f5a156c 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -966,6 +966,7 @@ const invalidSyntacticModeCommands: readonly protocol.CommandTypes[] = [ protocol.CommandTypes.NavtoFull, protocol.CommandTypes.DocumentHighlights, protocol.CommandTypes.DocumentHighlightsFull, + protocol.CommandTypes.PreparePasteEdits, ]; export interface SessionOptions { @@ -2966,6 +2967,10 @@ export class Session 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 @@ -3716,6 +3721,9 @@ export class Session 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)); }, diff --git a/src/services/_namespaces/ts.preparePasteEdits.ts b/src/services/_namespaces/ts.preparePasteEdits.ts new file mode 100644 index 00000000000..96018e8d963 --- /dev/null +++ b/src/services/_namespaces/ts.preparePasteEdits.ts @@ -0,0 +1 @@ +export * from "../preparePasteEdits.js"; \ No newline at end of file diff --git a/src/services/_namespaces/ts.ts b/src/services/_namespaces/ts.ts index b98a2f743cf..de867b6e5af 100644 --- a/src/services/_namespaces/ts.ts +++ b/src/services/_namespaces/ts.ts @@ -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 }; diff --git a/src/services/preparePasteEdits.ts b/src/services/preparePasteEdits.ts new file mode 100644 index 00000000000..9d3879fe542 --- /dev/null +++ b/src/services/preparePasteEdits.ts @@ -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; +} diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index c1e102fc9c8..241e9adf2ad 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -1000,8 +1000,8 @@ function forEachTopLevelDeclaration(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: diff --git a/src/services/services.ts b/src/services/services.ts index 641ea043430..7236f146ff1 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -258,6 +258,7 @@ import { positionIsSynthesized, PossibleProgramFileInfo, PragmaMap, + PreparePasteEdits, PrivateIdentifier, Program, PropertyName, @@ -1621,6 +1622,7 @@ const invalidOperationsInSyntacticMode: readonly (keyof LanguageService)[] = [ "getRenameInfo", "findRenameLocations", "getApplicableRefactors", + "preparePasteEditsForFile", ]; export function createLanguageService( host: LanguageServiceHost, @@ -2308,6 +2310,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, @@ -3424,6 +3435,7 @@ export function createLanguageService( uncommentSelection, provideInlayHints, getSupportedCodeFixes, + preparePasteEditsForFile, getPasteEdits, mapCode, }; diff --git a/src/services/types.ts b/src/services/types.ts index 30e4ee715d2..0811d858e6a 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -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, diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 7b5d2f47a60..f4875c9070f 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -107,6 +107,7 @@ declare namespace ts { GetApplicableRefactors = "getApplicableRefactors", GetEditsForRefactor = "getEditsForRefactor", GetMoveToRefactoringFileSuggestions = "getMoveToRefactoringFileSuggestions", + PreparePasteEdits = "preparePasteEdits", GetPasteEdits = "getPasteEdits", OrganizeImports = "organizeImports", GetEditsForFileRename = "getEditsForFileRename", @@ -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. */ @@ -3556,6 +3570,7 @@ declare namespace ts { private getApplicableRefactors; private getEditsForRefactor; private getMoveToRefactoringFileSuggestions; + private preparePasteEdits; private getPasteEdits; private organizeImports; private getEditsForFileRename; @@ -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 { diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 6fa6907a9c0..8be19c1cdf6 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -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: { diff --git a/tests/cases/fourslash/preparePasteEdits_multipleLocations.ts b/tests/cases/fourslash/preparePasteEdits_multipleLocations.ts new file mode 100644 index 00000000000..a0893a9b01e --- /dev/null +++ b/tests/cases/fourslash/preparePasteEdits_multipleLocations.ts @@ -0,0 +1,22 @@ +/// + +// @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, +}) \ No newline at end of file diff --git a/tests/cases/fourslash/preparePasteEdits_resolvedIdentifiers.ts b/tests/cases/fourslash/preparePasteEdits_resolvedIdentifiers.ts new file mode 100644 index 00000000000..1420340c9fd --- /dev/null +++ b/tests/cases/fourslash/preparePasteEdits_resolvedIdentifiers.ts @@ -0,0 +1,20 @@ +/// + +// @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, +}) diff --git a/tests/cases/fourslash/preparePasteEdits_resolvedTypeParameters.ts b/tests/cases/fourslash/preparePasteEdits_resolvedTypeParameters.ts new file mode 100644 index 00000000000..dc2b539a52a --- /dev/null +++ b/tests/cases/fourslash/preparePasteEdits_resolvedTypeParameters.ts @@ -0,0 +1,19 @@ +/// + +// @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 +}) diff --git a/tests/cases/fourslash/preparePasteEdits_returnFalse.ts b/tests/cases/fourslash/preparePasteEdits_returnFalse.ts new file mode 100644 index 00000000000..6c79c1e4c83 --- /dev/null +++ b/tests/cases/fourslash/preparePasteEdits_returnFalse.ts @@ -0,0 +1,13 @@ +/// + +// @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, +})