Skip to content

Commit 0f3a9d4

Browse files
authored
Support completions for local named exports (microsoft#37606)
* Support completions for local named exports * Add JSDoc * Use sort text instead of filtering * Revert new CompletionKind * Return valid completions even when export declaration is in an invalid place
1 parent 7f1df6e commit 0f3a9d4

File tree

4 files changed

+127
-6
lines changed

4 files changed

+127
-6
lines changed

src/services/completions.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,6 +1271,7 @@ namespace ts.Completions {
12711271
function tryGetGlobalSymbols(): boolean {
12721272
const result: GlobalsSearch = tryGetObjectLikeCompletionSymbols()
12731273
|| tryGetImportOrExportClauseCompletionSymbols()
1274+
|| tryGetLocalNamedExportCompletionSymbols()
12741275
|| tryGetConstructorCompletion()
12751276
|| tryGetClassLikeCompletionSymbols()
12761277
|| tryGetJsxCompletionSymbols()
@@ -1881,19 +1882,17 @@ namespace ts.Completions {
18811882
* export { | };
18821883
*
18831884
* Relevant symbols are stored in the captured 'symbols' variable.
1884-
*
1885-
* @returns true if 'symbols' was successfully populated; false otherwise.
18861885
*/
18871886
function tryGetImportOrExportClauseCompletionSymbols(): GlobalsSearch {
18881887
// `import { |` or `import { a as 0, | }`
18891888
const namedImportsOrExports = contextToken && (contextToken.kind === SyntaxKind.OpenBraceToken || contextToken.kind === SyntaxKind.CommaToken)
18901889
? tryCast(contextToken.parent, isNamedImportsOrExports) : undefined;
18911890
if (!namedImportsOrExports) return GlobalsSearch.Continue;
18921891

1893-
// cursor is in an import clause
1894-
// try to show exported member for imported module
1892+
// try to show exported member for imported/re-exported module
18951893
const { moduleSpecifier } = namedImportsOrExports.kind === SyntaxKind.NamedImports ? namedImportsOrExports.parent.parent : namedImportsOrExports.parent;
1896-
const moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(moduleSpecifier!); // TODO: GH#18217
1894+
if (!moduleSpecifier) return namedImportsOrExports.kind === SyntaxKind.NamedImports ? GlobalsSearch.Fail : GlobalsSearch.Continue;
1895+
const moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(moduleSpecifier); // TODO: GH#18217
18971896
if (!moduleSpecifierSymbol) return GlobalsSearch.Fail;
18981897

18991898
completionKind = CompletionKind.MemberLike;
@@ -1904,6 +1903,36 @@ namespace ts.Completions {
19041903
return GlobalsSearch.Success;
19051904
}
19061905

1906+
/**
1907+
* Adds local declarations for completions in named exports:
1908+
*
1909+
* export { | };
1910+
*
1911+
* Does not check for the absence of a module specifier (`export {} from "./other"`)
1912+
* because `tryGetImportOrExportClauseCompletionSymbols` runs first and handles that,
1913+
* preventing this function from running.
1914+
*/
1915+
function tryGetLocalNamedExportCompletionSymbols(): GlobalsSearch {
1916+
const namedExports = contextToken && (contextToken.kind === SyntaxKind.OpenBraceToken || contextToken.kind === SyntaxKind.CommaToken)
1917+
? tryCast(contextToken.parent, isNamedExports)
1918+
: undefined;
1919+
1920+
if (!namedExports) {
1921+
return GlobalsSearch.Continue;
1922+
}
1923+
1924+
const localsContainer = findAncestor(namedExports, or(isSourceFile, isModuleDeclaration))!;
1925+
completionKind = CompletionKind.None;
1926+
isNewIdentifierLocation = false;
1927+
localsContainer.locals?.forEach((symbol, name) => {
1928+
symbols.push(symbol);
1929+
if (localsContainer.symbol?.exports?.has(name)) {
1930+
symbolToSortTextMap[getSymbolId(symbol)] = SortText.OptionalMember;
1931+
}
1932+
});
1933+
return GlobalsSearch.Success;
1934+
}
1935+
19071936
/**
19081937
* Aggregates relevant symbols for completion in class declaration
19091938
* Relevant symbols are stored in the captured 'symbols' variable.
@@ -2299,7 +2328,7 @@ namespace ts.Completions {
22992328
}
23002329
}
23012330

2302-
// Set SortText to OptionalMember if it is an optinoal member
2331+
// Set SortText to OptionalMember if it is an optional member
23032332
function setSortTextToOptionalMember() {
23042333
symbols.forEach(m => {
23052334
if (m.flags & SymbolFlags.Optional) {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////const a = "a";
4+
////type T = number;
5+
////export { /**/ };
6+
7+
verify.completions({
8+
marker: "",
9+
exact: ["a", "T"]
10+
});
11+
12+
// Deprioritize 'a' since it has been exported already.
13+
// (Keep it in the list because you can still do 'a as b'.)
14+
edit.insert("a, ");
15+
verify.completions({
16+
exact: [{ name: "a", sortText: completion.SortText.OptionalMember }, "T"]
17+
});
18+
19+
// No completions for new name
20+
edit.insert("T as ");
21+
verify.completions({
22+
exact: []
23+
});
24+
25+
// 'T' still hasn't been exported by name
26+
edit.insert("U, ");
27+
verify.completions({
28+
exact: [{ name: "a", sortText: completion.SortText.OptionalMember }, "T"]
29+
});
30+
31+
// 'a' and 'T' are back to the same priority
32+
edit.insert("T, ");
33+
verify.completions({
34+
exact: [
35+
{ name: "a", sortText: completion.SortText.OptionalMember },
36+
{ name: "T", sortText: completion.SortText.OptionalMember }
37+
]
38+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////function topLevel() {}
4+
////if (!!true) {
5+
//// const blockScoped = 0;
6+
//// export { /**/ };
7+
////}
8+
9+
verify.completions({
10+
marker: "",
11+
exact: ["topLevel"]
12+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////const outOfScope = 0;
4+
////
5+
////declare module 'mod' {
6+
//// const a: string;
7+
//// type T = number;
8+
//// export { /**/ };
9+
////}
10+
11+
verify.completions({
12+
marker: "",
13+
exact: ["a", "T"]
14+
});
15+
16+
// Deprioritize 'a' since it has been exported already.
17+
// (Keep it in the list because you can still do 'a as b'.)
18+
edit.insert("a, ");
19+
verify.completions({
20+
exact: [{ name: "a", sortText: completion.SortText.OptionalMember }, "T"]
21+
});
22+
23+
// No completions for new name
24+
edit.insert("T as ");
25+
verify.completions({
26+
exact: []
27+
});
28+
29+
// 'T' still hasn't been exported by name
30+
edit.insert("U, ");
31+
verify.completions({
32+
exact: [{ name: "a", sortText: completion.SortText.OptionalMember }, "T"]
33+
});
34+
35+
// 'a' and 'T' are back to the same priority
36+
edit.insert("T, ");
37+
verify.completions({
38+
exact: [
39+
{ name: "a", sortText: completion.SortText.OptionalMember },
40+
{ name: "T", sortText: completion.SortText.OptionalMember }
41+
]
42+
});

0 commit comments

Comments
 (0)