diff --git a/src/harness/client.ts b/src/harness/client.ts index 612b006f8a6b4..c0b8ecc954768 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -800,6 +800,7 @@ export class SessionClient implements LanguageService { } mapCode: typeof notImplemented = notImplemented; + getImports: typeof notImplemented = notImplemented; private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs { return typeof positionOrRange === "number" diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 4e8695d78b734..9eaa30ccb567a 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -4631,6 +4631,28 @@ ${changes.join("\n// ---\n")} ${after}`; this.baseline("mapCode", baseline, ".mapCode.ts"); } + + public verifyGetImports(fileName: string, expectedImports: string[]): void { + const actualImports = this.languageService.getImports(fileName); + if (actualImports.length !== expectedImports.length) { + throw new Error(`Expected ${expectedImports.length} imports for ${fileName}, got ${actualImports.length} + Expected: +${expectedImports} + Actual: +${actualImports} +`); + } + for (let i = 0; i < expectedImports.length; i++) { + if (actualImports[i] !== expectedImports[i]) { + throw new Error(`Expected at ${fileName} index ${i}: ${expectedImports[i]}, got ${actualImports[i]} + Expected: +${expectedImports} + Actual: +${actualImports} +`); + } + } + } } function updateTextRangeForTextChanges({ pos, end }: ts.TextRange, textChanges: readonly ts.TextChange[]): ts.TextRange { diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index 8af5de8cdc5ba..b56792c343f04 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -257,6 +257,10 @@ export class VerifyNegatable { public baselineMapCode(ranges: FourSlash.Range[][], changes: string[] = []): void { this.state.baselineMapCode(ranges, changes); } + + public getImports(fileName: string, imports: string[]): void { + return this.state.verifyGetImports(fileName, imports); + } } export interface CompletionsResult { @@ -2039,3 +2043,8 @@ export interface RenameOptions { readonly providePrefixAndSuffixTextForRename?: boolean; readonly quotePreference?: "auto" | "double" | "single"; } + +export interface VerifyGetImportsOptions { + fileName: string; + imports: string[]; +} diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 0f9233f772aa4..2cb8f15a2607b 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -201,6 +201,7 @@ export const enum CommandTypes { ProvideInlayHints = "provideInlayHints", WatchChange = "watchChange", MapCode = "mapCode", + CopilotRelated = "copilotRelated", } /** @@ -2391,6 +2392,18 @@ export interface MapCodeResponse extends Response { body: readonly FileCodeEdits[]; } +export interface CopilotRelatedRequest extends FileRequest { + command: CommandTypes.CopilotRelated; + arguments: FileRequestArgs; +} + +export interface CopilotRelatedItems { + relatedFiles: readonly string[]; +} + +export interface CopilotRelatedResponse extends Response { + body: CopilotRelatedItems; +} /** * Synchronous request for semantic diagnostics of one file. */ diff --git a/src/server/session.ts b/src/server/session.ts index 0ce5c8c3fa44c..efd8ceab0e4c9 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -937,6 +937,7 @@ const invalidPartialSemanticModeCommands: readonly protocol.CommandTypes[] = [ protocol.CommandTypes.ProvideCallHierarchyIncomingCalls, protocol.CommandTypes.ProvideCallHierarchyOutgoingCalls, protocol.CommandTypes.GetPasteEdits, + protocol.CommandTypes.CopilotRelated, ]; const invalidSyntacticModeCommands: readonly protocol.CommandTypes[] = [ @@ -2029,7 +2030,6 @@ export class Session implements EventSender { }; }); } - private mapCode(args: protocol.MapCodeRequestArgs): protocol.FileCodeEdits[] { const formatOptions = this.getHostFormatOptions(); const preferences = this.getHostPreferences(); @@ -2050,6 +2050,14 @@ export class Session implements EventSender { return this.mapTextChangesToCodeEdits(changes); } + private getCopilotRelatedInfo(args: protocol.FileRequestArgs): protocol.CopilotRelatedItems { + const { file, project } = this.getFileAndProject(args); + + return { + relatedFiles: project.getLanguageService().getImports(file), + }; + } + private setCompilerOptionsForInferredProjects(args: protocol.SetCompilerOptionsForInferredProjectsArgs): void { this.projectService.setCompilerOptionsForInferredProjects(args.options, args.projectRootPath); } @@ -3783,6 +3791,9 @@ export class Session implements EventSender { [protocol.CommandTypes.MapCode]: (request: protocol.MapCodeRequest) => { return this.requiredResponse(this.mapCode(request.arguments)); }, + [protocol.CommandTypes.CopilotRelated]: (request: protocol.CopilotRelatedRequest) => { + return this.requiredResponse(this.getCopilotRelatedInfo(request.arguments)); + }, })); public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse): void { diff --git a/src/services/services.ts b/src/services/services.ts index c51089dfe25de..6243673b3cd03 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2,6 +2,7 @@ import { __String, ApplicableRefactorInfo, ApplyCodeActionCommandResult, + arrayFrom, AssignmentDeclarationKind, BaseType, BinaryExpression, @@ -233,6 +234,7 @@ import { Node, NodeArray, NodeFlags, + nodeIsSynthesized, noop, normalizePath, normalizeSpans, @@ -1593,6 +1595,7 @@ const invalidOperationsInPartialSemanticMode: readonly (keyof LanguageService)[] "provideInlayHints", "getSupportedCodeFixes", "getPasteEdits", + "getImports", ]; const invalidOperationsInSyntacticMode: readonly (keyof LanguageService)[] = [ @@ -3345,6 +3348,18 @@ export function createLanguageService( ); } + function getImports(fileName: string): readonly string[] { + synchronizeHostData(); + const file = getValidSourceFile(fileName); + let imports: Set | undefined; + for (const specifier of file.imports) { + if (nodeIsSynthesized(specifier)) continue; + const name = program.getResolvedModuleFromModuleSpecifier(specifier, file)?.resolvedModule?.resolvedFileName; + if (name) (imports ??= new Set()).add(name); + } + return imports ? arrayFrom(imports) : emptyArray; + } + const ls: LanguageService = { dispose, cleanupSemanticCache, @@ -3418,6 +3433,7 @@ export function createLanguageService( getSupportedCodeFixes, getPasteEdits, mapCode, + getImports, }; switch (languageServiceMode) { diff --git a/src/services/types.ts b/src/services/types.ts index 30e4ee715d2af..5ab5c54549e3a 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -697,6 +697,7 @@ export interface LanguageService { getSupportedCodeFixes(fileName?: string): readonly string[]; /** @internal */ mapCode(fileName: string, contents: string[], focusLocations: TextSpan[][] | undefined, formatOptions: FormatCodeSettings, preferences: UserPreferences): readonly FileTextChanges[]; + /** @internal */ getImports(fileName: string): readonly string[]; dispose(): void; getPasteEdits( diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 7b5d2f47a602c..b3fba6353f7b6 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -122,6 +122,7 @@ declare namespace ts { ProvideInlayHints = "provideInlayHints", WatchChange = "watchChange", MapCode = "mapCode", + CopilotRelated = "copilotRelated", } /** * A TypeScript Server message @@ -1816,6 +1817,16 @@ declare namespace ts { export interface MapCodeResponse extends Response { body: readonly FileCodeEdits[]; } + export interface CopilotRelatedRequest extends FileRequest { + command: CommandTypes.CopilotRelated; + arguments: FileRequestArgs; + } + export interface CopilotRelatedItems { + relatedFiles: readonly string[]; + } + export interface CopilotRelatedResponse extends Response { + body: CopilotRelatedItems; + } /** * Synchronous request for semantic diagnostics of one file. */ @@ -3500,6 +3511,7 @@ declare namespace ts { private getDocumentHighlights; private provideInlayHints; private mapCode; + private getCopilotRelatedInfo; private setCompilerOptionsForInferredProjects; private getProjectInfo; private getProjectInfoWorker; diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 6fa6907a9c0a6..4153846531258 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -467,6 +467,7 @@ declare namespace FourSlashInterface { } }): void; baselineMapCode(ranges: Range[][], changes: string[]): void; + getImports(fileName: string, imports: string[]): void; } class edit { caretPosition(): Marker; diff --git a/tests/cases/fourslash/getImportsDuplicate.ts b/tests/cases/fourslash/getImportsDuplicate.ts new file mode 100644 index 0000000000000..1e4e069978a75 --- /dev/null +++ b/tests/cases/fourslash/getImportsDuplicate.ts @@ -0,0 +1,16 @@ +/// + +// @Filename: /first.ts +//// export function foo() { +//// return 1; +//// } +//// export function bar() { +//// return 2; +//// } + +// @Filename: /index.ts +//// import { foo } from "./first"; +//// import { bar } from './first'; +//// console.log(foo() + bar()) + +verify.getImports('/index.ts', ['/first.ts']) diff --git a/tests/cases/fourslash/getImportsDynamic.ts b/tests/cases/fourslash/getImportsDynamic.ts new file mode 100644 index 0000000000000..0a16718bdcb02 --- /dev/null +++ b/tests/cases/fourslash/getImportsDynamic.ts @@ -0,0 +1,12 @@ +/// + +// @Filename: /first.ts +//// export function foo() { +//// return 1; +//// } +// @Filename: /index.ts +//// let bar: typeof import('./first').foo = function bar() { +//// return 2; +//// } + +verify.getImports('/index.ts', ['/first.ts']) diff --git a/tests/cases/fourslash/getImportsJSXFactory.ts b/tests/cases/fourslash/getImportsJSXFactory.ts new file mode 100644 index 0000000000000..2ccc885443a8b --- /dev/null +++ b/tests/cases/fourslash/getImportsJSXFactory.ts @@ -0,0 +1,108 @@ +/// + +// @strict: true +// @jsx: react-jsx +// @jsxImportSource: preact +// @filename: /node_modules/preact/index.d.ts +//// type Defaultize = +//// // Distribute over unions +//// Props extends any // Make any properties included in Default optional +//// ? Partial>> & +//// // Include the remaining properties from Props +//// Pick> +//// : never; +//// export namespace JSXInternal { +//// interface HTMLAttributes { } +//// interface SVGAttributes { } +//// type LibraryManagedAttributes = Component extends { +//// defaultProps: infer Defaults; +//// } +//// ? Defaultize +//// : Props; +//// +//// interface IntrinsicAttributes { +//// key?: any; +//// } +//// +//// interface Element extends VNode { } +//// +//// interface ElementClass extends Component { } +//// +//// interface ElementAttributesProperty { +//// props: any; +//// } +//// +//// interface ElementChildrenAttribute { +//// children: any; +//// } +//// +//// interface IntrinsicElements { +//// div: HTMLAttributes; +//// } +//// } +//// export const Fragment: unique symbol; +//// export type ComponentType = {}; +//// export type ComponentChild = {}; +//// export type ComponentChildren = {}; +//// export type VNode = {}; +//// export type Attributes = {}; +//// export type Component = {}; +// @filename: /node_modules/preact/jsx-runtime/index.d.ts +//// export { Fragment } from '..'; +//// import { +//// ComponentType, +//// ComponentChild, +//// ComponentChildren, +//// VNode, +//// Attributes +//// } from '..'; +//// import { JSXInternal } from '..'; +//// +//// export function jsx( +//// type: string, +//// props: JSXInternal.HTMLAttributes & +//// JSXInternal.SVGAttributes & +//// Record & { children?: ComponentChild }, +//// key?: string +//// ): VNode; +//// export function jsx

( +//// type: ComponentType

, +//// props: Attributes & P & { children?: ComponentChild }, +//// key?: string +//// ): VNode; +//// +//// +//// export function jsxs( +//// type: string, +//// props: JSXInternal.HTMLAttributes & +//// JSXInternal.SVGAttributes & +//// Record & { children?: ComponentChild[] }, +//// key?: string +//// ): VNode; +//// export function jsxs

( +//// type: ComponentType

, +//// props: Attributes & P & { children?: ComponentChild[] }, +//// key?: string +//// ): VNode; +//// +//// +//// export function jsxDEV( +//// type: string, +//// props: JSXInternal.HTMLAttributes & +//// JSXInternal.SVGAttributes & +//// Record & { children?: ComponentChildren }, +//// key?: string +//// ): VNode; +//// export function jsxDEV

( +//// type: ComponentType

, +//// props: Attributes & P & { children?: ComponentChildren }, +//// key?: string +//// ): VNode; +//// +//// export import JSX = JSXInternal; +//// +// @filename: /index.tsx +//// export const Comp = () =>

; + +verify.noErrors() +verify.getImports('/index.tsx', []) diff --git a/tests/cases/fourslash/getImportsNone.ts b/tests/cases/fourslash/getImportsNone.ts new file mode 100644 index 0000000000000..d408cb6f14da5 --- /dev/null +++ b/tests/cases/fourslash/getImportsNone.ts @@ -0,0 +1,12 @@ +/// + +// @Filename: /index.ts +//// function foo() { +//// return 1; +//// } +//// function bar() { +//// return 2; +//// } +//// + +verify.getImports('/index.ts', []) diff --git a/tests/cases/fourslash/getImportsOne.ts b/tests/cases/fourslash/getImportsOne.ts new file mode 100644 index 0000000000000..7ebf0a51f2b7f --- /dev/null +++ b/tests/cases/fourslash/getImportsOne.ts @@ -0,0 +1,14 @@ +/// + +// @Filename: /first.ts +//// export function foo() { +//// return 1; +//// } +// @Filename: /index.ts +//// import { foo } from "./first"; +//// function bar() { +//// return 2; +//// } +//// + +verify.getImports('/index.ts', ['/first.ts']) diff --git a/tests/cases/fourslash/getImportsOneJs.ts b/tests/cases/fourslash/getImportsOneJs.ts new file mode 100644 index 0000000000000..1d9157f73e917 --- /dev/null +++ b/tests/cases/fourslash/getImportsOneJs.ts @@ -0,0 +1,15 @@ +/// + +// @checkJs: true +// @Filename: /first.ts +//// export function foo() { +//// return 1; +//// } +// @Filename: /index.js +//// const { foo } = require("./first"); +//// function bar() { +//// return 2; +//// } +//// + +verify.getImports('/index.js', ['/first.ts']) diff --git a/tests/cases/fourslash/getImportsReexport.ts b/tests/cases/fourslash/getImportsReexport.ts new file mode 100644 index 0000000000000..d5832405ae3b6 --- /dev/null +++ b/tests/cases/fourslash/getImportsReexport.ts @@ -0,0 +1,15 @@ +/// + +// @Filename: /first.ts +//// export function foo() { +//// return 1; +//// } +// @Filename: /index.ts +//// export { foo } from "./first"; +//// function bar() { +//// return 2; +//// } +//// + + +verify.getImports('/index.ts', ['/first.ts']) diff --git a/tests/cases/fourslash/getImportsTslib.ts b/tests/cases/fourslash/getImportsTslib.ts new file mode 100644 index 0000000000000..8f0519a3b0834 --- /dev/null +++ b/tests/cases/fourslash/getImportsTslib.ts @@ -0,0 +1,19 @@ +/// + +// @importHelpers: true +// @target: es2015 +// @lib: es2015 +// @module: commonjs +// @Filename: /node_modules/tslib/index.d.ts +//// export function __awaiter(...args: any): any; +// @Filename: /first.ts +//// export function foo() { +//// return 2 +//// } +// @Filename: /index.ts +//// export async function importer() { +//// const mod = await import("./first"); +//// } + +verify.noErrors() +verify.getImports('/index.ts', ['/first.ts'])