Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exported members of all project files in the global completion list #13921

Closed
wants to merge 9 commits into from
12 changes: 7 additions & 5 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5998,11 +5998,13 @@ namespace ts {

function symbolsToArray(symbols: SymbolTable): Symbol[] {
const result: Symbol[] = [];
symbols.forEach((symbol, id) => {
if (!isReservedMemberName(id)) {
result.push(symbol);
}
});
if (symbols) {
symbols.forEach((symbol, id) => {
if (!isReservedMemberName(id)) {
result.push(symbol);
}
});
}
return result;
}

Expand Down
1 change: 0 additions & 1 deletion src/harness/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@
"../services/codefixes/importFixes.ts",
"../services/codefixes/unusedIdentifierFixes.ts",
"../services/codefixes/disableJsDiagnostics.ts",

"harness.ts",
"sourceMapRecorder.ts",
"harnessLanguageService.ts",
Expand Down
101 changes: 101 additions & 0 deletions src/harness/unittests/tsserverProjectSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3476,6 +3476,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.path.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 = {
Expand Down Expand Up @@ -3781,6 +3818,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: ``
};
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("guitar", /*hasAction*/ true, `import { guitar } from "./moduleFile";\n\n`);
checkEntryDetail("Jazz", /*hasAction*/ false);
checkEntryDetail("chetAtkins", /*hasAction*/ true, `import { chetAtkins } from "windyAndWarm";\n\n`);
checkEntryDetail("egyptianElla", /*hasAction*/ true, `import egyptianElla from "./defaultModuleFile";\n\n`);

function checkEntryDetail(entryName: string, hasAction: boolean, insertString?: string) {
const request = makeSessionRequest<protocol.CompletionDetailsRequestArgs>(
CommandNames.CompletionDetails,
{ entryNames: [entryName], file: file1.path, line: 1, offset: 0, projectFileName: configFile.path });
const response = session.executeCommand(request).response as protocol.CompletionEntryDetails[];
assert.isTrue(response.length === 1);

const entryDetails = response[0];
if (!hasAction) {
assert.isUndefined(entryDetails.codeActions);
}
else {
const action = entryDetails.codeActions[0];
assert.isTrue(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 = {
Expand Down
4 changes: 3 additions & 1 deletion src/server/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,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 {
Expand Down
10 changes: 10 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,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;
}

/**
Expand Down Expand Up @@ -1559,6 +1564,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 {
Expand Down
16 changes: 12 additions & 4 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1186,10 +1186,16 @@ namespace ts.server {
if (simplifiedResult) {
return completions.entries.reduce((result: protocol.CompletionEntry[], entry: ts.CompletionEntry) => {
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: protocol.TextSpan =
replacementSpan ? this.decorateSpan(replacementSpan, scriptInfo) : undefined;
result.push({ 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;
}
result.push(newEntry);
}
return result;
}, []).sort((a, b) => ts.compareStrings(a.name, b.name));
Expand All @@ -1203,11 +1209,13 @@ 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 args.entryNames.reduce((accum: protocol.CompletionEntryDetails[], entryName: string) => {
const details = project.getLanguageService().getCompletionEntryDetails(file, position, entryName);
const details = project.getLanguageService().getCompletionEntryDetails(file, position, entryName, formattingOptions);
if (details) {
accum.push(details);
const mappedCodeActions = map(details.codeActions, action => this.mapCodeAction(action, scriptInfo));
accum.push({ ...details, codeActions: mappedCodeActions });
}
return accum;
}, []);
Expand Down
Loading