diff --git a/src/harness/client.ts b/src/harness/client.ts index ec659d738c0..88458aa197d 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 43db92bf7e7..89b0e236a24 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -4638,6 +4638,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 ec23609de6d..0dd8236f94b 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 { @@ -2047,3 +2051,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 98e9ee2b95a..e2e699dc80b 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -202,6 +202,7 @@ export const enum CommandTypes { ProvideInlayHints = "provideInlayHints", WatchChange = "watchChange", MapCode = "mapCode", + CopilotRelated = "copilotRelated", } /** @@ -2406,6 +2407,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 0f66f5a156c..2c2f2895f01 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[] = [ @@ -2030,7 +2031,6 @@ export class Session implements EventSender { }; }); } - private mapCode(args: protocol.MapCodeRequestArgs): protocol.FileCodeEdits[] { const formatOptions = this.getHostFormatOptions(); const preferences = this.getHostPreferences(); @@ -2051,6 +2051,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); } @@ -3791,6 +3799,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 7236f146ff1..8f749a3217f 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, @@ -1602,6 +1604,7 @@ const invalidOperationsInPartialSemanticMode: readonly (keyof LanguageService)[] "provideInlayHints", "getSupportedCodeFixes", "getPasteEdits", + "getImports", ]; const invalidOperationsInSyntacticMode: readonly (keyof LanguageService)[] = [ @@ -3364,6 +3367,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, @@ -3438,6 +3453,7 @@ export function createLanguageService( preparePasteEditsForFile, getPasteEdits, mapCode, + getImports, }; switch (languageServiceMode) { diff --git a/src/services/types.ts b/src/services/types.ts index 0811d858e6a..69fdb42cf69 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; preparePasteEditsForFile(fileName: string, copiedTextRanges: TextRange[]): boolean; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index bdf8fa51678..7b03ccbe19b 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -123,6 +123,7 @@ declare namespace ts { ProvideInlayHints = "provideInlayHints", WatchChange = "watchChange", MapCode = "mapCode", + CopilotRelated = "copilotRelated", } /** * A TypeScript Server message @@ -1830,6 +1831,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. */ @@ -3514,6 +3525,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 8be19c1cdf6..f4f4b960753 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -472,6 +472,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 00000000000..1e4e069978a --- /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 00000000000..0a16718bdcb --- /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 00000000000..2ccc885443a --- /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 00000000000..d408cb6f14d --- /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 00000000000..7ebf0a51f2b --- /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 00000000000..1d9157f73e9 --- /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 00000000000..d5832405ae3 --- /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 00000000000..8f0519a3b08 --- /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'])