Compare commits

...

39 commits

Author SHA1 Message Date
Andy Hanson 1645750300 wip 2017-09-27 12:38:55 -07:00
Andy Hanson 474653dfcc wip: tests pass 2017-09-27 11:39:20 -07:00
Andy Hanson 71c7c3f46f Merge branch 'exportsincompletionlist' of https://github.com/minestarks/TypeScript into exportsincompletionlist 2017-09-27 11:09:06 -07:00
Mine Starks 211e3f92bb Lint 2017-08-16 16:59:07 -07:00
Mine Starks 49bb2b8d8b Merge branch 'master' of https://github.com/Microsoft/TypeScript into importcompletions-rebase 2017-08-16 16:56:04 -07:00
Mine Starks a41f3dfff6 Filter symbols after gathering exports instead of before 2017-08-16 10:49:35 -07:00
Mine Starks f0c983a605 importFixes.ts: Remove dummy getCanonicalFileName line 2017-08-10 14:18:09 -07:00
Mine Starks d1bdc25a9a importFixes.ts: Use local host instead of context.host in getCodeActionForImport 2017-08-10 14:18:09 -07:00
Mine Starks bc14bb0fe4 importFixes.ts: Use local newLineCharacter instead of context.newLineCharacter in getCodeActionForImport 2017-08-10 14:16:04 -07:00
Mine Starks e68c951930 importFixes.ts: Create and use importFixContext within getCodeActions lambda 2017-08-10 14:16:04 -07:00
Mine Starks 72dd99fec9 completions.ts: In getCompletionEntryDetails, if there's symbolOriginInfo, call getCodeActionForImport 2017-08-10 14:06:33 -07:00
Mine Starks b11f6e8f3e importFixes.ts: Move getCodeActionForImport out into an export, immediately below convertToImportCodeFixContext 2017-08-10 14:06:33 -07:00
Mine Starks 2ea36a603c importFixes.ts: Remove local getUniqueSymbolId function and add checker parameter to calls to it 2017-08-10 14:04:40 -07:00
Mine Starks 8e5febb4c6 importFixes.ts: Remove useCaseSensitiveFileNames altogether from getCodeActions lambda 2017-08-10 14:03:30 -07:00
Mine Starks 13a47e2405 importFixes.ts: Use symbolToken in getCodeActionForImport 2017-08-10 14:02:06 -07:00
Mine Starks fa33d5016b Move the declaration for lastImportDeclaration out of the getCodeActions lambda into getCodeActionForImport 2017-08-10 13:59:11 -07:00
Mine Starks de7b821a82 importFixes.ts: Move createCodeAction out, immediately above convertToImportCodeFixContext 2017-08-10 13:57:28 -07:00
Mine Starks e912a76682 importFixes.ts: Use cachedImportDeclarations from context in getCodeActionForImport 2017-08-10 13:54:27 -07:00
Mine Starks e7d966b2e6 importFixes.ts: Remove moduleSymbol parameter from getImportDeclarations and use the ambient one 2017-08-10 13:52:30 -07:00
Mine Starks 2875c15bbd importFixes.ts: Add context: ImportCodeFixContext parameter to getCodeActionForImport, update call sites, destructure it, use compilerOptions in getModuleSpecifierForNewImport 2017-08-10 13:50:23 -07:00
Mine Starks 8d5e075394 importFixes.ts: Add convertToImportCodeFixContext function and reference it from the getCodeActions lambda 2017-08-10 13:43:26 -07:00
Mine Starks 22c3373aee importFixes.ts: Move createChangeTracker into getCodeActionForImport, immediately after getImportDeclarations 2017-08-10 13:41:16 -07:00
Mine Starks 380b2994b2 Move getImportDeclarations into getCodeActionForImport, immediately after the implementation 2017-08-10 13:31:28 -07:00
Mine Starks 95a9c01ca8 importFixes.ts: Add types ImportDeclarationMap and ImportCodeFixContext 2017-08-10 13:16:18 -07:00
Mine Starks ae0ab477f7 completions.ts: Add TODO comment 2017-08-10 13:16:17 -07:00
Mine Starks abe1fdb0ea completions.ts, services.ts: Plumb host and rulesProvider into getCompletionEntryDetails 2017-08-10 13:16:17 -07:00
Mine Starks 041302fa1d completions.ts: Populate list with possible exports (implement getSymbolsFromOtherSourceFileExports) 2017-08-10 13:16:17 -07:00
Mine Starks b024285c23 completions.ts: Set CompletionEntry.hasAction when symbol is found in symbolToOriginInfoMap (meaning there's an import action) 2017-08-10 13:15:12 -07:00
Mine Starks c5cc2f148c utilities.ts: Add getOtherModuleSymbols, getUniqueSymbolIdAsString, getUniqueSymbolId 2017-08-10 12:59:53 -07:00
Mine Starks c838093e98 completions.ts: add symbolToOriginInfoMap parameter to getCompletionEntriesFromSymbols and to return value of getCompletionData 2017-08-10 12:58:31 -07:00
Mine Starks 9940c9261a completions.ts, services.ts: Plumb allSourceFiles into new function getSymbolsFromOtherSourceFileExports inside getCompletionData 2017-08-10 11:12:01 -07:00
Mine Starks 25831a8261 completions.ts, services.ts: Add allSourceFiles parameter to getCompletionsAtPosition 2017-08-10 11:08:40 -07:00
Mine Starks 0aa865f6cc completions.ts: define SymbolOriginInfo type 2017-08-10 11:07:23 -07:00
Mine Starks 8a1a124856 session.ts, services.ts, types.ts: Add formattingOptions parameter to getCompletionEntryDetails 2017-08-10 11:07:22 -07:00
Mine Starks 15b73d09c3 protocol.ts, session.ts, types.ts: add hasAction to CompletionEntry 2017-08-10 11:05:52 -07:00
Mine Starks a84b5b58ee protocol.ts, session.ts: Add codeActions member to CompletionEntryDetails protocol 2017-08-10 11:03:01 -07:00
Mine Starks 087de799d2 client.ts, completions.ts, types.ts: Add codeActions member to CompletionEntryDetails 2017-08-10 10:56:20 -07:00
Mine Starks 5bef866249 tsserverProjectSystem.ts: add two tests 2017-08-10 10:56:19 -07:00
Mine Starks d99675bb74 checker.ts: Remove null check on symbols 2017-08-10 10:56:19 -07:00
11 changed files with 875 additions and 554 deletions

View file

@ -6251,11 +6251,13 @@ namespace ts {
function symbolsToArray(symbols: SymbolTable): Symbol[] {
const result: Symbol[] = [];
if (symbols) {
symbols.forEach((symbol, id) => {
if (!isReservedMemberName(id)) {
result.push(symbol);
}
});
}
return result;
}

View file

@ -3788,6 +3788,43 @@ namespace ts.projectSystem {
});
});
describe("import in completion list", () => {
it("should include exported members of all source files", () => {
const file1: FileOrFolder = {
path: "/a/b/file1.ts",
content: `
export function Test1() { }
export function Test2() { }
`
};
const file2: FileOrFolder = {
path: "/a/b/file2.ts",
content: `
import { Test2 } from "./file1";
t`
};
const configFile: FileOrFolder = {
path: "/a/b/tsconfig.json",
content: "{}"
};
const host = createServerHost([file1, file2, configFile]);
const service = createProjectService(host);
service.openClientFile(file2.path);
const completions1 = service.configuredProjects[0].getLanguageService().getCompletionsAtPosition(file2.path, file2.content.length);
const test1Entry = find(completions1.entries, e => e.name === "Test1");
const test2Entry = find(completions1.entries, e => e.name === "Test2");
assert.isDefined(test1Entry, "should contain 'Test1'");
assert.isDefined(test2Entry, "should contain 'Test2'");
assert.isTrue(test1Entry.hasAction, "should set the 'hasAction' property to true for Test1");
assert.isUndefined(test2Entry.hasAction, "should not set the 'hasAction' property for Test2");
});
});
describe("import helpers", () => {
it("should not crash in tsserver", () => {
const f1 = {
@ -4127,6 +4164,70 @@ namespace ts.projectSystem {
});
});
describe("completion entry with code actions", () => {
it("should work for symbols from non-imported modules", () => {
const moduleFile = {
path: "/a/b/moduleFile.ts",
content: `export const guitar = 10;`
};
const file1 = {
path: "/a/b/file2.ts",
content: `var x:`
};
const globalFile = {
path: "/a/b/globalFile.ts",
content: `interface Jazz { }`
};
const ambientModuleFile = {
path: "/a/b/ambientModuleFile.ts",
content:
`declare module "windyAndWarm" {
export const chetAtkins = "great";
}`
};
const defaultModuleFile = {
path: "/a/b/defaultModuleFile.ts",
content:
`export default function egyptianElla() { };`
};
const configFile = {
path: "/a/b/tsconfig.json",
content: "{}"
};
const host = createServerHost([moduleFile, file1, globalFile, ambientModuleFile, defaultModuleFile, configFile]);
const session = createSession(host);
const projectService = session.getProjectService();
projectService.openClientFile(file1.path);
checkEntryDetail(1, "guitar", /*hasAction*/ true, `import { guitar } from "./moduleFile";\n\n`);
checkEntryDetail(1, "chetAtkins", /*hasAction*/ true, `import { chetAtkins } from "windyAndWarm";\n\n`);
checkEntryDetail(1, "egyptianElla", /*hasAction*/ true, `import egyptianElla from "./defaultModuleFile";\n\n`);
checkEntryDetail(7, "Jazz", /*hasAction*/ false);
function checkEntryDetail(offset: number, entryName: string, hasAction: boolean, insertString?: string) {
const request = makeSessionRequest<protocol.CompletionDetailsRequestArgs>(
CommandNames.CompletionDetails,
{ entryNames: [entryName], file: file1.path, line: 1, offset, projectFileName: configFile.path });
const response = session.executeCommand(request).response as protocol.CompletionEntryDetails[];
assert.equal(response.length, 1);
const entryDetails = response[0];
if (!hasAction) {
assert.isUndefined(entryDetails.codeActions);
}
else {
const action = entryDetails.codeActions[0];
assert.equal(action.changes[0].fileName, file1.path);
assert.deepEqual(action.changes[0], <protocol.FileCodeEdits>{
fileName: file1.path,
textChanges: [{ start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 }, newText: insertString }]
});
}
}
});
});
describe("maxNodeModuleJsDepth for inferred projects", () => {
it("should be set to 2 if the project has js root files", () => {
const file1: FileOrFolder = {

View file

@ -198,7 +198,9 @@ namespace ts.server {
const request = this.processRequest<protocol.CompletionDetailsRequest>(CommandNames.CompletionDetails, args);
const response = this.processResponse<protocol.CompletionDetailsResponse>(request);
Debug.assert(response.body.length === 1, "Unexpected length of completion details response body.");
return response.body[0];
const convertedCodeActions = map(response.body[0].codeActions, codeAction => this.convertCodeActions(codeAction, fileName));
return { ...response.body[0], codeActions: convertedCodeActions };
}
getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol {

View file

@ -1659,6 +1659,11 @@ namespace ts.server.protocol {
* this span should be used instead of the default one.
*/
replacementSpan?: TextSpan;
/**
* Indicating if commiting this completion entry will require additional code action to be
* made to avoid errors. The code action is normally adding an additional import statement.
*/
hasAction?: true;
}
/**
@ -1691,6 +1696,11 @@ namespace ts.server.protocol {
* JSDoc tags for the symbol.
*/
tags: JSDocTagInfo[];
/**
* The associated code actions for this entry
*/
codeActions?: CodeAction[];
}
export interface CompletionsResponse extends Response {

View file

@ -1186,9 +1186,15 @@ namespace ts.server {
if (simplifiedResult) {
return mapDefined(completions && completions.entries, entry => {
if (completions.isMemberCompletion || (entry.name.toLowerCase().indexOf(prefix.toLowerCase()) === 0)) {
const { name, kind, kindModifiers, sortText, replacementSpan } = entry;
const { name, kind, kindModifiers, sortText, replacementSpan, hasAction } = entry;
const convertedSpan = replacementSpan ? this.decorateSpan(replacementSpan, scriptInfo) : undefined;
return { name, kind, kindModifiers, sortText, replacementSpan: convertedSpan };
const newEntry: protocol.CompletionEntry = { name, kind, kindModifiers, sortText, replacementSpan: convertedSpan };
// avoid serialization when hasAction = false
if (hasAction) {
newEntry.hasAction = true;
}
return newEntry;
}
}).sort((a, b) => compareStrings(a.name, b.name));
}
@ -1201,9 +1207,18 @@ namespace ts.server {
const { file, project } = this.getFileAndProject(args);
const scriptInfo = project.getScriptInfoForNormalizedPath(file);
const position = this.getPosition(args, scriptInfo);
const formattingOptions = project.projectService.getFormatCodeOptions(file);
return mapDefined(args.entryNames, entryName =>
project.getLanguageService().getCompletionEntryDetails(file, position, entryName));
return mapDefined(args.entryNames, entryName => {
const details = project.getLanguageService().getCompletionEntryDetails(file, position, entryName, formattingOptions);
if (details) {
const mappedCodeActions = map(details.codeActions, action => this.mapCodeAction(action, scriptInfo));
return { ...details, codeActions: mappedCodeActions };
}
else {
return undefined;
}
});
}
private getCompileOnSaveAffectedFileList(args: protocol.FileRequestArgs): ReadonlyArray<protocol.CompileOnSaveAffectedFileListSingleProject> {

View file

@ -11,11 +11,28 @@ namespace ts.codefix {
});
type ImportCodeActionKind = "CodeChange" | "InsertingIntoExistingImport" | "NewImport";
// this is a module id -> module import declaration map
type ImportDeclarationMap = AnyImportSyntax[][];
interface ImportCodeAction extends CodeAction {
kind: ImportCodeActionKind;
moduleSpecifier?: string;
}
export interface ImportCodeFixContext {
host: LanguageServiceHost;
symbolName: string;
newLineCharacter: string;
rulesProvider: formatting.RulesProvider;
sourceFile: SourceFile;
checker: TypeChecker;
compilerOptions: CompilerOptions;
getCanonicalFileName: (fileName: string) => string;
// this is a module id -> module import declaration map
cachedImportDeclarations?: ImportDeclarationMap;
symbolToken?: Node;
}
enum ModuleSpecifierComparison {
Better,
Equal,
@ -122,77 +139,54 @@ namespace ts.codefix {
}
}
function getImportCodeActions(context: CodeFixContext): ImportCodeAction[] {
const sourceFile = context.sourceFile;
const checker = context.program.getTypeChecker();
const allSourceFiles = context.program.getSourceFiles();
const useCaseSensitiveFileNames = context.host.useCaseSensitiveFileNames ? context.host.useCaseSensitiveFileNames() : false;
const token = getTokenAtPosition(sourceFile, context.span.start, /*includeJsDocComment*/ false);
const name = token.getText();
const symbolIdActionMap = new ImportCodeActionMap();
// this is a module id -> module import declaration map
const cachedImportDeclarations: AnyImportSyntax[][] = [];
let lastImportDeclaration: Node;
const currentTokenMeaning = getMeaningFromLocation(token);
if (context.errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code) {
const umdSymbol = checker.getSymbolAtLocation(token);
let symbol: ts.Symbol;
let symbolName: string;
if (umdSymbol.flags & ts.SymbolFlags.Alias) {
symbol = checker.getAliasedSymbol(umdSymbol);
symbolName = name;
function createCodeAction(
description: DiagnosticMessage,
diagnosticArgs: string[],
changes: FileTextChanges[],
kind: ImportCodeActionKind,
moduleSpecifier?: string): ImportCodeAction {
return {
description: formatMessage.apply(undefined, [undefined, description].concat(<any[]>diagnosticArgs)),
changes,
kind,
moduleSpecifier
};
}
else if (isJsxOpeningLikeElement(token.parent) && token.parent.tagName === token) {
// The error wasn't for the symbolAtLocation, it was for the JSX tag itself, which needs access to e.g. `React`.
symbol = checker.getAliasedSymbol(checker.resolveName(checker.getJsxNamespace(), token.parent.tagName, SymbolFlags.Value));
symbolName = symbol.name;
function convertToImportCodeFixContext(context: CodeFixContext): ImportCodeFixContext {
const useCaseSensitiveFileNames = context.host.useCaseSensitiveFileNames ? context.host.useCaseSensitiveFileNames() : false;
const checker = context.program.getTypeChecker();
const token = getTokenAtPosition(context.sourceFile, context.span.start, /*includeJsDocComment*/ false);
return {
host: context.host,
newLineCharacter: context.newLineCharacter,
rulesProvider: context.rulesProvider,
sourceFile: context.sourceFile,
checker,
compilerOptions: context.program.getCompilerOptions(),
cachedImportDeclarations: [],
getCanonicalFileName: createGetCanonicalFileName(useCaseSensitiveFileNames),
symbolName: token.getText(),
symbolToken: token
};
}
export function getCodeActionForImport(moduleSymbol: Symbol, context: ImportCodeFixContext, symbolName: string, isDefault?: boolean, isNamespaceImport?: boolean): ImportCodeAction[] {
const existingDeclarations = getImportDeclarations(moduleSymbol, context);
if (existingDeclarations.length > 0) {
// With an existing import statement, there are more than one actions the user can do.
return getCodeActionsForExistingImport(moduleSymbol, context, symbolName, isDefault, isNamespaceImport, existingDeclarations);
}
else {
Debug.fail("Either the symbol or the JSX namespace should be a UMD global if we got here");
}
return getCodeActionForImport(symbol, symbolName, /*isDefault*/ false, /*isNamespaceImport*/ true);
}
const candidateModules = checker.getAmbientModules();
for (const otherSourceFile of allSourceFiles) {
if (otherSourceFile !== sourceFile && isExternalOrCommonJsModule(otherSourceFile)) {
candidateModules.push(otherSourceFile.symbol);
return [getCodeActionForNewImport(context, moduleSymbol, symbolName, isDefault, /*existingModuleSpecifier*/ undefined, isNamespaceImport)];
}
}
for (const moduleSymbol of candidateModules) {
context.cancellationToken.throwIfCancellationRequested();
function getImportDeclarations(moduleSymbol: Symbol, context: ImportCodeFixContext) {
const { checker, sourceFile } = context;
// check the default export
const defaultExport = checker.tryGetMemberInModuleExports("default", moduleSymbol);
if (defaultExport) {
const localSymbol = getLocalSymbolForExportDefault(defaultExport);
if (localSymbol && localSymbol.escapedName === name && checkSymbolHasMeaning(localSymbol, currentTokenMeaning)) {
// check if this symbol is already used
const symbolId = getUniqueSymbolId(localSymbol);
symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, name, /*isNamespaceImport*/ true));
}
}
// "default" is a keyword and not a legal identifier for the import, so we don't expect it here
Debug.assert(name !== "default");
// check exports with the same name
const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExportsAndProperties(name, moduleSymbol);
if (exportSymbolWithIdenticalName && checkSymbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) {
const symbolId = getUniqueSymbolId(exportSymbolWithIdenticalName);
symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, name));
}
}
return symbolIdActionMap.getAllActions();
function getImportDeclarations(moduleSymbol: Symbol) {
const moduleSymbolId = getUniqueSymbolId(moduleSymbol);
const cachedImportDeclarations = context.cachedImportDeclarations || [];
const moduleSymbolId = getUniqueSymbolId(moduleSymbol, checker);
const cached = cachedImportDeclarations[moduleSymbolId];
if (cached) {
@ -216,157 +210,21 @@ namespace ts.codefix {
}
}
function getUniqueSymbolId(symbol: Symbol) {
return getSymbolId(skipAlias(symbol, checker));
function createChangeTracker(context: ImportCodeFixContext) {
return textChanges.ChangeTracker.fromContext(context);
}
function checkSymbolHasMeaning(symbol: Symbol, meaning: SemanticMeaning) {
const declarations = symbol.getDeclarations();
return declarations ? some(symbol.declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning)) : false;
}
function getCodeActionForNewImport(
context: ImportCodeFixContext,
moduleSymbol: Symbol,
symbolName: string,
isDefault: boolean,
moduleSpecifier: string | undefined,
isNamespaceImport: boolean,
): ImportCodeAction {
const { sourceFile, getCanonicalFileName, newLineCharacter, host, compilerOptions } = context;
let lastImportDeclaration: Node;
function getCodeActionForImport(moduleSymbol: Symbol, symbolName: string, isDefault?: boolean, isNamespaceImport?: boolean): ImportCodeAction[] {
const existingDeclarations = getImportDeclarations(moduleSymbol);
if (existingDeclarations.length > 0) {
// With an existing import statement, there are more than one actions the user can do.
return getCodeActionsForExistingImport(existingDeclarations);
}
else {
return [getCodeActionForNewImport()];
}
function getCodeActionsForExistingImport(declarations: (ImportDeclaration | ImportEqualsDeclaration)[]): ImportCodeAction[] {
const actions: ImportCodeAction[] = [];
// It is possible that multiple import statements with the same specifier exist in the file.
// e.g.
//
// import * as ns from "foo";
// import { member1, member2 } from "foo";
//
// member3/**/ <-- cusor here
//
// in this case we should provie 2 actions:
// 1. change "member3" to "ns.member3"
// 2. add "member3" to the second import statement's import list
// and it is up to the user to decide which one fits best.
let namespaceImportDeclaration: ImportDeclaration | ImportEqualsDeclaration;
let namedImportDeclaration: ImportDeclaration;
let existingModuleSpecifier: string;
for (const declaration of declarations) {
if (declaration.kind === SyntaxKind.ImportDeclaration) {
const namedBindings = declaration.importClause && declaration.importClause.namedBindings;
if (namedBindings && namedBindings.kind === SyntaxKind.NamespaceImport) {
// case:
// import * as ns from "foo"
namespaceImportDeclaration = declaration;
}
else {
// cases:
// import default from "foo"
// import { bar } from "foo" or combination with the first one
// import "foo"
namedImportDeclaration = declaration;
}
existingModuleSpecifier = declaration.moduleSpecifier.getText();
}
else {
// case:
// import foo = require("foo")
namespaceImportDeclaration = declaration;
existingModuleSpecifier = getModuleSpecifierFromImportEqualsDeclaration(declaration);
}
}
if (namespaceImportDeclaration) {
actions.push(getCodeActionForNamespaceImport(namespaceImportDeclaration));
}
if (!isNamespaceImport && namedImportDeclaration && namedImportDeclaration.importClause &&
(namedImportDeclaration.importClause.name || namedImportDeclaration.importClause.namedBindings)) {
/**
* If the existing import declaration already has a named import list, just
* insert the identifier into that list.
*/
const fileTextChanges = getTextChangeForImportClause(namedImportDeclaration.importClause);
const moduleSpecifierWithoutQuotes = stripQuotes(namedImportDeclaration.moduleSpecifier.getText());
actions.push(createCodeAction(
Diagnostics.Add_0_to_existing_import_declaration_from_1,
[name, moduleSpecifierWithoutQuotes],
fileTextChanges,
"InsertingIntoExistingImport",
moduleSpecifierWithoutQuotes
));
}
else {
// we need to create a new import statement, but the existing module specifier can be reused.
actions.push(getCodeActionForNewImport(existingModuleSpecifier));
}
return actions;
function getModuleSpecifierFromImportEqualsDeclaration(declaration: ImportEqualsDeclaration) {
if (declaration.moduleReference && declaration.moduleReference.kind === SyntaxKind.ExternalModuleReference) {
return declaration.moduleReference.expression.getText();
}
return declaration.moduleReference.getText();
}
function getTextChangeForImportClause(importClause: ImportClause): FileTextChanges[] {
const importList = <NamedImports>importClause.namedBindings;
const newImportSpecifier = createImportSpecifier(/*propertyName*/ undefined, createIdentifier(name));
// case 1:
// original text: import default from "module"
// change to: import default, { name } from "module"
// case 2:
// original text: import {} from "module"
// change to: import { name } from "module"
if (!importList || importList.elements.length === 0) {
const newImportClause = createImportClause(importClause.name, createNamedImports([newImportSpecifier]));
return createChangeTracker().replaceNode(sourceFile, importClause, newImportClause).getChanges();
}
/**
* If the import list has one import per line, preserve that. Otherwise, insert on same line as last element
* import {
* foo
* } from "./module";
*/
return createChangeTracker().insertNodeInListAfter(
sourceFile,
importList.elements[importList.elements.length - 1],
newImportSpecifier).getChanges();
}
function getCodeActionForNamespaceImport(declaration: ImportDeclaration | ImportEqualsDeclaration): ImportCodeAction {
let namespacePrefix: string;
if (declaration.kind === SyntaxKind.ImportDeclaration) {
namespacePrefix = (<NamespaceImport>declaration.importClause.namedBindings).name.getText();
}
else {
namespacePrefix = declaration.name.getText();
}
namespacePrefix = stripQuotes(namespacePrefix);
/**
* Cases:
* import * as ns from "mod"
* import default, * as ns from "mod"
* import ns = require("mod")
*
* Because there is no import list, we alter the reference to include the
* namespace instead of altering the import declaration. For example, "foo" would
* become "ns.foo"
*/
return createCodeAction(
Diagnostics.Change_0_to_1,
[name, `${namespacePrefix}.${name}`],
createChangeTracker().replaceNode(sourceFile, token, createPropertyAccess(createIdentifier(namespacePrefix), name)).getChanges(),
"CodeChange"
);
}
}
function getCodeActionForNewImport(moduleSpecifier?: string): ImportCodeAction {
if (!lastImportDeclaration) {
// insert after any existing imports
for (let i = sourceFile.statements.length - 1; i >= 0; i--) {
@ -378,22 +236,21 @@ namespace ts.codefix {
}
}
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier || getModuleSpecifierForNewImport());
const changeTracker = createChangeTracker();
const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier || getModuleSpecifierForNewImport(sourceFile, moduleSymbol, compilerOptions, getCanonicalFileName, host));
const changeTracker = createChangeTracker(context);
const importClause = isDefault
? createImportClause(createIdentifier(symbolName), /*namedBindings*/ undefined)
: isNamespaceImport
? createImportClause(/*name*/ undefined, createNamespaceImport(createIdentifier(symbolName)))
: createImportClause(/*name*/ undefined, createNamedImports([createImportSpecifier(/*propertyName*/ undefined, createIdentifier(symbolName))]));
const moduleSpecifierLiteral = createLiteral(moduleSpecifierWithoutQuotes);
moduleSpecifierLiteral.singleQuote = getSingleQuoteStyleFromExistingImports();
moduleSpecifierLiteral.singleQuote = getSingleQuoteStyleFromExistingImports(sourceFile);
const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, importClause, moduleSpecifierLiteral);
if (!lastImportDeclaration) {
changeTracker.insertNodeAt(sourceFile, getSourceFileImportLocation(sourceFile), importDecl, { suffix: `${context.newLineCharacter}${context.newLineCharacter}` });
}
else {
changeTracker.insertNodeAfter(sourceFile, lastImportDeclaration, importDecl, { suffix: context.newLineCharacter });
changeTracker.insertNodeAfter(sourceFile, lastImportDeclaration, importDecl, { suffix: newLineCharacter });
}
// if this file doesn't have any import statements, insert an import statement and then insert a new line
@ -406,6 +263,7 @@ namespace ts.codefix {
"NewImport",
moduleSpecifierWithoutQuotes
);
}
function getSourceFileImportLocation(node: SourceFile) {
// For a source file, it is possible there are detached comments we should not skip
@ -429,7 +287,7 @@ namespace ts.codefix {
return position;
}
function getSingleQuoteStyleFromExistingImports() {
function getSingleQuoteStyleFromExistingImports(sourceFile: SourceFile) {
const firstModuleSpecifier = forEach(sourceFile.statements, node => {
if (isImportDeclaration(node) || isExportDeclaration(node)) {
if (node.moduleSpecifier && isStringLiteral(node.moduleSpecifier)) {
@ -447,32 +305,31 @@ namespace ts.codefix {
}
}
function getModuleSpecifierForNewImport() {
const fileName = sourceFile.fileName;
function getModuleSpecifierForNewImport(sourceFile: SourceFile, moduleSymbol: Symbol, options: CompilerOptions, getCanonicalFileName: (file: string) => string, host: LanguageServiceHost) {
const moduleFileName = moduleSymbol.valueDeclaration.getSourceFile().fileName;
const sourceDirectory = getDirectoryPath(fileName);
const options = context.program.getCompilerOptions();
const sourceDirectory = getDirectoryPath(sourceFile.fileName);
return tryGetModuleNameFromAmbientModule() ||
tryGetModuleNameFromTypeRoots() ||
tryGetModuleNameAsNodeModule() ||
tryGetModuleNameFromBaseUrl() ||
tryGetModuleNameFromRootDirs() ||
removeFileExtension(getRelativePath(moduleFileName, sourceDirectory));
return tryGetModuleNameFromAmbientModule(moduleSymbol) ||
tryGetModuleNameFromTypeRoots(options, host, getCanonicalFileName, moduleFileName) ||
tryGetModuleNameAsNodeModule(options, moduleFileName, host, getCanonicalFileName, sourceDirectory) ||
tryGetModuleNameFromBaseUrl(options, moduleFileName, getCanonicalFileName) ||
tryGetModuleNameFromRootDirs(options, moduleFileName, sourceDirectory, getCanonicalFileName) ||
removeFileExtension(getRelativePath(moduleFileName, sourceDirectory, getCanonicalFileName));
}
function tryGetModuleNameFromAmbientModule(): string {
function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol): string | undefined {
const decl = moduleSymbol.valueDeclaration;
if (isModuleDeclaration(decl) && isStringLiteral(decl.name)) {
return decl.name.text;
}
}
function tryGetModuleNameFromBaseUrl() {
function tryGetModuleNameFromBaseUrl(options: CompilerOptions, moduleFileName: string, getCanonicalFileName: (file: string) => string): string | undefined {
if (!options.baseUrl) {
return undefined;
}
let relativeName = getRelativePathIfInDirectory(moduleFileName, options.baseUrl);
let relativeName = getRelativePathIfInDirectory(moduleFileName, options.baseUrl, getCanonicalFileName);
if (!relativeName) {
return undefined;
}
@ -507,21 +364,24 @@ namespace ts.codefix {
return relativeName;
}
function tryGetModuleNameFromRootDirs() {
function tryGetModuleNameFromRootDirs(options: CompilerOptions, moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string): string | undefined {
if (options.rootDirs) {
const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, options.rootDirs);
const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, options.rootDirs);
const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, options.rootDirs, getCanonicalFileName);
const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, options.rootDirs, getCanonicalFileName);
if (normalizedTargetPath !== undefined) {
const relativePath = normalizedSourcePath !== undefined ? getRelativePath(normalizedTargetPath, normalizedSourcePath) : normalizedTargetPath;
const relativePath = normalizedSourcePath !== undefined ? getRelativePath(normalizedTargetPath, normalizedSourcePath, getCanonicalFileName) : normalizedTargetPath;
return removeFileExtension(relativePath);
}
}
return undefined;
}
function tryGetModuleNameFromTypeRoots() {
const typeRoots = getEffectiveTypeRoots(options, context.host);
if (typeRoots) {
function tryGetModuleNameFromTypeRoots(options: CompilerOptions, host: LanguageServiceHost, getCanonicalFileName: (file: string) => string, moduleFileName: string): string | undefined {
const typeRoots = getEffectiveTypeRoots(options, host);
if (!typeRoots) {
return undefined;
}
const normalizedTypeRoots = map(typeRoots, typeRoot => toPath(typeRoot, /*basePath*/ undefined, getCanonicalFileName));
for (const typeRoot of normalizedTypeRoots) {
if (startsWith(moduleFileName, typeRoot)) {
@ -530,9 +390,14 @@ namespace ts.codefix {
}
}
}
}
function tryGetModuleNameAsNodeModule() {
function tryGetModuleNameAsNodeModule(
options: CompilerOptions,
moduleFileName: string,
host: LanguageServiceHost,
getCanonicalFileName: (file: string) => string,
sourceDirectory: string,
): string | undefined {
if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs) {
// nothing to do here
return undefined;
@ -557,8 +422,8 @@ namespace ts.codefix {
// If the file is the main module, it can be imported by the package name
const packageRootPath = path.substring(0, parts.packageRootIndex);
const packageJsonPath = combinePaths(packageRootPath, "package.json");
if (context.host.fileExists(packageJsonPath)) {
const packageJsonContent = JSON.parse(context.host.readFile(packageJsonPath));
if (host.fileExists(packageJsonPath)) {
const packageJsonContent = JSON.parse(host.readFile(packageJsonPath));
if (packageJsonContent) {
const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main;
if (mainFileRelative) {
@ -588,8 +453,7 @@ namespace ts.codefix {
return path.substring(parts.topLevelPackageNameIndex + 1);
}
else {
return getRelativePath(path, sourceDirectory);
}
return getRelativePath(path, sourceDirectory, getCanonicalFileName);
}
}
}
@ -652,9 +516,9 @@ namespace ts.codefix {
return state > States.NodeModules ? { topLevelNodeModulesIndex, topLevelPackageNameIndex, packageRootIndex, fileNameIndex } : undefined;
}
function getPathRelativeToRootDirs(path: string, rootDirs: string[]) {
function getPathRelativeToRootDirs(path: string, rootDirs: string[], getCanonicalFileName: (fileName: string) => string) {
for (const rootDir of rootDirs) {
const relativeName = getRelativePathIfInDirectory(path, rootDir);
const relativeName = getRelativePathIfInDirectory(path, rootDir, getCanonicalFileName);
if (relativeName !== undefined) {
return relativeName;
}
@ -670,35 +534,213 @@ namespace ts.codefix {
return fileName;
}
function getRelativePathIfInDirectory(path: string, directoryPath: string) {
function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: (fileName: string) => string) {
const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
return isRootedDiskPath(relativePath) || startsWith(relativePath, "..") ? undefined : relativePath;
}
function getRelativePath(path: string, directoryPath: string) {
function getRelativePath(path: string, directoryPath: string, getCanonicalFileName: (fileName: string) => string) {
const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
return !pathIsRelative(relativePath) ? "./" + relativePath : relativePath;
}
function getCodeActionsForExistingImport(moduleSymbol: Symbol, context: ImportCodeFixContext, symbolName: string, isDefault: boolean, isNamespaceImport: boolean, declarations: AnyImportSyntax[]): ImportCodeAction[] {
const { symbolName: name, sourceFile, symbolToken } = context;
const { namespaceImportDeclaration, namedImportDeclaration, existingModuleSpecifier } = getDeclarations(declarations);
const actions: ImportCodeAction[] = [];
if (symbolToken && namespaceImportDeclaration) {
actions.push(getCodeActionForNamespaceImport(namespaceImportDeclaration));
}
if (!isNamespaceImport && namedImportDeclaration && namedImportDeclaration.importClause &&
(namedImportDeclaration.importClause.name || namedImportDeclaration.importClause.namedBindings)) {
/**
* If the existing import declaration already has a named import list, just
* insert the identifier into that list.
*/
const fileTextChanges = getTextChangeForImportClause(namedImportDeclaration.importClause);
const moduleSpecifierWithoutQuotes = stripQuotes(namedImportDeclaration.moduleSpecifier.getText());
actions.push(createCodeAction(
Diagnostics.Add_0_to_existing_import_declaration_from_1,
[name, moduleSpecifierWithoutQuotes],
fileTextChanges,
"InsertingIntoExistingImport",
moduleSpecifierWithoutQuotes
));
}
else {
// we need to create a new import statement, but the existing module specifier can be reused.
actions.push(getCodeActionForNewImport(context, moduleSymbol, symbolName, isDefault, existingModuleSpecifier, isNamespaceImport));
}
return actions;
function getTextChangeForImportClause(importClause: ImportClause): FileTextChanges[] {
const importList = <NamedImports>importClause.namedBindings;
const newImportSpecifier = createImportSpecifier(/*propertyName*/ undefined, createIdentifier(name));
// case 1:
// original text: import default from "module"
// change to: import default, { name } from "module"
// case 2:
// original text: import {} from "module"
// change to: import { name } from "module"
if (!importList || importList.elements.length === 0) {
const newImportClause = createImportClause(importClause.name, createNamedImports([newImportSpecifier]));
return createChangeTracker(context).replaceNode(sourceFile, importClause, newImportClause).getChanges();
}
function createChangeTracker() {
return textChanges.ChangeTracker.fromContext(context);
/**
* If the import list has one import per line, preserve that. Otherwise, insert on same line as last element
* import {
* foo
* } from "./module";
*/
return createChangeTracker(context).insertNodeInListAfter(
sourceFile,
importList.elements[importList.elements.length - 1],
newImportSpecifier).getChanges();
}
function createCodeAction(
description: DiagnosticMessage,
diagnosticArgs: string[],
changes: FileTextChanges[],
kind: ImportCodeActionKind,
moduleSpecifier?: string): ImportCodeAction {
return {
description: formatMessage.apply(undefined, [undefined, description].concat(<any[]>diagnosticArgs)),
changes,
kind,
moduleSpecifier
};
function getCodeActionForNamespaceImport(declaration: ImportDeclaration | ImportEqualsDeclaration): ImportCodeAction {
const namespacePrefix = stripQuotes(declaration.kind === SyntaxKind.ImportDeclaration
? (<NamespaceImport>declaration.importClause.namedBindings).name.getText()
: declaration.name.getText());
/**
* Cases:
* import * as ns from "mod"
* import default, * as ns from "mod"
* import ns = require("mod")
*
* Because there is no import list, we alter the reference to include the
* namespace instead of altering the import declaration. For example, "foo" would
* become "ns.foo"
*/
return createCodeAction(
Diagnostics.Change_0_to_1,
[name, `${namespacePrefix}.${name}`],
createChangeTracker(context).replaceNode(sourceFile, symbolToken, createPropertyAccess(createIdentifier(namespacePrefix), name)).getChanges(),
"CodeChange"
);
}
}
function getDeclarations(declarations: AnyImportSyntax[]) {
// It is possible that multiple import statements with the same specifier exist in the file.
// e.g.
//
// import * as ns from "foo";
// import { member1, member2 } from "foo";
//
// member3/**/ <-- cusor here
//
// in this case we should provie 2 actions:
// 1. change "member3" to "ns.member3"
// 2. add "member3" to the second import statement's import list
// and it is up to the user to decide which one fits best.
let namespaceImportDeclaration: ImportDeclaration | ImportEqualsDeclaration;
let namedImportDeclaration: ImportDeclaration;
let existingModuleSpecifier: string;
for (const declaration of declarations) {
if (declaration.kind === SyntaxKind.ImportDeclaration) {
const namedBindings = declaration.importClause && declaration.importClause.namedBindings;
if (namedBindings && namedBindings.kind === SyntaxKind.NamespaceImport) {
// case:
// import * as ns from "foo"
namespaceImportDeclaration = declaration;
}
else {
// cases:
// import default from "foo"
// import { bar } from "foo" or combination with the first one
// import "foo"
namedImportDeclaration = declaration;
}
existingModuleSpecifier = declaration.moduleSpecifier.getText();
}
else {
// case:
// import foo = require("foo")
namespaceImportDeclaration = declaration;
existingModuleSpecifier = getModuleSpecifierFromImportEqualsDeclaration(declaration);
}
}
return { namespaceImportDeclaration, namedImportDeclaration, existingModuleSpecifier };
function getModuleSpecifierFromImportEqualsDeclaration({ moduleReference }: ImportEqualsDeclaration): string {
return (moduleReference.kind === SyntaxKind.ExternalModuleReference ? moduleReference.expression : moduleReference).getText();
}
}
function getImportCodeActions(context: CodeFixContext): ImportCodeAction[] {
const sourceFile = context.sourceFile;
const allSourceFiles = context.program.getSourceFiles();
const importFixContext = convertToImportCodeFixContext(context);
const checker = importFixContext.checker;
const token = importFixContext.symbolToken;
const symbolIdActionMap = new ImportCodeActionMap();
const currentTokenMeaning = getMeaningFromLocation(token);
const name = importFixContext.symbolName;
if (context.errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code) {
const umdSymbol = checker.getSymbolAtLocation(token);
let symbol: ts.Symbol;
let symbolName: string;
if (umdSymbol.flags & ts.SymbolFlags.Alias) {
symbol = checker.getAliasedSymbol(umdSymbol);
symbolName = name;
}
else if (isJsxOpeningLikeElement(token.parent) && token.parent.tagName === token) {
// The error wasn't for the symbolAtLocation, it was for the JSX tag itself, which needs access to e.g. `React`.
symbol = checker.getAliasedSymbol(checker.resolveName(checker.getJsxNamespace(), token.parent.tagName, SymbolFlags.Value));
symbolName = symbol.name;
}
else {
Debug.fail("Either the symbol or the JSX namespace should be a UMD global if we got here");
}
return getCodeActionForImport(symbol, importFixContext, symbolName, /*isDefault*/ false, /*isNamespaceImport*/ true);
}
const candidateModules = checker.getAmbientModules();
for (const otherSourceFile of allSourceFiles) {
if (otherSourceFile !== sourceFile && isExternalOrCommonJsModule(otherSourceFile)) {
candidateModules.push(otherSourceFile.symbol);
}
}
for (const moduleSymbol of candidateModules) {
context.cancellationToken.throwIfCancellationRequested();
// check the default export
const defaultExport = checker.tryGetMemberInModuleExports("default", moduleSymbol);
if (defaultExport) {
const localSymbol = getLocalSymbolForExportDefault(defaultExport);
if (localSymbol && localSymbol.escapedName === name && checkSymbolHasMeaning(localSymbol, currentTokenMeaning)) {
// check if this symbol is already used
const symbolId = getUniqueSymbolId(localSymbol, checker);
symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, importFixContext, name, /*isNamespaceImport*/ true));
}
}
// "default" is a keyword and not a legal identifier for the import, so we don't expect it here
Debug.assert(name !== "default");
// check exports with the same name
const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExportsAndProperties(name, moduleSymbol);
if (exportSymbolWithIdenticalName && checkSymbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) {
const symbolId = getUniqueSymbolId(exportSymbolWithIdenticalName, checker);
symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, importFixContext, name));
}
}
return symbolIdActionMap.getAllActions();
function checkSymbolHasMeaning(symbol: Symbol, meaning: SemanticMeaning) {
const declarations = symbol.getDeclarations();
return declarations ? some(symbol.declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning)) : false;
}
}
}

View file

@ -4,13 +4,23 @@
namespace ts.Completions {
export type Log = (message: string) => void;
export type SymbolOriginInfo = { moduleSymbol: Symbol, isDefaultExport?: boolean };
const enum KeywordCompletionFilters {
None,
ClassElementKeywords, // Keywords at class keyword
ConstructorParameterKeywords, // Keywords at constructor parameter
}
export function getCompletionsAtPosition(host: LanguageServiceHost, typeChecker: TypeChecker, log: Log, compilerOptions: CompilerOptions, sourceFile: SourceFile, position: number): CompletionInfo | undefined {
export function getCompletionsAtPosition(
host: LanguageServiceHost,
typeChecker: TypeChecker,
log: Log,
compilerOptions: CompilerOptions,
sourceFile: SourceFile,
position: number,
allSourceFiles: ReadonlyArray<SourceFile>,
): CompletionInfo | undefined {
if (isInReferenceComment(sourceFile, position)) {
return PathCompletions.getTripleSlashReferenceCompletion(sourceFile, position, compilerOptions, host);
}
@ -19,12 +29,12 @@ namespace ts.Completions {
return getStringLiteralCompletionEntries(sourceFile, position, typeChecker, compilerOptions, host, log);
}
const completionData = getCompletionData(typeChecker, log, sourceFile, position);
const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles);
if (!completionData) {
return undefined;
}
const { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, request, keywordFilters } = completionData;
const { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, request, keywordFilters, symbolToOriginInfoMap } = completionData;
if (sourceFile.languageVariant === LanguageVariant.JSX &&
location && location.parent && location.parent.kind === SyntaxKind.JsxClosingElement) {
@ -56,7 +66,7 @@ namespace ts.Completions {
const entries: CompletionEntry[] = [];
if (isSourceFileJavaScript(sourceFile)) {
const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, allowStringLiteral);
const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, allowStringLiteral, symbolToOriginInfoMap);
getJavaScriptCompletionEntries(sourceFile, location.pos, uniqueNames, compilerOptions.target, entries);
}
else {
@ -64,7 +74,7 @@ namespace ts.Completions {
return undefined;
}
getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, allowStringLiteral);
getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, allowStringLiteral, symbolToOriginInfoMap);
}
// TODO add filter for keyword based on type/value/namespace and also location
@ -134,7 +144,17 @@ namespace ts.Completions {
};
}
function getCompletionEntriesFromSymbols(symbols: Symbol[], entries: Push<CompletionEntry>, location: Node, performCharacterChecks: boolean, typeChecker: TypeChecker, target: ScriptTarget, log: Log, allowStringLiteral: boolean): Map<true> {
function getCompletionEntriesFromSymbols(
symbols: Symbol[],
entries: Push<CompletionEntry>,
location: Node,
performCharacterChecks: boolean,
typeChecker: TypeChecker,
target: ScriptTarget,
log: Log,
allowStringLiteral: boolean,
symbolToOriginInfoMap?: Map<SymbolOriginInfo>,
): Map<true> {
const start = timestamp();
const uniqueNames = createMap<true>();
if (symbols) {
@ -143,6 +163,9 @@ namespace ts.Completions {
if (entry) {
const id = entry.name;
if (!uniqueNames.has(id)) {
if (symbolToOriginInfoMap && symbolToOriginInfoMap.has(getUniqueSymbolIdAsString(symbol, typeChecker))) {
entry.hasAction = true;
}
entries.push(entry);
uniqueNames.set(id, true);
}
@ -298,11 +321,22 @@ namespace ts.Completions {
}
}
export function getCompletionEntryDetails(typeChecker: TypeChecker, log: (message: string) => void, compilerOptions: CompilerOptions, sourceFile: SourceFile, position: number, entryName: string): CompletionEntryDetails {
export function getCompletionEntryDetails(
typeChecker: TypeChecker,
log: (message: string) => void,
compilerOptions: CompilerOptions,
sourceFile: SourceFile,
position: number,
entryName: string,
allSourceFiles: ReadonlyArray<SourceFile>,
host?: LanguageServiceHost,
rulesProvider?: formatting.RulesProvider,
): CompletionEntryDetails {
// Compute all the completion symbols again.
const completionData = getCompletionData(typeChecker, log, sourceFile, position);
const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles);
if (completionData) {
const { symbols, location, allowStringLiteral } = completionData;
const { symbols, location, allowStringLiteral, symbolToOriginInfoMap } = completionData;
// Find the symbol with the matching entry name.
// We don't need to perform character checks here because we're only comparing the
@ -311,6 +345,26 @@ namespace ts.Completions {
const symbol = forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral) === entryName ? s : undefined);
if (symbol) {
let codeActions: CodeAction[];
if (host && rulesProvider) {
const symbolOriginInfo = symbolToOriginInfoMap.get(getUniqueSymbolIdAsString(symbol, typeChecker));
if (symbolOriginInfo) {
const useCaseSensitiveFileNames = host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : false;
const context: codefix.ImportCodeFixContext = {
host,
checker: typeChecker,
newLineCharacter: host.getNewLine(),
compilerOptions,
sourceFile,
rulesProvider,
symbolName: symbol.name,
getCanonicalFileName: createGetCanonicalFileName(useCaseSensitiveFileNames)
};
codeActions = codefix.getCodeActionForImport(/*moduleSymbol*/ symbolOriginInfo.moduleSymbol, context, context.symbolName, /*isDefault*/ symbolOriginInfo.isDefaultExport);
}
}
const { displayParts, documentation, symbolKind, tags } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, location, location, SemanticMeaning.All);
return {
name: entryName,
@ -318,7 +372,8 @@ namespace ts.Completions {
kind: symbolKind,
displayParts,
documentation,
tags
tags,
codeActions
};
}
}
@ -335,16 +390,25 @@ namespace ts.Completions {
kindModifiers: ScriptElementKindModifier.none,
displayParts: [displayPart(entryName, SymbolDisplayPartKind.keyword)],
documentation: undefined,
tags: undefined
tags: undefined,
codeActions: undefined
};
}
return undefined;
}
export function getCompletionEntrySymbol(typeChecker: TypeChecker, log: (message: string) => void, compilerOptions: CompilerOptions, sourceFile: SourceFile, position: number, entryName: string): Symbol | undefined {
export function getCompletionEntrySymbol(
typeChecker: TypeChecker,
log: (message: string) => void,
compilerOptions: CompilerOptions,
sourceFile: SourceFile,
position: number,
entryName: string,
allSourceFiles: ReadonlyArray<SourceFile>,
): Symbol | undefined {
// Compute all the completion symbols again.
const completionData = getCompletionData(typeChecker, log, sourceFile, position);
const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles);
if (!completionData) {
return undefined;
}
@ -366,10 +430,17 @@ namespace ts.Completions {
isRightOfDot: boolean;
request?: Request;
keywordFilters: KeywordCompletionFilters;
symbolToOriginInfoMap: Map<SymbolOriginInfo>;
}
type Request = { kind: "JsDocTagName" } | { kind: "JsDocTag" } | { kind: "JsDocParameterName", tag: JSDocParameterTag };
function getCompletionData(typeChecker: TypeChecker, log: (message: string) => void, sourceFile: SourceFile, position: number): CompletionData | undefined {
function getCompletionData(
typeChecker: TypeChecker,
log: (message: string) => void,
sourceFile: SourceFile,
position: number,
allSourceFiles: ReadonlyArray<SourceFile>,
): CompletionData | undefined {
const isJavaScriptFile = isSourceFileJavaScript(sourceFile);
let request: Request | undefined;
@ -441,7 +512,18 @@ namespace ts.Completions {
}
if (request) {
return { symbols: undefined, isGlobalCompletion: false, isMemberCompletion: false, allowStringLiteral: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, request, keywordFilters: KeywordCompletionFilters.None };
return {
symbols: undefined,
isGlobalCompletion: false,
isMemberCompletion: false,
allowStringLiteral: false,
isNewIdentifierLocation: false,
location: undefined,
isRightOfDot: false,
request,
keywordFilters: KeywordCompletionFilters.None,
symbolToOriginInfoMap: undefined,
};
}
if (!insideJsDocTagTypeExpression) {
@ -523,7 +605,6 @@ namespace ts.Completions {
break;
}
// falls through
case SyntaxKind.JsxSelfClosingElement:
case SyntaxKind.JsxElement:
case SyntaxKind.JsxOpeningElement:
@ -543,6 +624,7 @@ namespace ts.Completions {
let isNewIdentifierLocation: boolean;
let keywordFilters = KeywordCompletionFilters.None;
let symbols: Symbol[] = [];
const symbolToOriginInfoMap = createMap<SymbolOriginInfo>();
if (isRightOfDot) {
getTypeScriptMemberSymbols();
@ -579,7 +661,7 @@ namespace ts.Completions {
log("getCompletionData: Semantic work: " + (timestamp() - semanticStart));
return { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, keywordFilters };
return { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, keywordFilters, symbolToOriginInfoMap };
type JSDocTagWithTypeExpression = JSDocAugmentsTag | JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
@ -753,7 +835,10 @@ namespace ts.Completions {
}
const symbolMeanings = SymbolFlags.Type | SymbolFlags.Value | SymbolFlags.Namespace | SymbolFlags.Alias;
symbols = filterGlobalCompletion(typeChecker.getSymbolsInScope(scopeNode, symbolMeanings));
symbols = typeChecker.getSymbolsInScope(scopeNode, symbolMeanings);
symbols.push(...getSymbolsFromOtherSourceFileExports(symbols, previousToken === undefined ? "" : previousToken.getText()));
symbols = filterGlobalCompletion(symbols);
return true;
}
@ -833,6 +918,36 @@ namespace ts.Completions {
}
}
function getSymbolsFromOtherSourceFileExports(knownSymbols: Symbol[], tokenText: string): Symbol[] {
const otherSourceFileExports: Symbol[] = [];
const tokenTextLowerCase = tokenText.toLowerCase();
const symbolIdMap = arrayToMap(knownSymbols, s => getUniqueSymbolIdAsString(s, typeChecker));
eachOtherModuleSymbol(allSourceFiles, sourceFile, typeChecker, moduleSymbol => {
// check the default export
const defaultExport = typeChecker.tryGetMemberInModuleExports("default", moduleSymbol);
if (defaultExport) {
const localSymbol = getLocalSymbolForExportDefault(defaultExport);
if (localSymbol && !symbolIdMap.has(getUniqueSymbolIdAsString(localSymbol, typeChecker)) && startsWith(localSymbol.name.toLowerCase(), tokenTextLowerCase)) {
otherSourceFileExports.push(localSymbol);
symbolToOriginInfoMap.set(getUniqueSymbolIdAsString(localSymbol, typeChecker), { moduleSymbol, isDefaultExport: true });
}
}
// check exports with the same name
const allExportedSymbols = typeChecker.getExportsOfModule(moduleSymbol);
if (allExportedSymbols) {
for (const exportedSymbol of allExportedSymbols) {
if (exportedSymbol.name && !symbolIdMap.has(getUniqueSymbolIdAsString(exportedSymbol, typeChecker)) && startsWith(exportedSymbol.name.toLowerCase(), tokenTextLowerCase)) {
otherSourceFileExports.push(exportedSymbol);
symbolToOriginInfoMap.set(getUniqueSymbolIdAsString(exportedSymbol, typeChecker), { moduleSymbol });
}
}
}
});
return otherSourceFileExports;
}
/**
* Finds the first node that "embraces" the position, so that one may
* accurately aggregate locals from the closest containing scope.
@ -1782,4 +1897,21 @@ namespace ts.Completions {
// If there are no property-only types, just provide completions for every type as usual.
return checker.getAllPossiblePropertiesOfTypes(filteredTypes);
}
function eachOtherModuleSymbol(
sourceFiles: ReadonlyArray<SourceFile>,
currentSourceFile: SourceFile,
typeChecker: TypeChecker,
cb: (symbol: Symbol) => void,
) {
for (const a of typeChecker.getAmbientModules()) {
cb(a);
}
for (const otherSourceFile of sourceFiles) {
if (otherSourceFile !== currentSourceFile && isExternalOrCommonJsModule(otherSourceFile)) {
cb(otherSourceFile.symbol);
}
}
}
}

View file

@ -1377,17 +1377,19 @@ namespace ts {
function getCompletionsAtPosition(fileName: string, position: number): CompletionInfo {
synchronizeHostData();
return Completions.getCompletionsAtPosition(host, program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position);
return Completions.getCompletionsAtPosition(host, program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position, program.getSourceFiles());
}
function getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails {
function getCompletionEntryDetails(fileName: string, position: number, entryName: string, formattingOptions?: FormatCodeSettings): CompletionEntryDetails {
synchronizeHostData();
return Completions.getCompletionEntryDetails(program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position, entryName);
const ruleProvider = formattingOptions ? getRuleProvider(formattingOptions) : undefined;
return Completions.getCompletionEntryDetails(
program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position, entryName, program.getSourceFiles(), host, ruleProvider);
}
function getCompletionEntrySymbol(fileName: string, position: number, entryName: string): Symbol {
synchronizeHostData();
return Completions.getCompletionEntrySymbol(program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position, entryName);
return Completions.getCompletionEntrySymbol(program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position, entryName, program.getSourceFiles());
}
function getQuickInfoAtPosition(fileName: string, position: number): QuickInfo {

View file

@ -190,7 +190,8 @@ namespace ts.textChanges {
private changes: Change[] = [];
private readonly newLineCharacter: string;
public static fromContext(context: RefactorContext | CodeFixContext) {
//todo: don't include ImportCodeFixContext
public static fromContext(context: RefactorContext | CodeFixContext | ts.codefix.ImportCodeFixContext) {
return new ChangeTracker(context.newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed, context.rulesProvider);
}

View file

@ -228,7 +228,8 @@ namespace ts {
getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications;
getCompletionsAtPosition(fileName: string, position: number): CompletionInfo;
getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails;
//We should probably not take formatting options here.
getCompletionEntryDetails(fileName: string, position: number, entryName: string, formattingOptions?: FormatCodeSettings): CompletionEntryDetails;
getCompletionEntrySymbol(fileName: string, position: number, entryName: string): Symbol;
getQuickInfoAtPosition(fileName: string, position: number): QuickInfo;
@ -665,6 +666,7 @@ namespace ts {
* be used in that case
*/
replacementSpan?: TextSpan;
hasAction?: true; //why do we need this?
}
export interface CompletionEntryDetails {
@ -674,6 +676,7 @@ namespace ts {
displayParts: SymbolDisplayPart[];
documentation: SymbolDisplayPart[];
tags: JSDocTagInfo[];
codeActions?: CodeAction[];
}
export interface OutliningSpan {

View file

@ -1313,10 +1313,21 @@ namespace ts {
export function getScriptKind(fileName: string, host?: LanguageServiceHost): ScriptKind {
// First check to see if the script kind was specified by the host. Chances are the host
// may override the default script kind for the file extension.
// may override the default script kind for the file extensison.
return ensureScriptKind(fileName, host && host.getScriptKind && host.getScriptKind(fileName));
}
export function getUniqueSymbolIdAsString(symbol: Symbol, typeChecker: TypeChecker) {
return getUniqueSymbolId(symbol, typeChecker) + "";
}
export function getUniqueSymbolId(symbol: Symbol, typeChecker: TypeChecker) {
if (symbol.flags & SymbolFlags.Alias) {
return getSymbolId(typeChecker.getAliasedSymbol(symbol));
}
return getSymbolId(symbol);
}
export function getFirstNonSpaceCharacterPosition(text: string, position: number) {
while (isWhiteSpaceLike(text.charCodeAt(position))) {
position += 1;