Skip to content
Draft
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
46 changes: 44 additions & 2 deletions _packages/native-preview/src/api/async/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ import type {
ConfigResponse,
DocumentIdentifier,
DocumentPosition,
ImportAdderActionRequest,
IndexInfoResponse,
InitializeResponse,
LSPUpdateSnapshotParams,
ProjectResponse,
SignatureResponse,
SymbolResponse,
TextEdit,
TypePredicateResponse,
TypeResponse,
UpdateSnapshotParams,
Expand All @@ -67,7 +69,9 @@ import type {
AssertsThisTypePredicate,
ConditionalType,
Diagnostic,
GetImportEditsForSymbolsOptions,
IdentifierTypePredicate,
ImportAdderAction,
IndexedAccessType,
IndexInfo,
IndexType,
Expand All @@ -90,8 +94,8 @@ import type {
} from "./types.ts";

export { DiagnosticCategory, ElementFlags, ModifierFlags, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind };
export type { APIOptions, ClientSocketOptions, ClientSpawnOptions, DocumentIdentifier, DocumentPosition, LSPConnectionOptions };
export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, ConditionalType, Diagnostic, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, LiteralType, ObjectType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType };
export type { APIOptions, ClientSocketOptions, ClientSpawnOptions, DocumentIdentifier, DocumentPosition, LSPConnectionOptions, TextEdit };
export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, ConditionalType, Diagnostic, GetImportEditsForSymbolsOptions, IdentifierTypePredicate, ImportAdderAction, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, LiteralType, ObjectType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType };
export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts";

/** Type alias for the snapshot-scoped object registry */
Expand Down Expand Up @@ -278,6 +282,7 @@ export class Project {
readonly checker: Checker;
readonly emitter: Emitter;
private client: Client;
private snapshotId: string;

constructor(
data: ProjectResponse,
Expand All @@ -292,6 +297,7 @@ export class Project {
this.compilerOptions = data.compilerOptions;
this.rootFiles = data.rootFiles;
this.client = client;
this.snapshotId = snapshotId;
this.program = new Program(
snapshotId,
this.id,
Expand All @@ -307,6 +313,42 @@ export class Project {
);
this.emitter = new Emitter(client);
}

async getImportAdderEdits(file: DocumentIdentifier, actions: readonly ImportAdderAction[]): Promise<readonly TextEdit[]> {
const requestActions: ImportAdderActionRequest[] = actions.map(action => {
const requestAction = action as ImportAdderAction & { kind: string; symbol?: Symbol | string; };
switch (requestAction.kind) {
case "importSymbol":
const symbol = typeof requestAction.symbol === "string" ? requestAction.symbol : requestAction.symbol?.id;
return {
kind: "importSymbol",
symbol,
isValidTypeOnlyUseSite: action.isValidTypeOnlyUseSite,
} as ImportAdderActionRequest;
default:
return requestAction as unknown as ImportAdderActionRequest;
}
});

const data = await this.client.apiRequest<TextEdit[]>("getImportAdderEdits", {
snapshot: this.snapshotId,
project: this.id,
file,
actions: requestActions,
});
return data ?? [];
}

async getImportEditsForSymbols(file: DocumentIdentifier, symbols: readonly Symbol[], options: GetImportEditsForSymbolsOptions = {}): Promise<readonly TextEdit[]> {
return this.getImportAdderEdits(
file,
symbols.map(symbol => ({
kind: "importSymbol",
symbol,
isValidTypeOnlyUseSite: options.isValidTypeOnlyUseSite,
})),
);
}
}

export class Program {
Expand Down
12 changes: 12 additions & 0 deletions _packages/native-preview/src/api/async/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,15 @@ export interface Diagnostic {
/** Related diagnostic information */
readonly relatedInformation?: readonly Diagnostic[];
}

export interface ImportSymbolAction {
readonly kind: "importSymbol";
readonly symbol: Symbol;
readonly isValidTypeOnlyUseSite?: boolean;
}

export type ImportAdderAction = ImportSymbolAction;

export interface GetImportEditsForSymbolsOptions {
readonly isValidTypeOnlyUseSite?: boolean;
}
14 changes: 14 additions & 0 deletions _packages/native-preview/src/api/proto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ export interface DocumentPosition {
position: number;
}

export interface TextEdit {
pos: number;
end: number;
newText: string;
}

export interface ImportSymbolActionRequest {
kind: "importSymbol";
symbol: string;
isValidTypeOnlyUseSite?: boolean;
}

export type ImportAdderActionRequest = ImportSymbolActionRequest;

/**
* Resolves a DocumentIdentifier to a file name.
* If the identifier contains a URI, it is converted to a file name.
Expand Down
46 changes: 44 additions & 2 deletions _packages/native-preview/src/api/sync/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,14 @@ import type {
ConfigResponse,
DocumentIdentifier,
DocumentPosition,
ImportAdderActionRequest,
IndexInfoResponse,
InitializeResponse,
LSPUpdateSnapshotParams,
ProjectResponse,
SignatureResponse,
SymbolResponse,
TextEdit,
TypePredicateResponse,
TypeResponse,
UpdateSnapshotParams,
Expand All @@ -75,7 +77,9 @@ import type {
AssertsThisTypePredicate,
ConditionalType,
Diagnostic,
GetImportEditsForSymbolsOptions,
IdentifierTypePredicate,
ImportAdderAction,
IndexedAccessType,
IndexInfo,
IndexType,
Expand All @@ -98,8 +102,8 @@ import type {
} from "./types.ts";

export { DiagnosticCategory, ElementFlags, ModifierFlags, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind };
export type { APIOptions, ClientSocketOptions, ClientSpawnOptions, DocumentIdentifier, DocumentPosition, LSPConnectionOptions };
export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, ConditionalType, Diagnostic, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, LiteralType, ObjectType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType };
export type { APIOptions, ClientSocketOptions, ClientSpawnOptions, DocumentIdentifier, DocumentPosition, LSPConnectionOptions, TextEdit };
export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, ConditionalType, Diagnostic, GetImportEditsForSymbolsOptions, IdentifierTypePredicate, ImportAdderAction, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, LiteralType, ObjectType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType };
export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts";

/** Type alias for the snapshot-scoped object registry */
Expand Down Expand Up @@ -286,6 +290,7 @@ export class Project {
readonly checker: Checker;
readonly emitter: Emitter;
private client: Client;
private snapshotId: string;

constructor(
data: ProjectResponse,
Expand All @@ -300,6 +305,7 @@ export class Project {
this.compilerOptions = data.compilerOptions;
this.rootFiles = data.rootFiles;
this.client = client;
this.snapshotId = snapshotId;
this.program = new Program(
snapshotId,
this.id,
Expand All @@ -315,6 +321,42 @@ export class Project {
);
this.emitter = new Emitter(client);
}

getImportAdderEdits(file: DocumentIdentifier, actions: readonly ImportAdderAction[]): readonly TextEdit[] {
const requestActions: ImportAdderActionRequest[] = actions.map(action => {
const requestAction = action as ImportAdderAction & { kind: string; symbol?: Symbol | string; };
switch (requestAction.kind) {
case "importSymbol":
const symbol = typeof requestAction.symbol === "string" ? requestAction.symbol : requestAction.symbol?.id;
return {
kind: "importSymbol",
symbol,
isValidTypeOnlyUseSite: action.isValidTypeOnlyUseSite,
} as ImportAdderActionRequest;
default:
return requestAction as unknown as ImportAdderActionRequest;
}
});

const data = this.client.apiRequest<TextEdit[]>("getImportAdderEdits", {
snapshot: this.snapshotId,
project: this.id,
file,
actions: requestActions,
});
return data ?? [];
}

getImportEditsForSymbols(file: DocumentIdentifier, symbols: readonly Symbol[], options: GetImportEditsForSymbolsOptions = {}): readonly TextEdit[] {
return this.getImportAdderEdits(
file,
symbols.map(symbol => ({
kind: "importSymbol",
symbol,
isValidTypeOnlyUseSite: options.isValidTypeOnlyUseSite,
})),
);
}
}

export class Program {
Expand Down
12 changes: 12 additions & 0 deletions _packages/native-preview/src/api/sync/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,15 @@ export interface Diagnostic {
/** Related diagnostic information */
readonly relatedInformation?: readonly Diagnostic[];
}

export interface ImportSymbolAction {
readonly kind: "importSymbol";
readonly symbol: Symbol;
readonly isValidTypeOnlyUseSite?: boolean;
}

export type ImportAdderAction = ImportSymbolAction;

export interface GetImportEditsForSymbolsOptions {
readonly isValidTypeOnlyUseSite?: boolean;
}
130 changes: 130 additions & 0 deletions _packages/native-preview/test/async/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
API,
type ConditionalType,
DiagnosticCategory,
type ImportAdderAction,
type IndexedAccessType,
type IndexType,
ModifierFlags,
Expand All @@ -42,6 +43,7 @@ import {
type StringMappingType,
SymbolFlags,
type TemplateLiteralType,
type TextEdit,
TypeFlags,
TypePredicateKind,
type TypeReference,
Expand Down Expand Up @@ -146,6 +148,125 @@ describe("Snapshot", () => {
await api.close();
}
});

test("getImportEditsForSymbols adds a named import", async () => {
const source = `const value = foo;\n`;
const api = spawnAPI({
"/tsconfig.json": "{}",
"/src/index.ts": source,
"/src/foo.ts": `export const foo = 1;\n`,
});
try {
const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" });
const project = snapshot.getProject("/tsconfig.json")!;
const symbol = await project.checker.getSymbolAtPosition("/src/foo.ts", "export const ".length);
assert.ok(symbol);

const edits = await project.getImportEditsForSymbols("/src/index.ts", [await symbol.getExportSymbol()]);

assert.equal(applyTextEdits(source, edits), `import { foo } from "./foo";\n\nconst value = foo;\n`);
}
finally {
await api.close();
}
});

test("getImportAdderEdits coalesces multiple importSymbol actions", async () => {
const source = `const value = foo + bar;\n`;
const api = spawnAPI({
"/tsconfig.json": "{}",
"/src/index.ts": source,
"/src/foo.ts": `export const foo = 1;\nexport const bar = 2;\n`,
});
try {
const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" });
const project = snapshot.getProject("/tsconfig.json")!;
const foo = await project.checker.getSymbolAtPosition("/src/foo.ts", "export const ".length);
const bar = await project.checker.getSymbolAtPosition("/src/foo.ts", "export const foo = 1;\nexport const ".length);
assert.ok(foo);
assert.ok(bar);

const edits = await project.getImportAdderEdits("/src/index.ts", [
{ kind: "importSymbol", symbol: await foo.getExportSymbol() },
{ kind: "importSymbol", symbol: await bar.getExportSymbol() },
]);

assert.equal(applyTextEdits(source, edits), `import { bar, foo } from "./foo";\n\nconst value = foo + bar;\n`);
}
finally {
await api.close();
}
});

test("getImportAdderEdits adds to an existing import", async () => {
const source = `import { foo } from "./foo";\nconst value = foo + bar;\n`;
const api = spawnAPI({
"/tsconfig.json": "{}",
"/src/index.ts": source,
"/src/foo.ts": `export const foo = 1;\nexport const bar = 2;\n`,
});
try {
const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" });
const project = snapshot.getProject("/tsconfig.json")!;
const bar = await project.checker.getSymbolAtPosition("/src/foo.ts", "export const foo = 1;\nexport const ".length);
assert.ok(bar);

const edits = await project.getImportAdderEdits("/src/index.ts", [
{ kind: "importSymbol", symbol: await bar.getExportSymbol() },
]);

assert.equal(applyTextEdits(source, edits), `import { bar, foo } from "./foo";\nconst value = foo + bar;\n`);
}
finally {
await api.close();
}
});

test("getImportAdderEdits returns no edits for non-exported symbols", async () => {
const source = `const value = local;\n`;
const api = spawnAPI({
"/tsconfig.json": "{}",
"/src/index.ts": source,
"/src/foo.ts": `const local = 1;\n`,
});
try {
const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" });
const project = snapshot.getProject("/tsconfig.json")!;
const symbol = await project.checker.getSymbolAtPosition("/src/foo.ts", "const ".length);
assert.ok(symbol);

const edits = await project.getImportAdderEdits("/src/index.ts", [
{ kind: "importSymbol", symbol },
]);

assert.deepEqual(edits, []);
}
finally {
await api.close();
}
});

test("getImportAdderEdits rejects invalid actions", async () => {
const api = spawnAPI();
try {
const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" });
const project = snapshot.getProject("/tsconfig.json")!;
const symbol = await project.checker.getSymbolAtPosition("/src/foo.ts", 13);
assert.ok(symbol);

await assert.rejects( // @sync: assert.throws(
() => project.getImportAdderEdits("/src/index.ts", [{ kind: "unknown", symbol: symbol.id } as unknown as ImportAdderAction]),
/unknown import adder action kind "unknown"/,
);
await assert.rejects( // @sync: assert.throws(
() => project.getImportAdderEdits("/src/index.ts", [{ kind: "importSymbol", symbol: { ...symbol, id: "sbad" } } as unknown as ImportAdderAction]),
/symbol handle "sbad" not found/,
);
}
finally {
await api.close();
}
});
});

describe("SourceFile", () => {
Expand Down Expand Up @@ -2595,3 +2716,12 @@ function rangeOf(source: string, searchString: string, occurrence: number = 0):
}
return { pos: index, end: index + searchString.length };
}

function applyTextEdits(source: string, edits: readonly TextEdit[]): string {
const sorted = [...edits].sort((a, b) => b.pos - a.pos);
let result = source;
for (const edit of sorted) {
result = result.slice(0, edit.pos) + edit.newText + result.slice(edit.end);
}
return result;
}
Loading
Loading