Compare commits

...

39 commits

Author SHA1 Message Date
Andy Hanson 30ebde4112 Merge branch 'master' into exportsincompletionlist3 2017-10-10 10:26:04 -07:00
Andy Hanson 64e30dbc25 Test, fix bugs, refactor 2017-10-10 10:11:22 -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
34 changed files with 1080 additions and 618 deletions

View file

@ -296,6 +296,7 @@ namespace ts {
const jsObjectLiteralIndexInfo = createIndexInfo(anyType, /*isReadonly*/ false);
const globals = createSymbolTable();
let ambientModulesCache: Symbol[] | undefined;
/**
* List of every ambient module with a "*" wildcard.
* Unlike other ambient modules, these can't be stored in `globals` because symbol tables only deal with exact matches.
@ -25521,13 +25522,16 @@ namespace ts {
}
function getAmbientModules(): Symbol[] {
const result: Symbol[] = [];
globals.forEach((global, sym) => {
if (ambientModuleSymbolRegex.test(unescapeLeadingUnderscores(sym))) {
result.push(global);
}
});
return result;
if (!ambientModulesCache) {
ambientModulesCache = [];
globals.forEach((global, sym) => {
// No need to `unescapeLeadingUnderscores`, an escaped symbol is never an ambient module.
if (ambientModuleSymbolRegex.test(sym as string)) {
ambientModulesCache.push(global);
}
});
}
return ambientModulesCache;
}
function checkGrammarImportCallExpression(node: ImportCall): boolean {

View file

@ -1183,8 +1183,8 @@ namespace ts {
}
}
function isDoubleQuotedString(node: Node) {
return node.kind === SyntaxKind.StringLiteral && getSourceTextOfNodeFromSourceFile(sourceFile, node).charCodeAt(0) === CharacterCodes.doubleQuote;
function isDoubleQuotedString(node: Node): boolean {
return isStringLiteral(node) && isStringDoubleQuoted(node, sourceFile);
}
}

View file

@ -191,6 +191,18 @@ namespace ts {
}
return undefined;
}
/** Like `forEach`, but suitable for use with numbers and strings (which may be falsy). */
export function firstDefined<T, U>(array: ReadonlyArray<T> | undefined, callback: (element: T, index: number) => U | undefined): U | undefined {
for (let i = 0; i < array.length; i++) {
const result = callback(array[i], i);
if (result !== undefined) {
return result;
}
}
return undefined;
}
/**
* Iterates through the parent chain of a node and performs the callback on each parent until the callback
* returns a truthy value, then returns that value.
@ -259,6 +271,16 @@ namespace ts {
return undefined;
}
export function findLast<T>(array: ReadonlyArray<T>, predicate: (element: T, index: number) => boolean): T | undefined {
for (let i = array.length - 1; i >= 0; i--) {
const value = array[i];
if (predicate(value, i)) {
return value;
}
}
return undefined;
}
/** Works like Array.prototype.findIndex, returning `-1` if no element satisfying the predicate is found. */
export function findIndex<T>(array: ReadonlyArray<T>, predicate: (element: T, index: number) => boolean): number {
for (let i = 0; i < array.length; i++) {
@ -1160,6 +1182,14 @@ namespace ts {
return result;
}
export function arrayToNumericMap<T>(array: ReadonlyArray<T>, makeKey: (value: T) => number): T[] {
const result: T[] = [];
for (const value of array) {
result[makeKey(value)] = value;
}
return result;
}
/**
* Creates a set from the elements of an array.
*

View file

@ -3657,7 +3657,7 @@
"category": "Error",
"code": 90010
},
"Import {0} from {1}.": {
"Import '{0}' from \"{1}\".": {
"category": "Message",
"code": 90013
},
@ -3665,7 +3665,7 @@
"category": "Message",
"code": 90014
},
"Add {0} to existing import declaration from {1}.": {
"Add '{0}' to existing import declaration from \"{1}\".": {
"category": "Message",
"code": 90015
},

View file

@ -124,7 +124,11 @@ namespace ts {
}
}
export function getEffectiveTypeRoots(options: CompilerOptions, host: { directoryExists?: (directoryName: string) => boolean, getCurrentDirectory?: () => string }): string[] | undefined {
export interface GetEffectiveTypeRootsHost {
directoryExists?(directoryName: string): boolean;
getCurrentDirectory?(): string;
}
export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined {
if (options.typeRoots) {
return options.typeRoots;
}

View file

@ -1055,6 +1055,7 @@ namespace ts {
export interface StringLiteral extends LiteralExpression {
kind: SyntaxKind.StringLiteral;
/* @internal */ textSourceNode?: Identifier | StringLiteral | NumericLiteral; // Allows a StringLiteral to get its text from another node (used by transforms).
/** Note: this is only set when synthesizing a node, not during parsing. */
/* @internal */ singleQuote?: boolean;
}

View file

@ -520,6 +520,17 @@ namespace ts {
}
}
/* @internal */
export function isAnyImportSyntax(node: Node): node is AnyImportSyntax {
switch (node.kind) {
case SyntaxKind.ImportDeclaration:
case SyntaxKind.ImportEqualsDeclaration:
return true;
default:
return false;
}
}
// Gets the nearest enclosing block scope container that has the provided node
// as a descendant, that is not the provided node.
export function getEnclosingBlockScopeContainer(node: Node): Node {
@ -1375,6 +1386,10 @@ namespace ts {
return charCode === CharacterCodes.singleQuote || charCode === CharacterCodes.doubleQuote;
}
export function isStringDoubleQuoted(string: StringLiteral, sourceFile: SourceFile): boolean {
return getSourceTextOfNodeFromSourceFile(sourceFile, string).charCodeAt(0) === CharacterCodes.doubleQuote;
}
/**
* Returns true if the node is a variable declaration whose initializer is a function expression.
* This function does not test if the node is in a JavaScript file or not.

View file

@ -783,10 +783,10 @@ namespace FourSlash {
});
}
public verifyCompletionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number) {
public verifyCompletionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number, hasAction?: boolean) {
const completions = this.getCompletionListAtCaret();
if (completions) {
this.assertItemInCompletionList(completions.entries, symbol, text, documentation, kind, spanIndex);
this.assertItemInCompletionList(completions.entries, symbol, text, documentation, kind, spanIndex, hasAction);
}
else {
this.raiseError(`No completions at position '${this.currentCaretPosition}' when looking for '${symbol}'.`);
@ -1127,7 +1127,7 @@ Actual: ${stringify(fullActual)}`);
}
private getCompletionEntryDetails(entryName: string) {
return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName);
return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings);
}
private getReferencesAtCaret() {
@ -2289,6 +2289,29 @@ Actual: ${stringify(fullActual)}`);
this.applyCodeActions(this.getCodeFixActions(fileName, errorCode), index);
}
public applyCodeActionFromCompletion(markerName: string, options: FourSlashInterface.VerifyCompletionActionOptions) {
this.goToMarker(markerName);
const actualCompletion = this.getCompletionListAtCaret().entries.find(e => e.name === options.name);
if (!actualCompletion.hasAction) {
this.raiseError(`Completion for ${options.name} does not have an associated action.`);
}
const details = this.getCompletionEntryDetails(options.name);
if (details.codeActions.length !== 1) {
this.raiseError(`Expected one code action, got ${details.codeActions.length}`);
}
if (details.codeActions[0].description !== options.description) {
this.raiseError(`Expected description to be:\n${options.description}\ngot:\n${details.codeActions[0].description}`);
}
this.applyCodeActions(details.codeActions);
this.verifyNewContent(options);
}
public verifyRangeIs(expectedText: string, includeWhiteSpace?: boolean) {
const ranges = this.getRanges();
if (ranges.length !== 1) {
@ -2360,6 +2383,10 @@ Actual: ${stringify(fullActual)}`);
this.applyEdits(change.fileName, change.textChanges, /*isFormattingEdit*/ false);
}
this.verifyNewContent(options);
}
private verifyNewContent(options: FourSlashInterface.NewContentOptions) {
if (options.newFileContent) {
assert(!options.newRangeContent);
this.verifyCurrentFileContent(options.newFileContent);
@ -2933,7 +2960,15 @@ Actual: ${stringify(fullActual)}`);
return text.substring(startPos, endPos);
}
private assertItemInCompletionList(items: ts.CompletionEntry[], name: string, text?: string, documentation?: string, kind?: string, spanIndex?: number) {
private assertItemInCompletionList(
items: ts.CompletionEntry[],
name: string,
text: string | undefined,
documentation: string | undefined,
kind: string | undefined,
spanIndex: number | undefined,
hasAction: boolean | undefined,
) {
for (const item of items) {
if (item.name === name) {
if (documentation !== undefined || text !== undefined) {
@ -2956,6 +2991,8 @@ Actual: ${stringify(fullActual)}`);
assert.isTrue(TestState.textSpansEqual(span, item.replacementSpan), this.assertionMessageAtLastKnownMarker(stringify(span) + " does not equal " + stringify(item.replacementSpan) + " replacement span for " + name));
}
assert.equal(item.hasAction, hasAction);
return;
}
}
@ -3669,12 +3706,12 @@ namespace FourSlashInterface {
// Verifies the completion list contains the specified symbol. The
// completion list is brought up if necessary
public completionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number) {
public completionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number, hasAction?: boolean) {
if (this.negative) {
this.state.verifyCompletionListDoesNotContain(symbol, text, documentation, kind, spanIndex);
}
else {
this.state.verifyCompletionListContains(symbol, text, documentation, kind, spanIndex);
this.state.verifyCompletionListContains(symbol, text, documentation, kind, spanIndex, hasAction);
}
}
@ -3999,6 +4036,10 @@ namespace FourSlashInterface {
this.state.getAndApplyCodeActions(errorCode, index);
}
public applyCodeActionFromCompletion(markerName: string, options: VerifyCompletionActionOptions): void {
this.state.applyCodeActionFromCompletion(markerName, options);
}
public importFixAtPosition(expectedTextArray: string[], errorCode?: number): void {
this.state.verifyImportFixAtPosition(expectedTextArray, errorCode);
}
@ -4396,12 +4437,20 @@ namespace FourSlashInterface {
isNewIdentifierLocation?: boolean;
}
export interface VerifyCodeFixOptions {
description: string;
// One of these should be defined.
export interface NewContentOptions {
// Exactly one of these should be defined.
newFileContent?: string;
newRangeContent?: string;
}
export interface VerifyCodeFixOptions extends NewContentOptions {
description: string;
errorCode?: number;
index?: number;
}
export interface VerifyCompletionActionOptions extends NewContentOptions {
name: string;
description: string;
}
}

View file

@ -405,8 +405,8 @@ namespace Harness.LanguageService {
getCompletionsAtPosition(fileName: string, position: number): ts.CompletionInfo {
return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position));
}
getCompletionEntryDetails(fileName: string, position: number, entryName: string): ts.CompletionEntryDetails {
return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName));
getCompletionEntryDetails(fileName: string, position: number, entryName: string, options: ts.FormatCodeOptions): ts.CompletionEntryDetails {
return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(options)));
}
getCompletionEntrySymbol(): ts.Symbol {
throw new Error("getCompletionEntrySymbol not implemented across the shim layer.");

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

@ -1658,6 +1658,11 @@ namespace ts.server.protocol {
* this span should be used instead of the default one.
*/
replacementSpan?: TextSpan;
/**
* Indicates whether 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;
}
/**
@ -1690,6 +1695,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));
}
@ -1199,10 +1205,20 @@ namespace ts.server {
private getCompletionEntryDetails(args: protocol.CompletionDetailsRequestArgs): ReadonlyArray<protocol.CompletionEntryDetails> {
const { file, project } = this.getFileAndProject(args);
const position = this.getPositionInFile(args, file);
const scriptInfo = this.projectService.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

@ -5,15 +5,13 @@ namespace ts {
getCodeActions(context: CodeFixContext): CodeAction[] | undefined;
}
export interface CodeFixContext {
export interface CodeFixContext extends textChanges.TextChangesContext {
errorCode: number;
sourceFile: SourceFile;
span: TextSpan;
program: Program;
newLineCharacter: string;
host: LanguageServiceHost;
cancellationToken: CancellationToken;
rulesProvider: formatting.RulesProvider;
}
export namespace codefix {

File diff suppressed because it is too large Load diff

View file

@ -4,13 +4,31 @@
namespace ts.Completions {
export type Log = (message: string) => void;
interface SymbolOriginInfo {
moduleSymbol: Symbol;
isDefaultExport: boolean;
}
/**
* Map from symbol id -> SymbolOriginInfo.
* Only populated for symbols that come from other modules.
*/
type SymbolOriginInfoMap = SymbolOriginInfo[];
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 +37,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 +74,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 +82,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 +152,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: ReadonlyArray<Symbol>,
entries: Push<CompletionEntry>,
location: Node,
performCharacterChecks: boolean,
typeChecker: TypeChecker,
target: ScriptTarget,
log: Log,
allowStringLiteral: boolean,
symbolToOriginInfoMap?: SymbolOriginInfoMap,
): Map<true> {
const start = timestamp();
const uniqueNames = createMap<true>();
if (symbols) {
@ -143,6 +171,9 @@ namespace ts.Completions {
if (entry) {
const id = entry.name;
if (!uniqueNames.has(id)) {
if (symbolToOriginInfoMap && symbolToOriginInfoMap[getUniqueSymbolId(symbol, typeChecker)]) {
entry.hasAction = true;
}
entries.push(entry);
uniqueNames.set(id, true);
}
@ -298,53 +329,89 @@ 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,
name: 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
// name against 'entryName' (which is known to be good), not building a new
// completion entry.
const symbol = forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral) === entryName ? s : undefined);
const symbol = find(symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral) === name);
if (symbol) {
const codeActions = getCompletionEntryCodeActions(symbolToOriginInfoMap, symbol, typeChecker, host, compilerOptions, sourceFile, rulesProvider);
const kindModifiers = SymbolDisplay.getSymbolModifiers(symbol);
const { displayParts, documentation, symbolKind, tags } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, location, location, SemanticMeaning.All);
return {
name: entryName,
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
kind: symbolKind,
displayParts,
documentation,
tags
};
return { name, kindModifiers, kind: symbolKind, displayParts, documentation, tags, codeActions };
}
}
// Didn't find a symbol with this name. See if we can find a keyword instead.
const keywordCompletion = forEach(
getKeywordCompletions(KeywordCompletionFilters.None),
c => c.name === entryName
c => c.name === name
);
if (keywordCompletion) {
return {
name: entryName,
name,
kind: ScriptElementKind.keyword,
kindModifiers: ScriptElementKindModifier.none,
displayParts: [displayPart(entryName, SymbolDisplayPartKind.keyword)],
displayParts: [displayPart(name, 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 {
function getCompletionEntryCodeActions(symbolToOriginInfoMap: SymbolOriginInfoMap, symbol: Symbol, checker: TypeChecker, host: LanguageServiceHost, compilerOptions: CompilerOptions, sourceFile: SourceFile, rulesProvider: formatting.RulesProvider): CodeAction[] | undefined {
const symbolOriginInfo = symbolToOriginInfoMap[getUniqueSymbolId(symbol, checker)];
if (!symbolOriginInfo) {
return undefined;
}
const { moduleSymbol, isDefaultExport } = symbolOriginInfo;
return codefix.getCodeActionForImport(moduleSymbol, {
host,
checker,
newLineCharacter: host.getNewLine(),
compilerOptions,
sourceFile,
rulesProvider,
symbolName: symbol.name,
getCanonicalFileName: createGetCanonicalFileName(host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : false),
symbolToken: undefined,
kind: isDefaultExport ? codefix.ImportKind.Default : codefix.ImportKind.Named,
});
}
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;
}
@ -353,7 +420,7 @@ namespace ts.Completions {
// We don't need to perform character checks here because we're only comparing the
// name against 'entryName' (which is known to be good), not building a new
// completion entry.
return forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral) === entryName ? s : undefined);
return find(symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral) === entryName);
}
interface CompletionData {
@ -366,10 +433,17 @@ namespace ts.Completions {
isRightOfDot: boolean;
request?: Request;
keywordFilters: KeywordCompletionFilters;
symbolToOriginInfoMap: SymbolOriginInfoMap;
}
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 +515,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) {
@ -543,6 +628,7 @@ namespace ts.Completions {
let isNewIdentifierLocation: boolean;
let keywordFilters = KeywordCompletionFilters.None;
let symbols: Symbol[] = [];
const symbolToOriginInfoMap: SymbolOriginInfoMap = [];
if (isRightOfDot) {
getTypeScriptMemberSymbols();
@ -579,7 +665,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 = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
@ -752,13 +838,16 @@ namespace ts.Completions {
}
const symbolMeanings = SymbolFlags.Type | SymbolFlags.Value | SymbolFlags.Namespace | SymbolFlags.Alias;
symbols = filterGlobalCompletion(typeChecker.getSymbolsInScope(scopeNode, symbolMeanings));
symbols = typeChecker.getSymbolsInScope(scopeNode, symbolMeanings);
getSymbolsFromOtherSourceFileExports(symbols, previousToken === undefined ? "" : previousToken.getText());
filterGlobalCompletion(symbols);
return true;
}
function filterGlobalCompletion(symbols: Symbol[]) {
return filter(symbols, symbol => {
function filterGlobalCompletion(symbols: Symbol[]): void {
filterMutate(symbols, symbol => {
if (!isSourceFile(location)) {
// export = /**/ here we want to get all meanings, so any symbol is ok
if (isExportAssignment(location.parent)) {
@ -832,6 +921,31 @@ namespace ts.Completions {
}
}
function getSymbolsFromOtherSourceFileExports(symbols: Symbol[], tokenText: string): void {
const tokenTextLowerCase = tokenText.toLowerCase();
const symbolIdMap = arrayToNumericMap(symbols, s => getUniqueSymbolId(s, typeChecker));
codefix.eachOtherExternalModule(typeChecker, allSourceFiles, sourceFile, moduleSymbol => {
for (let symbol of typeChecker.getExportsOfModule(moduleSymbol)) {
let { name } = symbol;
const isDefaultExport = name === "default";
if (isDefaultExport) {
const localSymbol = getLocalSymbolForExportDefault(symbol);
if (localSymbol) {
symbol = localSymbol;
name = localSymbol.name;
}
}
const id = getUniqueSymbolId(symbol, typeChecker);
if (!symbolIdMap[id] && startsWith(name.toLowerCase(), tokenTextLowerCase)) {
symbols.push(symbol);
symbolToOriginInfoMap[id] = { moduleSymbol, isDefaultExport };
}
}
});
}
/**
* Finds the first node that "embraces" the position, so that one may
* accurately aggregate locals from the closest containing scope.
@ -1622,7 +1736,7 @@ namespace ts.Completions {
// First check of the displayName is not external module; if it is an external module, it is not valid entry
if (symbol.flags & SymbolFlags.Namespace) {
const firstCharCode = name.charCodeAt(0);
if (firstCharCode === CharacterCodes.singleQuote || firstCharCode === CharacterCodes.doubleQuote) {
if (isSingleOrDoubleQuote(firstCharCode)) {
// If the symbol is external module, don't show it in the completion list
// (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there)
return undefined;

View file

@ -339,7 +339,7 @@ namespace ts.Completions.PathCompletions {
}
}
else if (host.getDirectories) {
let typeRoots: string[];
let typeRoots: ReadonlyArray<string>;
try {
// Wrap in try catch because getEffectiveTypeRoots touches the filesystem
typeRoots = getEffectiveTypeRoots(options, host);

View file

@ -14,13 +14,11 @@ namespace ts {
getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined;
}
export interface RefactorContext {
export interface RefactorContext extends textChanges.TextChangesContext {
file: SourceFile;
startPosition: number;
endPosition?: number;
program: Program;
newLineCharacter: string;
rulesProvider?: formatting.RulesProvider;
cancellationToken?: CancellationToken;
}

View file

@ -1324,17 +1324,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

@ -141,7 +141,7 @@ namespace ts {
getEncodedSemanticClassifications(fileName: string, start: number, length: number): string;
getCompletionsAtPosition(fileName: string, position: number): string;
getCompletionEntryDetails(fileName: string, position: number, entryName: string): string;
getCompletionEntryDetails(fileName: string, position: number, entryName: string, options: string/*Services.FormatCodeOptions*/): string;
getQuickInfoAtPosition(fileName: string, position: number): string;
@ -601,7 +601,7 @@ namespace ts {
this.logger = this.host;
}
public forwardJSONCall(actionDescription: string, action: () => any): string {
public forwardJSONCall(actionDescription: string, action: () => {}): string {
return forwardJSONCall(this.logger, actionDescription, action, this.logPerformance);
}
@ -893,10 +893,13 @@ namespace ts {
}
/** Get a string based representation of a completion list entry details */
public getCompletionEntryDetails(fileName: string, position: number, entryName: string) {
public getCompletionEntryDetails(fileName: string, position: number, entryName: string, options: string/*Services.FormatCodeOptions*/) {
return this.forwardJSONCall(
`getCompletionEntryDetails('${fileName}', ${position}, '${entryName}')`,
() => this.languageService.getCompletionEntryDetails(fileName, position, entryName)
() => {
const localOptions: ts.FormatCodeOptions = JSON.parse(options);
return this.languageService.getCompletionEntryDetails(fileName, position, entryName, localOptions);
}
);
}
@ -1030,7 +1033,7 @@ namespace ts {
super(factory);
}
private forwardJSONCall(actionDescription: string, action: () => any): any {
private forwardJSONCall(actionDescription: string, action: () => {}): any {
return forwardJSONCall(this.logger, actionDescription, action, this.logPerformance);
}

View file

@ -186,14 +186,25 @@ namespace ts.textChanges {
return s;
}
export interface TextChangesContext {
newLineCharacter: string;
rulesProvider: formatting.RulesProvider;
}
export class ChangeTracker {
private changes: Change[] = [];
private readonly newLineCharacter: string;
public static fromContext(context: RefactorContext | CodeFixContext) {
public static fromContext(context: TextChangesContext): ChangeTracker {
return new ChangeTracker(context.newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed, context.rulesProvider);
}
public static with(context: TextChangesContext, cb: (tracker: ChangeTracker) => void): FileTextChanges[] {
const tracker = ChangeTracker.fromContext(context);
cb(tracker);
return tracker.getChanges();
}
constructor(
private readonly newLine: NewLineKind,
private readonly rulesProvider: formatting.RulesProvider,

View file

@ -230,7 +230,8 @@ namespace ts {
getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications;
getCompletionsAtPosition(fileName: string, position: number): CompletionInfo;
getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails;
// "options" is optional only for backwards-compatibility
getCompletionEntryDetails(fileName: string, position: number, entryName: string, options?: FormatCodeOptions | FormatCodeSettings): CompletionEntryDetails;
getCompletionEntrySymbol(fileName: string, position: number, entryName: string): Symbol;
getQuickInfoAtPosition(fileName: string, position: number): QuickInfo;
@ -668,6 +669,7 @@ namespace ts {
* be used in that case
*/
replacementSpan?: TextSpan;
hasAction?: true;
}
export interface CompletionEntryDetails {
@ -677,6 +679,7 @@ namespace ts {
displayParts: SymbolDisplayPart[];
documentation: SymbolDisplayPart[];
tags: JSDocTagInfo[];
codeActions?: CodeAction[];
}
export interface OutliningSpan {

View file

@ -1278,9 +1278,7 @@ namespace ts {
*/
export function stripQuotes(name: string) {
const length = name.length;
if (length >= 2 &&
name.charCodeAt(0) === name.charCodeAt(length - 1) &&
(name.charCodeAt(0) === CharacterCodes.doubleQuote || name.charCodeAt(0) === CharacterCodes.singleQuote)) {
if (length >= 2 && name.charCodeAt(0) === name.charCodeAt(length - 1) && isSingleOrDoubleQuote(name.charCodeAt(0))) {
return name.substring(1, length - 1);
}
return name;
@ -1297,6 +1295,10 @@ namespace ts {
return ensureScriptKind(fileName, host && host.getScriptKind && host.getScriptKind(fileName));
}
export function getUniqueSymbolId(symbol: Symbol, checker: TypeChecker) {
return getSymbolId(skipAlias(symbol, checker));
}
export function getFirstNonSpaceCharacterPosition(text: string, position: number) {
while (isWhiteSpaceLike(text.charCodeAt(position))) {
position += 1;

View file

@ -3205,10 +3205,11 @@ declare namespace ts {
};
}
declare namespace ts {
function getEffectiveTypeRoots(options: CompilerOptions, host: {
directoryExists?: (directoryName: string) => boolean;
getCurrentDirectory?: () => string;
}): string[] | undefined;
interface GetEffectiveTypeRootsHost {
directoryExists?(directoryName: string): boolean;
getCurrentDirectory?(): string;
}
function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined;
/**
* @param {string | undefined} containingFile - file that contains type reference directive, can be undefined if containing file is unknown.
* This is possible in case if resolution is performed for directives specified via 'types' parameter. In this case initial path for secondary lookups
@ -3911,7 +3912,7 @@ declare namespace ts {
getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications;
getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications;
getCompletionsAtPosition(fileName: string, position: number): CompletionInfo;
getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails;
getCompletionEntryDetails(fileName: string, position: number, entryName: string, options?: FormatCodeOptions | FormatCodeSettings): CompletionEntryDetails;
getCompletionEntrySymbol(fileName: string, position: number, entryName: string): Symbol;
getQuickInfoAtPosition(fileName: string, position: number): QuickInfo;
getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan;
@ -4279,6 +4280,7 @@ declare namespace ts {
* be used in that case
*/
replacementSpan?: TextSpan;
hasAction?: true;
}
interface CompletionEntryDetails {
name: string;
@ -4287,6 +4289,7 @@ declare namespace ts {
displayParts: SymbolDisplayPart[];
documentation: SymbolDisplayPart[];
tags: JSDocTagInfo[];
codeActions?: CodeAction[];
}
interface OutliningSpan {
/** The span of the document to actually collapse. */
@ -6036,6 +6039,11 @@ declare namespace ts.server.protocol {
* this span should be used instead of the default one.
*/
replacementSpan?: TextSpan;
/**
* Indicates whether 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;
}
/**
* Additional completion entry details, available on demand
@ -6065,6 +6073,10 @@ declare namespace ts.server.protocol {
* JSDoc tags for the symbol.
*/
tags: JSDocTagInfo[];
/**
* The associated code actions for this entry
*/
codeActions?: CodeAction[];
}
interface CompletionsResponse extends Response {
body?: CompletionEntry[];

View file

@ -3152,10 +3152,11 @@ declare namespace ts {
function updateSourceFile(sourceFile: SourceFile, newText: string, textChangeRange: TextChangeRange, aggressiveChecks?: boolean): SourceFile;
}
declare namespace ts {
function getEffectiveTypeRoots(options: CompilerOptions, host: {
directoryExists?: (directoryName: string) => boolean;
getCurrentDirectory?: () => string;
}): string[] | undefined;
interface GetEffectiveTypeRootsHost {
directoryExists?(directoryName: string): boolean;
getCurrentDirectory?(): string;
}
function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined;
/**
* @param {string | undefined} containingFile - file that contains type reference directive, can be undefined if containing file is unknown.
* This is possible in case if resolution is performed for directives specified via 'types' parameter. In this case initial path for secondary lookups
@ -3911,7 +3912,7 @@ declare namespace ts {
getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications;
getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications;
getCompletionsAtPosition(fileName: string, position: number): CompletionInfo;
getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails;
getCompletionEntryDetails(fileName: string, position: number, entryName: string, options?: FormatCodeOptions | FormatCodeSettings): CompletionEntryDetails;
getCompletionEntrySymbol(fileName: string, position: number, entryName: string): Symbol;
getQuickInfoAtPosition(fileName: string, position: number): QuickInfo;
getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan;
@ -4279,6 +4280,7 @@ declare namespace ts {
* be used in that case
*/
replacementSpan?: TextSpan;
hasAction?: true;
}
interface CompletionEntryDetails {
name: string;
@ -4287,6 +4289,7 @@ declare namespace ts {
displayParts: SymbolDisplayPart[];
documentation: SymbolDisplayPart[];
tags: JSDocTagInfo[];
codeActions?: CodeAction[];
}
interface OutliningSpan {
/** The span of the document to actually collapse. */

View file

@ -0,0 +1,19 @@
/// <reference path="fourslash.ts" />
// @Filename: /a.ts
////export default function foo() {}
////export const x = 0;
// @Filename: /b.ts
////import { x } from "./a";
////f/**/;
goTo.marker("");
verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.applyCodeActionFromCompletion("", {
name: "foo",
description: `Add 'foo' to existing import declaration from "./a".`,
newFileContent: `import foo, { x } from "./a";
f;`,
});

View file

@ -0,0 +1,18 @@
/// <reference path="fourslash.ts" />
// @Filename: /a.ts
////export default function foo() {}
// @Filename: /b.ts
////import * as a from "./a";
////f/**/;
goTo.marker("");
verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.applyCodeActionFromCompletion("", {
name: "foo",
description: `Add 'foo' to existing import declaration from "./a".`,
newFileContent: `import foo, * as a from "./a";
f;`,
});

View file

@ -0,0 +1,20 @@
/// <reference path="fourslash.ts" />
// @Filename: /a.ts
////export default function foo() {}
// @Filename: /b.ts
////import f_o_o from "./a";
////f/**/;
goTo.marker("");
verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.applyCodeActionFromCompletion("", {
name: "foo",
description: `Import 'foo' from "./a".`,
// TODO: GH#18445
newFileContent: `import f_o_o from "./a";
import foo from "./a";\r
f;`,
});

View file

@ -0,0 +1,19 @@
/// <reference path="fourslash.ts" />
// @Filename: /a.ts
////export default function foo() {}
// @Filename: /b.ts
////f/**/;
goTo.marker("");
verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.applyCodeActionFromCompletion("", {
name: "foo",
description: `Import 'foo' from "./a".`,
// TODO: GH#18445
newFileContent: `import foo from "./a";\r
\r
f;`,
});

View file

@ -0,0 +1,18 @@
/// <reference path="fourslash.ts" />
// @Filename: /a.ts
////declare module "m" {
//// export const x: number;
////}
// @Filename: /b.ts
/////**/
verify.applyCodeActionFromCompletion("", {
name: "x",
description: `Import 'x' from "m".`,
// TODO: GH#18445
newFileContent: `import { x } from "m";\r
\r
`,
});

View file

@ -0,0 +1,19 @@
/// <reference path="fourslash.ts" />
// @Filename: /a.ts
////export function foo() {}
////export const x = 0;
// @Filename: /b.ts
////import { x } from "./a";
////f/**/;
goTo.marker("");
verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.applyCodeActionFromCompletion("", {
name: "foo",
description: `Add 'foo' to existing import declaration from "./a".`,
newFileContent: `import { x, foo } from "./a";
f;`,
});

View file

@ -0,0 +1,20 @@
/// <reference path="fourslash.ts" />
// @Filename: /a.ts
////export function Test1() {}
////export function Test2() {}
// @Filename: /b.ts
////import { Test2 } from "./a";
////t/**/
goTo.marker("");
verify.completionListContains("Test1", "function Test1(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.completionListContains("Test2", "import Test2", "", "alias", /*spanIndex*/ undefined, /*hasAction*/ undefined);
verify.applyCodeActionFromCompletion("", {
name: "Test1",
description: `Add 'Test1' to existing import declaration from "./a".`,
newFileContent: `import { Test2, Test1 } from "./a";
t`,
});

View file

@ -0,0 +1,20 @@
/// <reference path="fourslash.ts" />
// @Filename: /a.ts
////export function foo() {}
// @Filename: /b.ts
////import * as a from "./a";
////f/**/;
goTo.marker("");
verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.applyCodeActionFromCompletion("", {
name: "foo",
description: `Import 'foo' from "./a".`,
// TODO: GH#18445
newFileContent: `import * as a from "./a";
import { foo } from "./a";\r
f;`,
});

View file

@ -140,7 +140,14 @@ declare namespace FourSlashInterface {
allowedConstructorParameterKeywords: string[];
constructor(negative?: boolean);
completionListCount(expectedCount: number): void;
completionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number): void;
completionListContains(
symbol: string,
text?: string,
documentation?: string,
kind?: string,
spanIndex?: number,
hasAction?: boolean,
): void;
completionListItemsCountIsGreaterThan(count: number): void;
completionListIsEmpty(): void;
completionListContainsClassElementKeywords(): void;
@ -173,6 +180,20 @@ declare namespace FourSlashInterface {
assertHasRanges(ranges: Range[]): void;
caretAtMarker(markerName?: string): void;
completionsAt(markerName: string, completions: string[], options?: { isNewIdentifierLocation?: boolean }): void;
completionsAndDetailsAt(
markerName: string,
completions: {
excludes?: ReadonlyArray<string>,
//TODO: better type
entries: ReadonlyArray<{ entry: any, details: any }>,
},
): void; //TODO: better type
applyCodeActionFromCompletion(markerName: string, options: {
name: string,
description: string,
newFileContent?: string,
newRangeContent?: string,
});
indentationIs(numberOfSpaces: number): void;
indentationAtPositionIs(fileName: string, position: number, numberOfSpaces: number, indentStyle?: ts.IndentStyle, baseIndentSize?: number): void;
textAtCaretIs(text: string): void;

View file

@ -8,7 +8,7 @@
//// export function foo() {};
// @Filename: a/foo.ts
//// export { foo } from "./foo/bar";
//// export { foo } from "./foo/bar";
verify.importFixAtPosition([
`import * as ns from "./foo";