Skip to content
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