Merge branch 'master' into strictObjectLiterals

This commit is contained in:
Anders Hejlsberg
2017-10-26 13:15:36 -07:00
50 changed files with 749 additions and 273 deletions
+29 -23
View File
@@ -783,13 +783,13 @@ namespace FourSlash {
});
}
public verifyCompletionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number, hasAction?: boolean) {
public verifyCompletionListContains(entryId: ts.Completions.CompletionEntryIdentifier, 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, hasAction);
this.assertItemInCompletionList(completions.entries, entryId, text, documentation, kind, spanIndex, hasAction);
}
else {
this.raiseError(`No completions at position '${this.currentCaretPosition}' when looking for '${symbol}'.`);
this.raiseError(`No completions at position '${this.currentCaretPosition}' when looking for '${JSON.stringify(entryId)}'.`);
}
}
@@ -804,7 +804,7 @@ namespace FourSlash {
* @param expectedKind the kind of symbol (see ScriptElementKind)
* @param spanIndex the index of the range that the completion item's replacement text span should match
*/
public verifyCompletionListDoesNotContain(symbol: string, expectedText?: string, expectedDocumentation?: string, expectedKind?: string, spanIndex?: number) {
public verifyCompletionListDoesNotContain(entryId: ts.Completions.CompletionEntryIdentifier, expectedText?: string, expectedDocumentation?: string, expectedKind?: string, spanIndex?: number) {
const that = this;
let replacementSpan: ts.TextSpan;
if (spanIndex !== undefined) {
@@ -833,14 +833,14 @@ namespace FourSlash {
const completions = this.getCompletionListAtCaret();
if (completions) {
let filterCompletions = completions.entries.filter(e => e.name === symbol);
let filterCompletions = completions.entries.filter(e => e.name === entryId.name && e.source === entryId.source);
filterCompletions = expectedKind ? filterCompletions.filter(e => e.kind === expectedKind) : filterCompletions;
filterCompletions = filterCompletions.filter(filterByTextOrDocumentation);
if (filterCompletions.length !== 0) {
// After filtered using all present criterion, if there are still symbol left in the list
// then these symbols must meet the criterion for Not supposed to be in the list. So we
// raise an error
let error = "Completion list did contain \'" + symbol + "\'.";
let error = `Completion list did contain '${JSON.stringify(entryId)}\'.`;
const details = this.getCompletionEntryDetails(filterCompletions[0].name);
if (expectedText) {
error += "Expected text: " + expectedText + " to equal: " + ts.displayPartsToString(details.displayParts) + ".";
@@ -1130,8 +1130,8 @@ Actual: ${stringify(fullActual)}`);
return this.languageService.getCompletionsAtPosition(this.activeFile.fileName, this.currentCaretPosition);
}
private getCompletionEntryDetails(entryName: string) {
return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings);
private getCompletionEntryDetails(entryName: string, source?: string) {
return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings, source);
}
private getReferencesAtCaret() {
@@ -1640,7 +1640,7 @@ Actual: ${stringify(fullActual)}`);
const longestNameLength = max(entries, m => m.name.length);
const longestKindLength = max(entries, m => m.kind.length);
entries.sort((m, n) => m.sortText > n.sortText ? 1 : m.sortText < n.sortText ? -1 : m.name > n.name ? 1 : m.name < n.name ? -1 : 0);
const membersString = entries.map(m => `${pad(m.name, longestNameLength)} ${pad(m.kind, longestKindLength)} ${m.kindModifiers}`).join("\n");
const membersString = entries.map(m => `${pad(m.name, longestNameLength)} ${pad(m.kind, longestKindLength)} ${m.kindModifiers} ${m.source === undefined ? "" : m.source}`).join("\n");
Harness.IO.log(membersString);
}
@@ -2296,13 +2296,13 @@ Actual: ${stringify(fullActual)}`);
public applyCodeActionFromCompletion(markerName: string, options: FourSlashInterface.VerifyCompletionActionOptions) {
this.goToMarker(markerName);
const actualCompletion = this.getCompletionListAtCaret().entries.find(e => e.name === options.name);
const actualCompletion = this.getCompletionListAtCaret().entries.find(e => e.name === options.name && e.source === options.source);
if (!actualCompletion.hasAction) {
this.raiseError(`Completion for ${options.name} does not have an associated action.`);
}
const details = this.getCompletionEntryDetails(options.name);
const details = this.getCompletionEntryDetails(options.name, actualCompletion.source);
if (details.codeActions.length !== 1) {
this.raiseError(`Expected one code action, got ${details.codeActions.length}`);
}
@@ -2984,7 +2984,7 @@ Actual: ${stringify(fullActual)}`);
private assertItemInCompletionList(
items: ts.CompletionEntry[],
name: string,
entryId: ts.Completions.CompletionEntryIdentifier,
text: string | undefined,
documentation: string | undefined,
kind: string | undefined,
@@ -2992,25 +2992,27 @@ Actual: ${stringify(fullActual)}`);
hasAction: boolean | undefined,
) {
for (const item of items) {
if (item.name === name) {
if (documentation !== undefined || text !== undefined) {
const details = this.getCompletionEntryDetails(item.name);
if (item.name === entryId.name && item.source === entryId.source) {
if (documentation !== undefined || text !== undefined || entryId.source !== undefined) {
const details = this.getCompletionEntryDetails(item.name, item.source);
if (documentation !== undefined) {
assert.equal(ts.displayPartsToString(details.documentation), documentation, this.assertionMessageAtLastKnownMarker("completion item documentation for " + name));
assert.equal(ts.displayPartsToString(details.documentation), documentation, this.assertionMessageAtLastKnownMarker("completion item documentation for " + entryId));
}
if (text !== undefined) {
assert.equal(ts.displayPartsToString(details.displayParts), text, this.assertionMessageAtLastKnownMarker("completion item detail text for " + name));
assert.equal(ts.displayPartsToString(details.displayParts), text, this.assertionMessageAtLastKnownMarker("completion item detail text for " + entryId));
}
assert.deepEqual(details.source, entryId.source === undefined ? undefined : [ts.textPart(entryId.source)]);
}
if (kind !== undefined) {
assert.equal(item.kind, kind, this.assertionMessageAtLastKnownMarker("completion item kind for " + name));
assert.equal(item.kind, kind, this.assertionMessageAtLastKnownMarker("completion item kind for " + entryId));
}
if (spanIndex !== undefined) {
const span = this.getTextSpanForRangeAtIndex(spanIndex);
assert.isTrue(TestState.textSpansEqual(span, item.replacementSpan), this.assertionMessageAtLastKnownMarker(stringify(span) + " does not equal " + stringify(item.replacementSpan) + " replacement span for " + name));
assert.isTrue(TestState.textSpansEqual(span, item.replacementSpan), this.assertionMessageAtLastKnownMarker(stringify(span) + " does not equal " + stringify(item.replacementSpan) + " replacement span for " + entryId));
}
assert.equal(item.hasAction, hasAction);
@@ -3021,7 +3023,7 @@ Actual: ${stringify(fullActual)}`);
const itemsString = items.map(item => stringify({ name: item.name, kind: item.kind })).join(",\n");
this.raiseError(`Expected "${stringify({ name, text, documentation, kind })}" to be in list [${itemsString}]`);
this.raiseError(`Expected "${stringify({ entryId, text, documentation, kind })}" to be in list [${itemsString}]`);
}
private findFile(indexOrName: any) {
@@ -3732,12 +3734,15 @@ 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, hasAction?: boolean) {
public completionListContains(entryId: string | ts.Completions.CompletionEntryIdentifier, text?: string, documentation?: string, kind?: string, spanIndex?: number, hasAction?: boolean) {
if (typeof entryId === "string") {
entryId = { name: entryId, source: undefined };
}
if (this.negative) {
this.state.verifyCompletionListDoesNotContain(symbol, text, documentation, kind, spanIndex);
this.state.verifyCompletionListDoesNotContain(entryId, text, documentation, kind, spanIndex);
}
else {
this.state.verifyCompletionListContains(symbol, text, documentation, kind, spanIndex, hasAction);
this.state.verifyCompletionListContains(entryId, text, documentation, kind, spanIndex, hasAction);
}
}
@@ -4492,6 +4497,7 @@ namespace FourSlashInterface {
export interface VerifyCompletionActionOptions extends NewContentOptions {
name: string;
source?: string;
description: string;
}
}
+3
View File
@@ -1196,6 +1196,9 @@ namespace Harness {
traceResults = [];
compilerHost.trace = text => traceResults.push(text);
}
else {
compilerHost.directoryExists = () => true; // This only visibly affects resolution traces, so to save time we always return true where possible
}
const program = ts.createProgram(programFileNames, options, compilerHost);
const emitResult = program.emit();
+12 -8
View File
@@ -89,6 +89,7 @@ interface PlaybackControl {
namespace Playback {
let recordLog: IOLog = undefined;
let replayLog: IOLog = undefined;
let replayFilesRead: ts.Map<IOLogFile> | undefined = undefined;
let recordLogFileNameBase = "";
interface Memoized<T> {
@@ -222,10 +223,15 @@ namespace Playback {
replayLog = log;
// Remove non-found files from the log (shouldn't really need them, but we still record them for diagnostic purposes)
replayLog.filesRead = replayLog.filesRead.filter(f => f.result.contents !== undefined);
replayFilesRead = ts.createMap();
for (const file of replayLog.filesRead) {
replayFilesRead.set(ts.normalizeSlashes(file.path).toLowerCase(), file);
}
};
wrapper.endReplay = () => {
replayLog = undefined;
replayFilesRead = undefined;
};
wrapper.startRecord = (fileNameBase) => {
@@ -254,7 +260,7 @@ namespace Playback {
path => callAndRecord(underlying.fileExists(path), recordLog.fileExists, { path }),
memoize(path => {
// If we read from the file, it must exist
if (findFileByPath(replayLog.filesRead, path, /*throwFileNotFoundError*/ false)) {
if (findFileByPath(path, /*throwFileNotFoundError*/ false)) {
return true;
}
else {
@@ -298,7 +304,7 @@ namespace Playback {
recordLog.filesRead.push(logEntry);
return result;
},
memoize(path => findFileByPath(replayLog.filesRead, path, /*throwFileNotFoundError*/ true).contents));
memoize(path => findFileByPath(path, /*throwFileNotFoundError*/ true).contents));
wrapper.readDirectory = recordReplay(wrapper.readDirectory, underlying)(
(path, extensions, exclude, include, depth) => {
@@ -379,14 +385,12 @@ namespace Playback {
return results[0].result;
}
function findFileByPath(logArray: IOLogFile[],
expectedPath: string, throwFileNotFoundError: boolean): FileInformation {
function findFileByPath(expectedPath: string, throwFileNotFoundError: boolean): FileInformation {
const normalizedName = ts.normalizePath(expectedPath).toLowerCase();
// Try to find the result through normal fileName
for (const log of logArray) {
if (ts.normalizeSlashes(log.path).toLowerCase() === normalizedName) {
return log.result;
}
const result = replayFilesRead.get(normalizedName);
if (result) {
return result.result;
}
// If we got here, we didn't find a match
-20
View File
@@ -39,8 +39,6 @@ namespace RWC {
const baseName = ts.getBaseFileName(jsonPath);
let currentDirectory: string;
let useCustomLibraryFile: boolean;
let skipTypeBaselines = false;
const typeSizeLimit = 10000000;
after(() => {
// Mocha holds onto the closure environment of the describe callback even after the test is done.
// Therefore we have to clean out large objects after the test is done.
@@ -54,7 +52,6 @@ namespace RWC {
// or to use lib.d.ts inside the json object. If the flag is true, use the lib.d.ts inside json file
// otherwise use the lib.d.ts from built/local
useCustomLibraryFile = undefined;
skipTypeBaselines = false;
});
it("can compile", function(this: Mocha.ITestCallbackContext) {
@@ -64,7 +61,6 @@ namespace RWC {
const ioLog: IOLog = Playback.newStyleLogIntoOldStyleLog(JSON.parse(Harness.IO.readFile(`internal/cases/rwc/${jsonPath}/test.json`)), Harness.IO, `internal/cases/rwc/${baseName}`);
currentDirectory = ioLog.currentDirectory;
useCustomLibraryFile = ioLog.useCustomLibraryFile;
skipTypeBaselines = ioLog.filesRead.reduce((acc, elem) => (elem && elem.result && elem.result.contents) ? acc + elem.result.contents.length : acc, 0) > typeSizeLimit;
runWithIOLog(ioLog, () => {
opts = ts.parseCommandLine(ioLog.arguments, fileName => Harness.IO.readFile(fileName));
assert.equal(opts.errors.length, 0);
@@ -199,14 +195,6 @@ namespace RWC {
}, baselineOpts, [".map"]);
});
/*it("has correct source map record", () => {
if (compilerOptions.sourceMap) {
Harness.Baseline.runBaseline(baseName + ".sourcemap.txt", () => {
return compilerResult.getSourceMapRecord();
}, baselineOpts);
}
});*/
it("has the expected errors", () => {
Harness.Baseline.runMultifileBaseline(baseName, ".errors.txt", () => {
if (compilerResult.errors.length === 0) {
@@ -219,14 +207,6 @@ namespace RWC {
}, baselineOpts);
});
it("has the expected types", () => {
// We don't need to pass the extension here because "doTypeAndSymbolBaseline" will append appropriate extension of ".types" or ".symbols"
Harness.Compiler.doTypeAndSymbolBaseline(baseName, compilerResult.program, inputFiles
.concat(otherFiles)
.filter(file => !!compilerResult.program.getSourceFile(file.unitName))
.filter(e => !Harness.isDefaultLibraryFile(e.unitName)), baselineOpts, /*multifile*/ true, skipTypeBaselines, /*skipSymbolbaselines*/ true);
});
// Ideally, a generated declaration file will have no errors. But we allow generated
// declaration file errors as part of the baseline.
it("has the expected errors in generated declaration files", () => {
+12 -6
View File
@@ -362,27 +362,32 @@ function parsePrimaryExpression(): any {
}`);
testExtractFunction("extractFunction_VariableDeclaration_Var", `
[#|var x = 1;|]
[#|var x = 1;
"hello"|]
x;
`);
testExtractFunction("extractFunction_VariableDeclaration_Let_Type", `
[#|let x: number = 1;|]
[#|let x: number = 1;
"hello";|]
x;
`);
testExtractFunction("extractFunction_VariableDeclaration_Let_NoType", `
[#|let x = 1;|]
[#|let x = 1;
"hello";|]
x;
`);
testExtractFunction("extractFunction_VariableDeclaration_Const_Type", `
[#|const x: number = 1;|]
[#|const x: number = 1;
"hello";|]
x;
`);
testExtractFunction("extractFunction_VariableDeclaration_Const_NoType", `
[#|const x = 1;|]
[#|const x = 1;
"hello";|]
x;
`);
@@ -404,7 +409,8 @@ x; y; z;
`);
testExtractFunction("extractFunction_VariableDeclaration_ConsumedTwice", `
[#|const x: number = 1;|]
[#|const x: number = 1;
"hello";|]
x; x;
`);
+25
View File
@@ -162,6 +162,19 @@ namespace ts {
}|]|]
}
`);
// Variable statements
testExtractRange(`[#|let x = [$|1|];|]`);
testExtractRange(`[#|let x = [$|1|], y;|]`);
testExtractRange(`[#|[$|let x = 1, y = 1;|]|]`);
// Variable declarations
testExtractRange(`let [#|x = [$|1|]|];`);
testExtractRange(`let [#|x = [$|1|]|], y = 2;`);
testExtractRange(`let x = 1, [#|y = [$|2|]|];`);
// Return statements
testExtractRange(`[#|return [$|1|];|]`);
});
testExtractRangeFailed("extractRangeFailed1",
@@ -340,6 +353,18 @@ switch (x) {
refactor.extractSymbol.Messages.CannotExtractRangeContainingConditionalBreakOrContinueStatements.message
]);
testExtractRangeFailed("extractRangeFailed12",
`let [#|x|];`,
[
refactor.extractSymbol.Messages.StatementOrExpressionExpected.message
]);
testExtractRangeFailed("extractRangeFailed13",
`[#|return;|]`,
[
refactor.extractSymbol.Messages.CannotExtractRange.message
]);
testExtractRangeFailed("extract-method-not-for-token-expression-statement", `[#|a|]`, [refactor.extractSymbol.Messages.CannotExtractIdentifier.message]);
});
}
+2 -4
View File
@@ -67,7 +67,8 @@ namespace ts {
}
export const newLineCharacter = "\n";
export function getRuleProvider(action?: (opts: FormatCodeSettings) => void) {
export const getRuleProvider = memoize(getRuleProviderInternal);
function getRuleProviderInternal() {
const options = {
indentSize: 4,
tabSize: 4,
@@ -89,9 +90,6 @@ namespace ts {
placeOpenBraceOnNewLineForFunctions: false,
placeOpenBraceOnNewLineForControlBlocks: false,
};
if (action) {
action(options);
}
const rulesProvider = new formatting.RulesProvider();
rulesProvider.ensureUpToDate(options);
return rulesProvider;
+159 -1
View File
@@ -356,7 +356,7 @@ namespace ts.projectSystem {
}
function checkOpenFiles(projectService: server.ProjectService, expectedFiles: FileOrFolder[]) {
checkFileNames("Open files", projectService.openFiles.map(info => info.fileName), expectedFiles.map(file => file.path));
checkFileNames("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path).fileName), expectedFiles.map(file => file.path));
}
/**
@@ -3893,6 +3893,96 @@ namespace ts.projectSystem {
assert.equal(snap2.getText(0, snap2.getLength()), f1.content, "content should be equal to the content of original file");
});
it("should work when script info doesnt have any project open", () => {
const f1 = {
path: "/a/b/app.ts",
content: "let x = 1"
};
const tmp = {
path: "/a/b/app.tmp",
content: "const y = 42"
};
const host = createServerHost([f1, tmp, libFile]);
const session = createSession(host);
const openContent = "let z = 1";
// send open request
session.executeCommandSeq(<server.protocol.OpenRequest>{
command: server.protocol.CommandTypes.Open,
arguments: { file: f1.path, fileContent: openContent }
});
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { inferredProjects: 1 });
const info = projectService.getScriptInfo(f1.path);
assert.isDefined(info);
checkScriptInfoContents(openContent, "contents set during open request");
// send close request
session.executeCommandSeq(<server.protocol.CloseRequest>{
command: server.protocol.CommandTypes.Close,
arguments: { file: f1.path }
});
checkScriptInfoAndProjects(0, f1.content, "contents of closed file");
// Can reload contents of the file when its not open and has no project
// reload from temp file
session.executeCommandSeq(<server.protocol.ReloadRequest>{
command: server.protocol.CommandTypes.Reload,
arguments: { file: f1.path, tmpfile: tmp.path }
});
checkScriptInfoAndProjects(0, tmp.content, "contents of temp file");
// reload from own file
session.executeCommandSeq(<server.protocol.ReloadRequest>{
command: server.protocol.CommandTypes.Reload,
arguments: { file: f1.path }
});
checkScriptInfoAndProjects(0, f1.content, "contents of closed file");
// Open file again without setting its content
session.executeCommandSeq(<server.protocol.OpenRequest>{
command: server.protocol.CommandTypes.Open,
arguments: { file: f1.path }
});
checkScriptInfoAndProjects(1, f1.content, "contents of file when opened without specifying contents");
const snap = info.getSnapshot();
// send close request
session.executeCommandSeq(<server.protocol.CloseRequest>{
command: server.protocol.CommandTypes.Close,
arguments: { file: f1.path }
});
checkScriptInfoAndProjects(0, f1.content, "contents of closed file");
assert.strictEqual(info.getSnapshot(), snap);
// reload from temp file
session.executeCommandSeq(<server.protocol.ReloadRequest>{
command: server.protocol.CommandTypes.Reload,
arguments: { file: f1.path, tmpfile: tmp.path }
});
checkScriptInfoAndProjects(0, tmp.content, "contents of temp file");
assert.notStrictEqual(info.getSnapshot(), snap);
// reload from own file
session.executeCommandSeq(<server.protocol.ReloadRequest>{
command: server.protocol.CommandTypes.Reload,
arguments: { file: f1.path }
});
checkScriptInfoAndProjects(0, f1.content, "contents of closed file");
assert.notStrictEqual(info.getSnapshot(), snap);
function checkScriptInfoAndProjects(inferredProjects: number, contentsOfInfo: string, captionForContents: string) {
checkNumberOfProjects(projectService, { inferredProjects });
assert.strictEqual(projectService.getScriptInfo(f1.path), info);
checkScriptInfoContents(contentsOfInfo, captionForContents);
}
function checkScriptInfoContents(contentsOfInfo: string, captionForContents: string) {
const snap = info.getSnapshot();
assert.equal(snap.getText(0, snap.getLength()), contentsOfInfo, "content should be equal to " + captionForContents);
}
});
});
describe("Inferred projects", () => {
@@ -4293,6 +4383,74 @@ namespace ts.projectSystem {
checkNumberOfConfiguredProjects(service, 1);
checkNumberOfInferredProjects(service, 0);
});
it("should use projectRootPath when searching for inferred project again", () => {
const projectDir = "/a/b/projects/project";
const configFileLocation = `${projectDir}/src`;
const f1 = {
path: `${configFileLocation}/file1.ts`,
content: ""
};
const configFile = {
path: `${configFileLocation}/tsconfig.json`,
content: "{}"
};
const configFile2 = {
path: "/a/b/projects/tsconfig.json",
content: "{}"
};
const host = createServerHost([f1, libFile, configFile, configFile2]);
const service = createProjectService(host);
service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir);
checkNumberOfProjects(service, { configuredProjects: 1 });
assert.isDefined(service.configuredProjects.get(configFile.path));
checkWatchedFiles(host, [libFile.path, configFile.path]);
checkWatchedDirectories(host, [], /*recursive*/ false);
const typeRootLocations = getTypeRootsFromLocation(configFileLocation);
checkWatchedDirectories(host, typeRootLocations.concat(configFileLocation), /*recursive*/ true);
// Delete config file - should create inferred project and not configured project
host.reloadFS([f1, libFile, configFile2]);
host.runQueuedTimeoutCallbacks();
checkNumberOfProjects(service, { inferredProjects: 1 });
checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]);
checkWatchedDirectories(host, [], /*recursive*/ false);
checkWatchedDirectories(host, typeRootLocations, /*recursive*/ true);
});
it("should use projectRootPath when searching for inferred project again 2", () => {
const projectDir = "/a/b/projects/project";
const configFileLocation = `${projectDir}/src`;
const f1 = {
path: `${configFileLocation}/file1.ts`,
content: ""
};
const configFile = {
path: `${configFileLocation}/tsconfig.json`,
content: "{}"
};
const configFile2 = {
path: "/a/b/projects/tsconfig.json",
content: "{}"
};
const host = createServerHost([f1, libFile, configFile, configFile2]);
const service = createProjectService(host, { useSingleInferredProject: true }, { useInferredProjectPerProjectRoot: true });
service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir);
checkNumberOfProjects(service, { configuredProjects: 1 });
assert.isDefined(service.configuredProjects.get(configFile.path));
checkWatchedFiles(host, [libFile.path, configFile.path]);
checkWatchedDirectories(host, [], /*recursive*/ false);
checkWatchedDirectories(host, getTypeRootsFromLocation(configFileLocation).concat(configFileLocation), /*recursive*/ true);
// Delete config file - should create inferred project with project root path set
host.reloadFS([f1, libFile, configFile2]);
host.runQueuedTimeoutCallbacks();
checkNumberOfProjects(service, { inferredProjects: 1 });
assert.equal(service.inferredProjects[0].projectRootPath, projectDir);
checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]);
checkWatchedDirectories(host, [], /*recursive*/ false);
checkWatchedDirectories(host, getTypeRootsFromLocation(projectDir), /*recursive*/ true);
});
});
describe("cancellationToken", () => {
+9 -2
View File
@@ -1,10 +1,17 @@
/// <reference path="harness.ts" />
namespace ts.TestFSWithWatch {
const { content: libFileContent } = Harness.getDefaultLibraryFile(Harness.IO);
export const libFile: FileOrFolder = {
path: "/a/lib/lib.d.ts",
content: libFileContent
content: `/// <reference no-default-lib="true"/>
interface Boolean {}
interface Function {}
interface IArguments {}
interface Number { toExponential: any; }
interface Object {}
interface RegExp {}
interface String { charAt: any; }
interface Array<T> {}`
};
export const safeList = {
+41 -35
View File
@@ -352,9 +352,9 @@ namespace ts.server {
*/
readonly configuredProjects = createMap<ConfiguredProject>();
/**
* list of open files
* Open files: with value being project root path, and key being Path of the file that is open
*/
readonly openFiles: ScriptInfo[] = [];
readonly openFiles = createMap<NormalizedPath>();
private compilerOptionsForInferredProjects: CompilerOptions;
private compilerOptionsForInferredProjectsPerProjectRoot = createMap<CompilerOptions>();
@@ -582,7 +582,7 @@ namespace ts.server {
const event: ProjectsUpdatedInBackgroundEvent = {
eventName: ProjectsUpdatedInBackgroundEvent,
data: {
openFiles: this.openFiles.map(f => f.fileName)
openFiles: arrayFrom(this.openFiles.keys(), path => this.getScriptInfoForPath(path as Path).fileName)
}
};
this.eventHandler(event);
@@ -891,7 +891,7 @@ namespace ts.server {
}
/*@internal*/
assignOrphanScriptInfoToInferredProject(info: ScriptInfo, projectRootPath?: string) {
assignOrphanScriptInfoToInferredProject(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) {
Debug.assert(info.isOrphan());
const project = this.getOrCreateInferredProjectForProjectRootPathIfEnabled(info, projectRootPath) ||
@@ -935,7 +935,7 @@ namespace ts.server {
info.close();
this.stopWatchingConfigFilesForClosedScriptInfo(info);
unorderedRemoveItem(this.openFiles, info);
this.openFiles.delete(info.path);
const fileExists = this.host.fileExists(info.fileName);
@@ -974,11 +974,12 @@ namespace ts.server {
}
// collect orphaned files and assign them to inferred project just like we treat open of a file
for (const f of this.openFiles) {
this.openFiles.forEach((projectRootPath, path) => {
const f = this.getScriptInfoForPath(path as Path);
if (f.isOrphan()) {
this.assignOrphanScriptInfoToInferredProject(f);
this.assignOrphanScriptInfoToInferredProject(f, projectRootPath);
}
}
});
// Cleanup script infos that arent part of any project (eg. those could be closed script infos not referenced by any project)
// is postponed to next file open so that if file from same project is opened,
@@ -1172,7 +1173,7 @@ namespace ts.server {
* This is called by inferred project whenever script info is added as a root
*/
/* @internal */
startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) {
startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) {
Debug.assert(info.isScriptOpen());
this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => {
let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
@@ -1194,7 +1195,7 @@ namespace ts.server {
!this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) {
this.createConfigFileWatcherOfConfigFileExistence(configFileName, canonicalConfigFilePath, configFileExistenceInfo);
}
});
}, projectRootPath);
}
/**
@@ -1262,7 +1263,7 @@ namespace ts.server {
* The server must start searching from the directory containing
* the newly opened file.
*/
private getConfigFileNameForFile(info: ScriptInfo, projectRootPath?: NormalizedPath) {
private getConfigFileNameForFile(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) {
Debug.assert(info.isScriptOpen());
this.logger.info(`Search path: ${getDirectoryPath(info.fileName)}`);
const configFileName = this.forEachConfigFileLocation(info,
@@ -1301,9 +1302,9 @@ namespace ts.server {
printProjects(this.inferredProjects, counter);
this.logger.info("Open files: ");
for (const rootFile of this.openFiles) {
this.logger.info(`\t${rootFile.fileName}`);
}
this.openFiles.forEach((projectRootPath, path) => {
this.logger.info(`\tFileName: ${this.getScriptInfoForPath(path as Path).fileName} ProjectRootPath: ${projectRootPath}`);
});
this.logger.endGroup();
}
@@ -1605,7 +1606,7 @@ namespace ts.server {
});
}
private getOrCreateInferredProjectForProjectRootPathIfEnabled(info: ScriptInfo, projectRootPath: string | undefined): InferredProject | undefined {
private getOrCreateInferredProjectForProjectRootPathIfEnabled(info: ScriptInfo, projectRootPath: NormalizedPath | undefined): InferredProject | undefined {
if (!this.useInferredProjectPerProjectRoot) {
return undefined;
}
@@ -1659,7 +1660,7 @@ namespace ts.server {
return this.createInferredProject(/*currentDirectory*/ undefined, /*isSingleInferredProject*/ true);
}
private createInferredProject(currentDirectory: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: string): InferredProject {
private createInferredProject(currentDirectory: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: NormalizedPath): InferredProject {
const compilerOptions = projectRootPath && this.compilerOptionsForInferredProjectsPerProjectRoot.get(projectRootPath) || this.compilerOptionsForInferredProjects;
const project = new InferredProject(this, this.documentRegistry, compilerOptions, projectRootPath, currentDirectory);
if (isSingleInferredProject) {
@@ -1796,23 +1797,19 @@ namespace ts.server {
// as there is no need to load contents of the files from the disk
// Reload Projects
this.reloadConfiguredProjectForFiles(this.openFiles, /*delayReload*/ false);
this.reloadConfiguredProjectForFiles(this.openFiles, /*delayReload*/ false, returnTrue);
this.refreshInferredProjects();
}
private delayReloadConfiguredProjectForFiles(configFileExistenceInfo: ConfigFileExistenceInfo, ignoreIfNotRootOfInferredProject: boolean) {
// Get open files to reload projects for
const openFiles = mapDefinedIter(
configFileExistenceInfo.openFilesImpactedByConfigFile.entries(),
([path, isRootOfInferredProject]) => {
if (!ignoreIfNotRootOfInferredProject || isRootOfInferredProject) {
const info = this.getScriptInfoForPath(path as Path);
Debug.assert(!!info);
return info;
}
}
this.reloadConfiguredProjectForFiles(
configFileExistenceInfo.openFilesImpactedByConfigFile,
/*delayReload*/ true,
ignoreIfNotRootOfInferredProject ?
isRootOfInferredProject => isRootOfInferredProject : // Reload open files if they are root of inferred project
returnTrue // Reload all the open files impacted by config file
);
this.reloadConfiguredProjectForFiles(openFiles, /*delayReload*/ true);
this.delayInferredProjectsRefresh();
}
@@ -1821,16 +1818,24 @@ namespace ts.server {
* If the config file is found and it refers to existing project, it reloads it either immediately
* or schedules it for reload depending on delayReload option
* If the there is no existing project it just opens the configured project for the config file
* reloadForInfo provides a way to filter out files to reload configured project for
*/
private reloadConfiguredProjectForFiles(openFiles: ReadonlyArray<ScriptInfo>, delayReload: boolean) {
private reloadConfiguredProjectForFiles<T>(openFiles: Map<T>, delayReload: boolean, shouldReloadProjectFor: (openFileValue: T) => boolean) {
const updatedProjects = createMap<true>();
// try to reload config file for all open files
for (const info of openFiles) {
openFiles.forEach((openFileValue, path) => {
// Filter out the files that need to be ignored
if (!shouldReloadProjectFor(openFileValue)) {
return;
}
const info = this.getScriptInfoForPath(path as Path);
Debug.assert(info.isScriptOpen());
// This tries to search for a tsconfig.json for the given file. If we found it,
// we first detect if there is already a configured project created for it: if so,
// we re- read the tsconfig file content and update the project only if we havent already done so
// otherwise we create a new one.
const configFileName = this.getConfigFileNameForFile(info);
const configFileName = this.getConfigFileNameForFile(info, this.openFiles.get(path));
if (configFileName) {
const project = this.findConfiguredProjectByProjectName(configFileName);
if (!project) {
@@ -1848,7 +1853,7 @@ namespace ts.server {
updatedProjects.set(configFileName, true);
}
}
}
});
}
/**
@@ -1893,16 +1898,17 @@ namespace ts.server {
this.logger.info("refreshInferredProjects: updating project structure from ...");
this.printProjects();
for (const info of this.openFiles) {
this.openFiles.forEach((projectRootPath, path) => {
const info = this.getScriptInfoForPath(path as Path);
// collect all orphaned script infos from open files
if (info.isOrphan()) {
this.assignOrphanScriptInfoToInferredProject(info);
this.assignOrphanScriptInfoToInferredProject(info, projectRootPath);
}
else {
// Or remove the root of inferred project if is referenced in more than one projects
this.removeRootOfInferredProjectIfNowPartOfOtherProject(info);
}
}
});
for (const p of this.inferredProjects) {
p.updateGraph();
@@ -1956,7 +1962,7 @@ namespace ts.server {
this.assignOrphanScriptInfoToInferredProject(info, projectRootPath);
}
Debug.assert(!info.isOrphan());
this.openFiles.push(info);
this.openFiles.set(info.path, projectRootPath);
if (sendConfigFileDiagEvent) {
configFileErrors = project.getAllProjectErrors();
+3 -12
View File
@@ -946,16 +946,6 @@ namespace ts.server {
}
}
reloadScript(filename: NormalizedPath, tempFileName?: NormalizedPath): boolean {
const script = this.projectService.getScriptInfoForNormalizedPath(filename);
if (script) {
Debug.assert(script.isAttached(this));
script.reloadFromFile(tempFileName);
return true;
}
return false;
}
/* @internal */
getChangesSinceVersion(lastKnownVersion?: number): ProjectFilesWithTSDiagnostics {
this.updateGraph();
@@ -1064,7 +1054,7 @@ namespace ts.server {
projectService: ProjectService,
documentRegistry: DocumentRegistry,
compilerOptions: CompilerOptions,
projectRootPath: string | undefined,
projectRootPath: NormalizedPath | undefined,
currentDirectory: string | undefined) {
super(InferredProject.newName(),
ProjectKind.Inferred,
@@ -1080,7 +1070,8 @@ namespace ts.server {
}
addRoot(info: ScriptInfo) {
this.projectService.startWatchingConfigFilesForInferredProjectRoot(info);
Debug.assert(info.isScriptOpen());
this.projectService.startWatchingConfigFilesForInferredProjectRoot(info, this.projectService.openFiles.get(info.path));
if (!this._isJsInferredProject && info.isJavaScript()) {
this.toggleJsInferredProject(/*isJsInferredProject*/ true);
}
+15 -1
View File
@@ -1625,7 +1625,12 @@ namespace ts.server.protocol {
/**
* Names of one or more entries for which to obtain details.
*/
entryNames: string[];
entryNames: (string | CompletionEntryIdentifier)[];
}
export interface CompletionEntryIdentifier {
name: string;
source: string;
}
/**
@@ -1685,6 +1690,10 @@ namespace ts.server.protocol {
* made to avoid errors. The CompletionEntryDetails will have these actions.
*/
hasAction?: true;
/**
* Identifier (not necessarily human-readable) identifying where this completion came from.
*/
source?: string;
}
/**
@@ -1722,6 +1731,11 @@ namespace ts.server.protocol {
* The associated code actions for this entry
*/
codeActions?: CodeAction[];
/**
* Human-readable description of the `source` from the CompletionEntry.
*/
source?: SymbolDisplayPart[];
}
export interface CompletionsResponse extends Response {
+24 -22
View File
@@ -67,7 +67,10 @@ namespace ts.server {
this.lineMap = undefined;
}
/** returns true if text changed */
/**
* Set the contents as newText
* returns true if text changed
*/
public reload(newText: string) {
Debug.assert(newText !== undefined);
@@ -87,31 +90,31 @@ namespace ts.server {
}
}
/** returns true if text changed */
public reloadFromDisk() {
let reloaded = false;
if (!this.pendingReloadFromDisk && !this.ownFileText) {
reloaded = this.reload(this.getFileText());
this.ownFileText = true;
}
/**
* Reads the contents from tempFile(if supplied) or own file and sets it as contents
* returns true if text changed
*/
public reloadWithFileText(tempFileName?: string) {
const reloaded = this.reload(this.getFileText(tempFileName));
this.ownFileText = !tempFileName || tempFileName === this.fileName;
return reloaded;
}
/**
* Reloads the contents from the file if there is no pending reload from disk or the contents of file are same as file text
* returns true if text changed
*/
public reloadFromDisk() {
if (!this.pendingReloadFromDisk && !this.ownFileText) {
return this.reloadWithFileText();
}
return false;
}
public delayReloadFromFileIntoText() {
this.pendingReloadFromDisk = true;
}
/** returns true if text changed */
public reloadFromFile(tempFileName: string) {
let reloaded = false;
// Reload if different file or we dont know if we are working with own file text
if (tempFileName !== this.fileName || !this.ownFileText) {
reloaded = this.reload(this.getFileText(tempFileName));
this.ownFileText = !tempFileName || tempFileName === this.fileName;
}
return reloaded;
}
public getSnapshot(): IScriptSnapshot {
return this.useScriptVersionCacheIfValidOrOpen()
? this.svc.getSnapshot()
@@ -180,8 +183,7 @@ namespace ts.server {
private getOrLoadText() {
if (this.text === undefined || this.pendingReloadFromDisk) {
Debug.assert(!this.svc || this.pendingReloadFromDisk, "ScriptVersionCache should not be set when reloading from disk");
this.reload(this.getFileText());
this.ownFileText = true;
this.reloadWithFileText();
}
return this.text;
}
@@ -385,7 +387,7 @@ namespace ts.server {
this.markContainingProjectsAsDirty();
}
else {
if (this.textStorage.reloadFromFile(tempFileName)) {
if (this.textStorage.reloadWithFileText(tempFileName)) {
this.markContainingProjectsAsDirty();
}
}
+12 -9
View File
@@ -1188,10 +1188,10 @@ namespace ts.server {
if (simplifiedResult) {
return mapDefined<CompletionEntry, protocol.CompletionEntry>(completions && completions.entries, entry => {
if (completions.isMemberCompletion || (entry.name.toLowerCase().indexOf(prefix.toLowerCase()) === 0)) {
const { name, kind, kindModifiers, sortText, replacementSpan, hasAction } = entry;
const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source } = entry;
const convertedSpan = replacementSpan ? this.decorateSpan(replacementSpan, scriptInfo) : undefined;
// Use `hasAction || undefined` to avoid serializing `false`.
return { name, kind, kindModifiers, sortText, replacementSpan: convertedSpan, hasAction: hasAction || undefined };
return { name, kind, kindModifiers, sortText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source };
}
}).sort((a, b) => compareStrings(a.name, b.name));
}
@@ -1206,8 +1206,9 @@ namespace ts.server {
const position = this.getPosition(args, scriptInfo);
const formattingOptions = project.projectService.getFormatCodeOptions(file);
return mapDefined(args.entryNames, entryName => {
const details = project.getLanguageService().getCompletionEntryDetails(file, position, entryName, formattingOptions);
return mapDefined<string | protocol.CompletionEntryIdentifier, protocol.CompletionEntryDetails>(args.entryNames, entryName => {
const { name, source } = typeof entryName === "string" ? { name: entryName, source: undefined } : entryName;
const details = project.getLanguageService().getCompletionEntryDetails(file, position, name, formattingOptions, source);
if (details) {
const mappedCodeActions = map(details.codeActions, action => this.mapCodeAction(action, scriptInfo));
return { ...details, codeActions: mappedCodeActions };
@@ -1311,11 +1312,13 @@ namespace ts.server {
private reload(args: protocol.ReloadRequestArgs, reqSeq: number) {
const file = toNormalizedPath(args.file);
const tempFileName = args.tmpfile && toNormalizedPath(args.tmpfile);
const project = this.projectService.getDefaultProjectForFile(file, /*ensureProject*/ true);
this.changeSeq++;
// make sure no changes happen before this one is finished
if (project.reloadScript(file, tempFileName)) {
this.doOutput(/*info*/ undefined, CommandNames.Reload, reqSeq, /*success*/ true);
const info = this.projectService.getScriptInfoForNormalizedPath(file);
if (info) {
this.changeSeq++;
// make sure no changes happen before this one is finished
if (info.reloadFromFile(tempFileName)) {
this.doOutput(/*info*/ undefined, CommandNames.Reload, reqSeq, /*success*/ true);
}
}
}
+12 -7
View File
@@ -249,7 +249,11 @@ namespace ts.codefix {
const lastImportDeclaration = findLast(sourceFile.statements, isAnyImportSyntax);
const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier);
const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createImportClauseOfKind(kind, symbolName), createStringLiteralWithQuoteStyle(sourceFile, moduleSpecifierWithoutQuotes));
const importDecl = createImportDeclaration(
/*decorators*/ undefined,
/*modifiers*/ undefined,
createImportClauseOfKind(kind, symbolName),
createStringLiteralWithQuoteStyle(sourceFile, moduleSpecifierWithoutQuotes));
const changes = ChangeTracker.with(context, changeTracker => {
if (lastImportDeclaration) {
changeTracker.insertNodeAfter(sourceFile, lastImportDeclaration, importDecl, { suffix: newLineCharacter });
@@ -279,13 +283,14 @@ namespace ts.codefix {
}
function createImportClauseOfKind(kind: ImportKind, symbolName: string) {
const id = createIdentifier(symbolName);
switch (kind) {
case ImportKind.Default:
return createImportClause(createIdentifier(symbolName), /*namedBindings*/ undefined);
return createImportClause(id, /*namedBindings*/ undefined);
case ImportKind.Namespace:
return createImportClause(/*name*/ undefined, createNamespaceImport(createIdentifier(symbolName)));
return createImportClause(/*name*/ undefined, createNamespaceImport(id));
case ImportKind.Named:
return createImportClause(/*name*/ undefined, createNamedImports([createImportSpecifier(/*propertyName*/ undefined, createIdentifier(symbolName))]));
return createImportClause(/*name*/ undefined, createNamedImports([createImportSpecifier(/*propertyName*/ undefined, id)]));
default:
Debug.assertNever(kind);
}
@@ -529,7 +534,7 @@ namespace ts.codefix {
declarations: ReadonlyArray<AnyImportSyntax>): ImportCodeAction {
const fromExistingImport = firstDefined(declarations, declaration => {
if (declaration.kind === SyntaxKind.ImportDeclaration && declaration.importClause) {
const changes = tryUpdateExistingImport(ctx, ctx.kind, declaration.importClause);
const changes = tryUpdateExistingImport(ctx, declaration.importClause);
if (changes) {
const moduleSpecifierWithoutQuotes = stripQuotes(declaration.moduleSpecifier.getText());
return createCodeAction(
@@ -559,8 +564,8 @@ namespace ts.codefix {
return expression && isStringLiteral(expression) ? expression.text : undefined;
}
function tryUpdateExistingImport(context: SymbolContext, kind: ImportKind, importClause: ImportClause): FileTextChanges[] | undefined {
const { symbolName, sourceFile } = context;
function tryUpdateExistingImport(context: SymbolContext & { kind: ImportKind }, importClause: ImportClause): FileTextChanges[] | undefined {
const { symbolName, sourceFile, kind } = context;
const { name, namedBindings } = importClause;
switch (kind) {
case ImportKind.Default:
+132 -69
View File
@@ -12,7 +12,7 @@ namespace ts.Completions {
* Map from symbol id -> SymbolOriginInfo.
* Only populated for symbols that come from other modules.
*/
type SymbolOriginInfoMap = SymbolOriginInfo[];
type SymbolOriginInfoMap = (SymbolOriginInfo | undefined)[];
const enum KeywordCompletionFilters {
None,
@@ -100,7 +100,7 @@ namespace ts.Completions {
function getJavaScriptCompletionEntries(
sourceFile: SourceFile,
position: number,
uniqueNames: Map<true>,
uniqueNames: Map<{}>,
target: ScriptTarget,
entries: Push<CompletionEntry>): void {
getNameTable(sourceFile).forEach((pos, name) => {
@@ -127,7 +127,15 @@ namespace ts.Completions {
});
}
function createCompletionEntry(symbol: Symbol, location: Node, performCharacterChecks: boolean, typeChecker: TypeChecker, target: ScriptTarget, allowStringLiteral: boolean): CompletionEntry {
function createCompletionEntry(
symbol: Symbol,
location: Node,
performCharacterChecks: boolean,
typeChecker: TypeChecker,
target: ScriptTarget,
allowStringLiteral: boolean,
origin: SymbolOriginInfo,
): CompletionEntry | undefined {
// Try to get a valid display name for this symbol, if we could not find one, then ignore it.
// We would like to only show things that can be added after a dot, so for instance numeric properties can
// not be accessed with a dot (a.1 <- invalid)
@@ -149,9 +157,15 @@ namespace ts.Completions {
kind: SymbolDisplay.getSymbolKind(typeChecker, symbol, location),
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
sortText: "0",
source: getSourceFromOrigin(origin),
hasAction: origin === undefined ? undefined : true,
};
}
function getSourceFromOrigin(origin: SymbolOriginInfo | undefined): string | undefined {
return origin && stripQuotes(origin.moduleSymbol.name);
}
function getCompletionEntriesFromSymbols(
symbols: ReadonlyArray<Symbol>,
entries: Push<CompletionEntry>,
@@ -164,25 +178,35 @@ namespace ts.Completions {
symbolToOriginInfoMap?: SymbolOriginInfoMap,
): Map<true> {
const start = timestamp();
const uniqueNames = createMap<true>();
// Tracks unique names.
// We don't set this for global variables or completions from external module exports, because we can have multiple of those.
// Based on the order we add things we will always see locals first, then globals, then module exports.
// So adding a completion for a local will prevent us from adding completions for external module exports sharing the same name.
const uniques = createMap<true>();
if (symbols) {
for (const symbol of symbols) {
const entry = createCompletionEntry(symbol, location, performCharacterChecks, typeChecker, target, allowStringLiteral);
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);
}
const origin = symbolToOriginInfoMap ? symbolToOriginInfoMap[getSymbolId(symbol)] : undefined;
const entry = createCompletionEntry(symbol, location, performCharacterChecks, typeChecker, target, allowStringLiteral, origin);
if (!entry) {
continue;
}
const { name } = entry;
if (uniques.has(name)) {
continue;
}
// Latter case tests whether this is a global variable.
if (!origin && !(symbol.parent === undefined && !some(symbol.declarations, d => d.getSourceFile() === location.getSourceFile()))) {
uniques.set(name, true);
}
entries.push(entry);
}
}
log("getCompletionsAtPosition: getCompletionEntriesFromSymbols: " + (timestamp() - start));
return uniqueNames;
return uniques;
}
function getStringLiteralCompletionEntries(sourceFile: SourceFile, position: number, typeChecker: TypeChecker, compilerOptions: CompilerOptions, host: LanguageServiceHost, log: Log): CompletionInfo | undefined {
@@ -329,59 +353,104 @@ namespace ts.Completions {
}
}
function getSymbolCompletionFromEntryId(
typeChecker: TypeChecker,
log: (message: string) => void,
compilerOptions: CompilerOptions,
sourceFile: SourceFile,
position: number,
{ name, source }: CompletionEntryIdentifier,
allSourceFiles: ReadonlyArray<SourceFile>,
): { type: "symbol", symbol: Symbol, location: Node, symbolToOriginInfoMap: SymbolOriginInfoMap } | { type: "request", request: Request } | { type: "none" } {
const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles);
if (!completionData) {
return { type: "none" };
}
const { symbols, location, allowStringLiteral, symbolToOriginInfoMap, request } = completionData;
if (request) {
return { type: "request", request };
}
// 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 = find(symbols, s =>
getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral) === name
&& getSourceFromOrigin(symbolToOriginInfoMap[getSymbolId(s)]) === source);
return symbol ? { type: "symbol", symbol, location, symbolToOriginInfoMap } : { type: "none" };
}
export interface CompletionEntryIdentifier {
name: string;
source?: string;
}
export function getCompletionEntryDetails(
typeChecker: TypeChecker,
log: (message: string) => void,
compilerOptions: CompilerOptions,
sourceFile: SourceFile,
position: number,
name: string,
entryId: CompletionEntryIdentifier,
allSourceFiles: ReadonlyArray<SourceFile>,
host: LanguageServiceHost,
rulesProvider: formatting.RulesProvider,
): CompletionEntryDetails {
const { name, source } = entryId;
// Compute all the completion symbols again.
const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles);
if (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 = find(symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral) === name);
if (symbol) {
const symbolCompletion = getSymbolCompletionFromEntryId(typeChecker, log, compilerOptions, sourceFile, position, entryId, allSourceFiles);
switch (symbolCompletion.type) {
case "request": {
const { request } = symbolCompletion;
switch (request.kind) {
case "JsDocTagName":
return JsDoc.getJSDocTagNameCompletionDetails(name);
case "JsDocTag":
return JsDoc.getJSDocTagCompletionDetails(name);
case "JsDocParameterName":
return JsDoc.getJSDocParameterNameCompletionDetails(name);
default:
return Debug.assertNever(request);
}
}
case "symbol": {
const { symbol, location, symbolToOriginInfoMap } = symbolCompletion;
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, kindModifiers, kind: symbolKind, displayParts, documentation, tags, codeActions };
return { name, kindModifiers, kind: symbolKind, displayParts, documentation, tags, codeActions, source: source === undefined ? undefined : [textPart(source)] };
}
case "none": {
// Didn't find a symbol with this name. See if we can find a keyword instead.
if (some(getKeywordCompletions(KeywordCompletionFilters.None), c => c.name === name)) {
return {
name,
kind: ScriptElementKind.keyword,
kindModifiers: ScriptElementKindModifier.none,
displayParts: [displayPart(name, SymbolDisplayPartKind.keyword)],
documentation: undefined,
tags: undefined,
codeActions: undefined,
source: undefined,
};
}
return undefined;
}
}
// 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 === name
);
if (keywordCompletion) {
return {
name,
kind: ScriptElementKind.keyword,
kindModifiers: ScriptElementKindModifier.none,
displayParts: [displayPart(name, SymbolDisplayPartKind.keyword)],
documentation: undefined,
tags: undefined,
codeActions: undefined,
};
}
return 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)];
function getCompletionEntryCodeActions(
symbolToOriginInfoMap: SymbolOriginInfoMap,
symbol: Symbol,
checker: TypeChecker,
host: LanguageServiceHost,
compilerOptions: CompilerOptions,
sourceFile: SourceFile,
rulesProvider: formatting.RulesProvider,
): CodeAction[] | undefined {
const symbolOriginInfo = symbolToOriginInfoMap[getSymbolId(symbol)];
if (!symbolOriginInfo) {
return undefined;
}
@@ -407,29 +476,20 @@ namespace ts.Completions {
compilerOptions: CompilerOptions,
sourceFile: SourceFile,
position: number,
entryName: string,
entryId: CompletionEntryIdentifier,
allSourceFiles: ReadonlyArray<SourceFile>,
): Symbol | undefined {
// Compute all the completion symbols again.
const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles);
if (!completionData) {
return undefined;
}
const { symbols, allowStringLiteral } = 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.
return find(symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral) === entryName);
const completion = getSymbolCompletionFromEntryId(typeChecker, log, compilerOptions, sourceFile, position, entryId, allSourceFiles);
return completion.type === "symbol" ? completion.symbol : undefined;
}
interface CompletionData {
symbols: Symbol[];
symbols: ReadonlyArray<Symbol>;
isGlobalCompletion: boolean;
isMemberCompletion: boolean;
allowStringLiteral: boolean;
isNewIdentifierLocation: boolean;
location: Node;
location: Node | undefined;
isRightOfDot: boolean;
request?: Request;
keywordFilters: KeywordCompletionFilters;
@@ -516,7 +576,7 @@ namespace ts.Completions {
if (request) {
return {
symbols: undefined,
symbols: emptyArray,
isGlobalCompletion: false,
isMemberCompletion: false,
allowStringLiteral: false,
@@ -923,7 +983,6 @@ namespace ts.Completions {
function getSymbolsFromOtherSourceFileExports(symbols: Symbol[], tokenText: string): void {
const tokenTextLowerCase = tokenText.toLowerCase();
const symbolIdMap = arrayToNumericMap(symbols, s => getUniqueSymbolId(s, typeChecker));
codefix.forEachExternalModule(typeChecker, allSourceFiles, moduleSymbol => {
if (moduleSymbol === sourceFile.symbol) {
@@ -941,10 +1000,14 @@ namespace ts.Completions {
}
}
const id = getUniqueSymbolId(symbol, typeChecker);
if (!symbolIdMap[id] && stringContainsCharactersInOrder(name.toLowerCase(), tokenTextLowerCase)) {
if (symbol.declarations && symbol.declarations.some(d => isExportSpecifier(d) && !!d.parent.parent.moduleSpecifier)) {
// Don't add a completion for a re-export, only for the original.
continue;
}
if (stringContainsCharactersInOrder(name.toLowerCase(), tokenTextLowerCase)) {
symbols.push(symbol);
symbolToOriginInfoMap[id] = { moduleSymbol, isDefaultExport };
symbolToOriginInfoMap[getSymbolId(symbol)] = { moduleSymbol, isDefaultExport };
}
}
});
+26
View File
@@ -108,6 +108,8 @@ namespace ts.JsDoc {
}));
}
export const getJSDocTagNameCompletionDetails = getJSDocTagCompletionDetails;
export function getJSDocTagCompletions(): CompletionEntry[] {
return jsDocTagCompletionEntries || (jsDocTagCompletionEntries = ts.map(jsDocTagNames, tagName => {
return {
@@ -119,6 +121,18 @@ namespace ts.JsDoc {
}));
}
export function getJSDocTagCompletionDetails(name: string): CompletionEntryDetails {
return {
name,
kind: ScriptElementKind.unknown, // TODO: should have its own kind?
kindModifiers: "",
displayParts: [textPart(name)],
documentation: emptyArray,
tags: emptyArray,
codeActions: undefined,
};
}
export function getJSDocParameterNameCompletions(tag: JSDocParameterTag): CompletionEntry[] {
if (!isIdentifier(tag.name)) {
return emptyArray;
@@ -141,6 +155,18 @@ namespace ts.JsDoc {
});
}
export function getJSDocParameterNameCompletionDetails(name: string): CompletionEntryDetails {
return {
name,
kind: ScriptElementKind.parameterElement,
kindModifiers: "",
displayParts: [textPart(name)],
documentation: emptyArray,
tags: emptyArray,
codeActions: undefined,
};
}
/**
* Checks if position points to a valid position to add JSDoc comments, and if so,
* returns the appropriate template. Otherwise returns an empty string.
+42 -2
View File
@@ -244,12 +244,52 @@ namespace ts.refactor.extractSymbol {
return { targetRange: { range: statements, facts: rangeFacts, declarations } };
}
if (isReturnStatement(start) && !start.expression) {
// Makes no sense to extract an expression-less return statement.
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.CannotExtractRange)] };
}
// We have a single node (start)
const errors = checkRootNode(start) || checkNode(start);
const node = refineNode(start);
const errors = checkRootNode(node) || checkNode(node);
if (errors) {
return { errors };
}
return { targetRange: { range: getStatementOrExpressionRange(start), facts: rangeFacts, declarations } };
return { targetRange: { range: getStatementOrExpressionRange(node), facts: rangeFacts, declarations } };
/**
* Attempt to refine the extraction node (generally, by shrinking it) to produce better results.
* @param node The unrefined extraction node.
*/
function refineNode(node: Node) {
if (isReturnStatement(node)) {
if (node.expression) {
return node.expression;
}
}
else if (isVariableStatement(node)) {
let numInitializers = 0;
let lastInitializer: Expression | undefined = undefined;
for (const declaration of node.declarationList.declarations) {
if (declaration.initializer) {
numInitializers++;
lastInitializer = declaration.initializer;
}
}
if (numInitializers === 1) {
return lastInitializer;
}
// No special handling if there are multiple initializers.
}
else if (isVariableDeclaration(node)) {
if (node.initializer) {
return node.initializer;
}
}
return node;
}
function checkRootNode(node: Node): Diagnostic[] | undefined {
if (isIdentifier(isExpressionStatement(node) ? node.expression : node)) {
+19 -4
View File
@@ -1327,16 +1327,31 @@ namespace ts {
return Completions.getCompletionsAtPosition(host, program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position, program.getSourceFiles());
}
function getCompletionEntryDetails(fileName: string, position: number, entryName: string, formattingOptions?: FormatCodeSettings): CompletionEntryDetails {
function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions?: FormatCodeSettings, source?: string): CompletionEntryDetails {
synchronizeHostData();
const ruleProvider = formattingOptions ? getRuleProvider(formattingOptions) : undefined;
return Completions.getCompletionEntryDetails(
program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position, entryName, program.getSourceFiles(), host, ruleProvider);
program.getTypeChecker(),
log,
program.getCompilerOptions(),
getValidSourceFile(fileName),
position,
{ name, source },
program.getSourceFiles(),
host,
ruleProvider);
}
function getCompletionEntrySymbol(fileName: string, position: number, entryName: string): Symbol {
function getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string): Symbol {
synchronizeHostData();
return Completions.getCompletionEntrySymbol(program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position, entryName, program.getSourceFiles());
return Completions.getCompletionEntrySymbol(
program.getTypeChecker(),
log,
program.getCompilerOptions(),
getValidSourceFile(fileName),
position,
{ name, source },
program.getSourceFiles());
}
function getQuickInfoAtPosition(fileName: string, position: number): QuickInfo {
+5 -3
View File
@@ -237,9 +237,9 @@ namespace ts {
getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications;
getCompletionsAtPosition(fileName: string, position: number): CompletionInfo;
// "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;
// "options" and "source" are optional only for backwards-compatibility
getCompletionEntryDetails(fileName: string, position: number, name: string, options?: FormatCodeOptions | FormatCodeSettings, source?: string): CompletionEntryDetails;
getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string): Symbol;
getQuickInfoAtPosition(fileName: string, position: number): QuickInfo;
@@ -696,6 +696,7 @@ namespace ts {
*/
replacementSpan?: TextSpan;
hasAction?: true;
source?: string;
}
export interface CompletionEntryDetails {
@@ -706,6 +707,7 @@ namespace ts {
documentation: SymbolDisplayPart[];
tags: JSDocTagInfo[];
codeActions?: CodeAction[];
source?: SymbolDisplayPart[];
}
export interface OutliningSpan {
+22 -8
View File
@@ -3915,8 +3915,8 @@ 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, options?: FormatCodeOptions | FormatCodeSettings): CompletionEntryDetails;
getCompletionEntrySymbol(fileName: string, position: number, entryName: string): Symbol;
getCompletionEntryDetails(fileName: string, position: number, name: string, options?: FormatCodeOptions | FormatCodeSettings, source?: string): CompletionEntryDetails;
getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string): Symbol;
getQuickInfoAtPosition(fileName: string, position: number): QuickInfo;
getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan;
getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan;
@@ -4296,6 +4296,7 @@ declare namespace ts {
*/
replacementSpan?: TextSpan;
hasAction?: true;
source?: string;
}
interface CompletionEntryDetails {
name: string;
@@ -4305,6 +4306,7 @@ declare namespace ts {
documentation: SymbolDisplayPart[];
tags: JSDocTagInfo[];
codeActions?: CodeAction[];
source?: SymbolDisplayPart[];
}
interface OutliningSpan {
/** The span of the document to actually collapse. */
@@ -6031,7 +6033,11 @@ declare namespace ts.server.protocol {
/**
* Names of one or more entries for which to obtain details.
*/
entryNames: string[];
entryNames: (string | CompletionEntryIdentifier)[];
}
interface CompletionEntryIdentifier {
name: string;
source: string;
}
/**
* Completion entry details request; value of command field is
@@ -6087,6 +6093,10 @@ declare namespace ts.server.protocol {
* made to avoid errors. The CompletionEntryDetails will have these actions.
*/
hasAction?: true;
/**
* Identifier (not necessarily human-readable) identifying where this completion came from.
*/
source?: string;
}
/**
* Additional completion entry details, available on demand
@@ -6120,6 +6130,10 @@ declare namespace ts.server.protocol {
* The associated code actions for this entry
*/
codeActions?: CodeAction[];
/**
* Human-readable description of the `source` from the CompletionEntry.
*/
source?: SymbolDisplayPart[];
}
interface CompletionsResponse extends Response {
body?: CompletionEntry[];
@@ -7227,7 +7241,6 @@ declare namespace ts.server {
getScriptInfo(uncheckedFileName: string): ScriptInfo;
filesToString(writeProjectFileNames: boolean): string;
setCompilerOptions(compilerOptions: CompilerOptions): void;
reloadScript(filename: NormalizedPath, tempFileName?: NormalizedPath): boolean;
protected removeRoot(info: ScriptInfo): void;
}
/**
@@ -7435,9 +7448,9 @@ declare namespace ts.server {
*/
readonly configuredProjects: Map<ConfiguredProject>;
/**
* list of open files
* Open files: with value being project root path, and key being Path of the file that is open
*/
readonly openFiles: ScriptInfo[];
readonly openFiles: Map<NormalizedPath>;
private compilerOptionsForInferredProjects;
private compilerOptionsForInferredProjectsPerProjectRoot;
/**
@@ -7556,7 +7569,7 @@ declare namespace ts.server {
* The server must start searching from the directory containing
* the newly opened file.
*/
private getConfigFileNameForFile(info, projectRootPath?);
private getConfigFileNameForFile(info, projectRootPath);
private printProjects();
private findConfiguredProjectByProjectName(configFileName);
private getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath);
@@ -7592,8 +7605,9 @@ declare namespace ts.server {
* If the config file is found and it refers to existing project, it reloads it either immediately
* or schedules it for reload depending on delayReload option
* If the there is no existing project it just opens the configured project for the config file
* reloadForInfo provides a way to filter out files to reload configured project for
*/
private reloadConfiguredProjectForFiles(openFiles, delayReload);
private reloadConfiguredProjectForFiles<T>(openFiles, delayReload, shouldReloadProjectFor);
/**
* Remove the root of inferred project if script info is part of another project
*/
+4 -2
View File
@@ -3915,8 +3915,8 @@ 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, options?: FormatCodeOptions | FormatCodeSettings): CompletionEntryDetails;
getCompletionEntrySymbol(fileName: string, position: number, entryName: string): Symbol;
getCompletionEntryDetails(fileName: string, position: number, name: string, options?: FormatCodeOptions | FormatCodeSettings, source?: string): CompletionEntryDetails;
getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string): Symbol;
getQuickInfoAtPosition(fileName: string, position: number): QuickInfo;
getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan;
getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan;
@@ -4296,6 +4296,7 @@ declare namespace ts {
*/
replacementSpan?: TextSpan;
hasAction?: true;
source?: string;
}
interface CompletionEntryDetails {
name: string;
@@ -4305,6 +4306,7 @@ declare namespace ts {
documentation: SymbolDisplayPart[];
tags: JSDocTagInfo[];
codeActions?: CodeAction[];
source?: SymbolDisplayPart[];
}
interface OutliningSpan {
/** The span of the document to actually collapse. */
@@ -26,7 +26,7 @@ interface UnaryExpression {
function parseUnaryExpression(operator: string): UnaryExpression {
return /*RENAME*/newFunction();
function newFunction() {
function newFunction(): UnaryExpression {
return {
kind: "Unary",
operator,
@@ -49,7 +49,7 @@ function parseUnaryExpression(operator: string): UnaryExpression {
return /*RENAME*/newFunction(operator);
}
function newFunction(operator: string) {
function newFunction(operator: string): UnaryExpression {
return {
kind: "Unary",
operator,
@@ -17,11 +17,11 @@ namespace N {
export const value = 1;
() => {
/*RENAME*/newFunction();
var c = /*RENAME*/newFunction()
}
function newFunction() {
var c = class {
return class {
M() {
return value;
}
@@ -34,12 +34,12 @@ namespace N {
export const value = 1;
() => {
/*RENAME*/newFunction();
var c = /*RENAME*/newFunction()
}
}
function newFunction() {
var c = class {
return class {
M() {
return N.value;
}
@@ -1,6 +1,7 @@
// ==ORIGINAL==
/*[#|*/const x = 1;/*|]*/
/*[#|*/const x = 1;
"hello";/*|]*/
x;
// ==SCOPE::Extract to function in global scope==
@@ -10,5 +11,6 @@ x;
function newFunction() {
const x = 1;
"hello";
return x;
}
@@ -1,6 +1,7 @@
// ==ORIGINAL==
/*[#|*/const x = 1;/*|]*/
/*[#|*/const x = 1;
"hello";/*|]*/
x;
// ==SCOPE::Extract to function in global scope==
@@ -10,5 +11,6 @@ x;
function newFunction() {
const x = 1;
"hello";
return x;
}
@@ -1,6 +1,7 @@
// ==ORIGINAL==
/*[#|*/const x: number = 1;/*|]*/
/*[#|*/const x: number = 1;
"hello";/*|]*/
x;
// ==SCOPE::Extract to function in global scope==
@@ -10,5 +11,6 @@ x;
function newFunction() {
const x: number = 1;
"hello";
return x;
}
@@ -1,6 +1,7 @@
// ==ORIGINAL==
/*[#|*/const x: number = 1;/*|]*/
/*[#|*/const x: number = 1;
"hello";/*|]*/
x; x;
// ==SCOPE::Extract to function in global scope==
@@ -10,5 +11,6 @@ x; x;
function newFunction() {
const x: number = 1;
"hello";
return x;
}
@@ -1,6 +1,7 @@
// ==ORIGINAL==
/*[#|*/let x = 1;/*|]*/
/*[#|*/let x = 1;
"hello";/*|]*/
x;
// ==SCOPE::Extract to function in global scope==
@@ -10,5 +11,6 @@ x;
function newFunction() {
let x = 1;
"hello";
return x;
}
@@ -1,6 +1,7 @@
// ==ORIGINAL==
/*[#|*/let x = 1;/*|]*/
/*[#|*/let x = 1;
"hello";/*|]*/
x;
// ==SCOPE::Extract to function in global scope==
@@ -10,5 +11,6 @@ x;
function newFunction() {
let x = 1;
"hello";
return x;
}
@@ -1,6 +1,7 @@
// ==ORIGINAL==
/*[#|*/let x: number = 1;/*|]*/
/*[#|*/let x: number = 1;
"hello";/*|]*/
x;
// ==SCOPE::Extract to function in global scope==
@@ -10,5 +11,6 @@ x;
function newFunction() {
let x: number = 1;
"hello";
return x;
}
@@ -1,6 +1,7 @@
// ==ORIGINAL==
/*[#|*/var x = 1;/*|]*/
/*[#|*/var x = 1;
"hello"/*|]*/
x;
// ==SCOPE::Extract to function in global scope==
@@ -10,5 +11,6 @@ x;
function newFunction() {
var x = 1;
"hello";
return x;
}
@@ -1,6 +1,7 @@
// ==ORIGINAL==
/*[#|*/var x = 1;/*|]*/
/*[#|*/var x = 1;
"hello"/*|]*/
x;
// ==SCOPE::Extract to function in global scope==
@@ -10,5 +11,6 @@ x;
function newFunction() {
var x = 1;
"hello";
return x;
}
@@ -9,10 +9,11 @@
////f/**/;
goTo.marker("");
verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.completionListContains({ name: "foo", source: "/a" }, "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.applyCodeActionFromCompletion("", {
name: "foo",
source: "/a",
description: `Add 'foo' to existing import declaration from "./a".`,
newFileContent: `import foo, { x } from "./a";
f;`,
@@ -8,10 +8,11 @@
////f/**/;
goTo.marker("");
verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.completionListContains({ name: "foo", source: "/a" }, "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.applyCodeActionFromCompletion("", {
name: "foo",
source: "/a",
description: `Add 'foo' to existing import declaration from "./a".`,
newFileContent: `import foo, * as a from "./a";
f;`,
@@ -8,10 +8,11 @@
////f/**/;
goTo.marker("");
verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.completionListContains({ name: "foo", source: "/a" }, "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.applyCodeActionFromCompletion("", {
name: "foo",
source: "/a",
description: `Import 'foo' from "./a".`,
// TODO: GH#18445
newFileContent: `import f_o_o from "./a";
@@ -7,10 +7,11 @@
////f/**/;
goTo.marker("");
verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.completionListContains({ name: "foo", source: "/a" }, "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.applyCodeActionFromCompletion("", {
name: "foo",
source: "/a",
description: `Import 'foo' from "./a".`,
// TODO: GH#18445
newFileContent: `import foo from "./a";\r
@@ -10,6 +10,7 @@
verify.applyCodeActionFromCompletion("", {
name: "x",
source: "m",
description: `Import 'x' from "m".`,
// TODO: GH#18445
newFileContent: `import { x } from "m";\r
@@ -14,9 +14,9 @@
goTo.marker("");
verify.not.completionListContains("abcde");
verify.not.completionListContains("dbf");
verify.not.completionListContains({ name: "abcde", source: "/a" });
verify.not.completionListContains({ name: "dbf", source: "/a" });
verify.completionListContains("bdf", "function bdf(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.completionListContains("abcdef", "function abcdef(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.completionListContains("BDF", "function BDF(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.completionListContains({ name: "bdf", source: "/a" }, "function bdf(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.completionListContains({ name: "abcdef", source: "/a" }, "function abcdef(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.completionListContains({ name: "BDF", source: "/a" }, "function BDF(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
@@ -0,0 +1,29 @@
/// <reference path="fourslash.ts" />
// @Filename: /global.d.ts
// A local variable would prevent import completions (see `completionsImport_shadowedByLocal.ts`), but a global doesn't.
////declare var foo: number;
// @Filename: /a.ts
////export const foo = 0;
// @Filename: /b.ts
////export const foo = 1;
// @Filename: /c.ts
////fo/**/
goTo.marker("");
verify.completionListContains("foo", "var foo: number", "", "var");
verify.completionListContains({ name: "foo", source: "/a" }, "const foo: 0", "", "const", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.completionListContains({ name: "foo", source: "/b" }, "const foo: 1", "", "const", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.applyCodeActionFromCompletion("", {
name: "foo",
source: "/b",
description: `Import 'foo' from "./b".`,
// TODO: GH#18445
newFileContent: `import { foo } from "./b";\r
\r
fo`,
});
@@ -9,10 +9,11 @@
////f/**/;
goTo.marker("");
verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.completionListContains({ name: "foo", source: "/a" }, "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.applyCodeActionFromCompletion("", {
name: "foo",
source: "/a",
description: `Add 'foo' to existing import declaration from "./a".`,
newFileContent: `import { x, foo } from "./a";
f;`,
@@ -9,11 +9,13 @@
////t/**/
goTo.marker("");
verify.completionListContains("Test1", "function Test1(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.completionListContains({ name: "Test1", source: "/a" }, "function Test1(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.completionListContains("Test2", "import Test2", "", "alias", /*spanIndex*/ undefined, /*hasAction*/ undefined);
verify.not.completionListContains({ name: "Test2", source: "/a" });
verify.applyCodeActionFromCompletion("", {
name: "Test1",
source: "/a",
description: `Add 'Test1' to existing import declaration from "./a".`,
newFileContent: `import { Test2, Test1 } from "./a";
t`,
@@ -8,10 +8,11 @@
////f/**/;
goTo.marker("");
verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.completionListContains({ name: "foo", source: "/a" }, "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.applyCodeActionFromCompletion("", {
name: "foo",
source: "/a",
description: `Import 'foo' from "./a".`,
// TODO: GH#18445
newFileContent: `import * as a from "./a";
@@ -0,0 +1,30 @@
/// <reference path="fourslash.ts" />
// Tests that we don't filter out a completion for an alias,
// so long as it's not an alias to a different module.
// @Filename: /a.ts
////const foo = 0;
////export { foo };
// @Filename: /a_reexport.ts
// Should not show up in completions
////export { foo } from "./a";
// @Filename: /b.ts
////fo/**/
goTo.marker("");
// https://github.com/Microsoft/TypeScript/issues/14003
verify.completionListContains({ name: "foo", source: "/a" }, "import foo", "", "alias", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.not.completionListContains({ name: "foo", source: "/a_reexport" });
verify.applyCodeActionFromCompletion("", {
name: "foo",
source: "/a",
description: `Import 'foo' from "./a".`,
// TODO: GH#18445
newFileContent: `import { foo } from "./a";\r
\r
fo`,
});
@@ -8,4 +8,4 @@
/////**/
goTo.marker("");
verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
verify.completionListContains({ name: "foo", source: "/a" }, "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true);
@@ -0,0 +1,12 @@
/// <reference path="fourslash.ts" />
// @Filename: /a.ts
////export const foo = 0;
// @Filename: /b.ts
////const foo = 1;
////fo/**/
goTo.marker("");
verify.completionListContains("foo", "const foo: 1", "", "const");
verify.not.completionListContains({ name: "foo", source: "/a" });
@@ -0,0 +1,9 @@
/// <reference path="fourslash.ts" />
/////**
//// * @typedef {object} T
//// * /**/
//// */
goTo.marker();
verify.completionListContains("@property", "@property", "", "keyword");
+3 -5
View File
@@ -7,7 +7,7 @@
// @Filename: foo.js
//// function foo() {
//// var i = 10;
//// /*a*/return i++;/*b*/
//// /*a*/return i + 1;/*b*/
//// }
goTo.select('a', 'b');
@@ -18,13 +18,11 @@ edit.applyRefactor({
newContent:
`function foo() {
var i = 10;
let __return;
({ __return, i } = /*RENAME*/newFunction(i));
return __return;
return /*RENAME*/newFunction(i);
}
function newFunction(i) {
return { __return: i++, i };
return i + 1;
}
`
});
+1 -1
View File
@@ -3,7 +3,7 @@
// You may not extract variable declarations with the export modifier
//// namespace NS {
//// /*start*/export var x = 10;/*end*/
//// /*start*/export var x = 10, y = 15;/*end*/
//// }
goTo.select('start', 'end')
+2 -1
View File
@@ -142,7 +142,7 @@ declare namespace FourSlashInterface {
constructor(negative?: boolean);
completionListCount(expectedCount: number): void;
completionListContains(
symbol: string,
entryId: string | { name: string, source?: string },
text?: string,
documentation?: string,
kind?: string,
@@ -196,6 +196,7 @@ declare namespace FourSlashInterface {
): void; //TODO: better type
applyCodeActionFromCompletion(markerName: string, options: {
name: string,
source?: string,
description: string,
newFileContent?: string,
newRangeContent?: string,