diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index d88f0e0854e..7ba4e94902d 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -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; } } diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 9622a6df332..b867ca77628 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -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(); diff --git a/src/harness/loggedIO.ts b/src/harness/loggedIO.ts index ecdcc1a740d..fcc163e77f3 100644 --- a/src/harness/loggedIO.ts +++ b/src/harness/loggedIO.ts @@ -89,6 +89,7 @@ interface PlaybackControl { namespace Playback { let recordLog: IOLog = undefined; let replayLog: IOLog = undefined; + let replayFilesRead: ts.Map | undefined = undefined; let recordLogFileNameBase = ""; interface Memoized { @@ -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 diff --git a/src/harness/rwcRunner.ts b/src/harness/rwcRunner.ts index 6675f5c6e7e..2b4fc7b24e4 100644 --- a/src/harness/rwcRunner.ts +++ b/src/harness/rwcRunner.ts @@ -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", () => { diff --git a/src/harness/unittests/extractFunctions.ts b/src/harness/unittests/extractFunctions.ts index 32c7139587d..56b3c35a292 100644 --- a/src/harness/unittests/extractFunctions.ts +++ b/src/harness/unittests/extractFunctions.ts @@ -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; `); diff --git a/src/harness/unittests/extractRanges.ts b/src/harness/unittests/extractRanges.ts index a467bb23e0f..e8e9a918f0d 100644 --- a/src/harness/unittests/extractRanges.ts +++ b/src/harness/unittests/extractRanges.ts @@ -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]); }); } \ No newline at end of file diff --git a/src/harness/unittests/extractTestHelpers.ts b/src/harness/unittests/extractTestHelpers.ts index ac4d1494811..b8974694cfe 100644 --- a/src/harness/unittests/extractTestHelpers.ts +++ b/src/harness/unittests/extractTestHelpers.ts @@ -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; diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index f0de849163a..ab736088a26 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -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({ + 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({ + 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({ + 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({ + 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({ + 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({ + 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({ + 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({ + 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", () => { diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 1518e6ed4c6..d6b1fc1a771 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -1,10 +1,17 @@ /// namespace ts.TestFSWithWatch { - const { content: libFileContent } = Harness.getDefaultLibraryFile(Harness.IO); export const libFile: FileOrFolder = { path: "/a/lib/lib.d.ts", - content: libFileContent + content: `/// +interface Boolean {} +interface Function {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array {}` }; export const safeList = { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f749c27cbbc..b7617e1fa17 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -352,9 +352,9 @@ namespace ts.server { */ readonly configuredProjects = createMap(); /** - * 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(); private compilerOptionsForInferredProjects: CompilerOptions; private compilerOptionsForInferredProjectsPerProjectRoot = createMap(); @@ -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, delayReload: boolean) { + private reloadConfiguredProjectForFiles(openFiles: Map, delayReload: boolean, shouldReloadProjectFor: (openFileValue: T) => boolean) { const updatedProjects = createMap(); // 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(); diff --git a/src/server/project.ts b/src/server/project.ts index 7542b30df00..0e1e7ac2cab 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -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); } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 35e9f20d058..6092f235942 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -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 { diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 245b27dd58a..9f029c00f0a 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -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(); } } diff --git a/src/server/session.ts b/src/server/session.ts index e8ce752745e..7ada8d2ff9c 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1188,10 +1188,10 @@ namespace ts.server { if (simplifiedResult) { return mapDefined(completions && completions.entries, entry => { if (completions.isMemberCompletion || (entry.name.toLowerCase().indexOf(prefix.toLowerCase()) === 0)) { - const { name, kind, kindModifiers, sortText, replacementSpan, 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(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); + } } } diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 1167f117c33..3f0d27e5b07 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -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): 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: diff --git a/src/services/completions.ts b/src/services/completions.ts index 7afa85ce26d..562cdd5cbb2 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -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, + uniqueNames: Map<{}>, target: ScriptTarget, entries: Push): 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, entries: Push, @@ -164,25 +178,35 @@ namespace ts.Completions { symbolToOriginInfoMap?: SymbolOriginInfoMap, ): Map { const start = timestamp(); - const uniqueNames = createMap(); + // 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(); 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, + ): { 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, 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, ): 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; 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 }; } } }); diff --git a/src/services/jsDoc.ts b/src/services/jsDoc.ts index a135a9e8eef..3e9913fc6c7 100644 --- a/src/services/jsDoc.ts +++ b/src/services/jsDoc.ts @@ -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. diff --git a/src/services/refactors/extractSymbol.ts b/src/services/refactors/extractSymbol.ts index 84f6ffb0815..b18e12d2b96 100644 --- a/src/services/refactors/extractSymbol.ts +++ b/src/services/refactors/extractSymbol.ts @@ -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)) { diff --git a/src/services/services.ts b/src/services/services.ts index 3b90a80a387..d6e75868852 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -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 { diff --git a/src/services/types.ts b/src/services/types.ts index 075d2d6e643..c854eb8aacc 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -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 { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 0f20bfc8687..5d439e840b0 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -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; /** - * 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; 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(openFiles, delayReload, shouldReloadProjectFor); /** * Remove the root of inferred project if script info is part of another project */ diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 67a30bcd371..2afef7d692c 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -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. */ diff --git a/tests/baselines/reference/extractFunction/extractFunction29.ts b/tests/baselines/reference/extractFunction/extractFunction29.ts index 492db8e6df9..339d218c714 100644 --- a/tests/baselines/reference/extractFunction/extractFunction29.ts +++ b/tests/baselines/reference/extractFunction/extractFunction29.ts @@ -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, diff --git a/tests/baselines/reference/extractFunction/extractFunction32.ts b/tests/baselines/reference/extractFunction/extractFunction32.ts index 720d0b227b0..acd5ac50804 100644 --- a/tests/baselines/reference/extractFunction/extractFunction32.ts +++ b/tests/baselines/reference/extractFunction/extractFunction32.ts @@ -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; } diff --git a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Const_NoType.js b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Const_NoType.js index fff1488ef41..f4b8a452a6e 100644 --- a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Const_NoType.js +++ b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Const_NoType.js @@ -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; } diff --git a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Const_NoType.ts b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Const_NoType.ts index fff1488ef41..f4b8a452a6e 100644 --- a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Const_NoType.ts +++ b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Const_NoType.ts @@ -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; } diff --git a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Const_Type.ts b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Const_Type.ts index 94576953758..5b619e2e4f5 100644 --- a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Const_Type.ts +++ b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Const_Type.ts @@ -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; } diff --git a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_ConsumedTwice.ts b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_ConsumedTwice.ts index ac57711614c..943fee55fa0 100644 --- a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_ConsumedTwice.ts +++ b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_ConsumedTwice.ts @@ -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; } diff --git a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Let_NoType.js b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Let_NoType.js index 4f407ce5703..39002be4a73 100644 --- a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Let_NoType.js +++ b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Let_NoType.js @@ -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; } diff --git a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Let_NoType.ts b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Let_NoType.ts index 4f407ce5703..39002be4a73 100644 --- a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Let_NoType.ts +++ b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Let_NoType.ts @@ -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; } diff --git a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Let_Type.ts b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Let_Type.ts index 048b77094ea..75d8643941f 100644 --- a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Let_Type.ts +++ b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Let_Type.ts @@ -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; } diff --git a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Var.js b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Var.js index 5a784c366c1..e4ee8ac186c 100644 --- a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Var.js +++ b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Var.js @@ -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; } diff --git a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Var.ts b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Var.ts index 5a784c366c1..e4ee8ac186c 100644 --- a/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Var.ts +++ b/tests/baselines/reference/extractFunction/extractFunction_VariableDeclaration_Var.ts @@ -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; } diff --git a/tests/cases/fourslash/completionsImport_default_addToNamedImports.ts b/tests/cases/fourslash/completionsImport_default_addToNamedImports.ts index 5662b57fdf6..734b3ff0f66 100644 --- a/tests/cases/fourslash/completionsImport_default_addToNamedImports.ts +++ b/tests/cases/fourslash/completionsImport_default_addToNamedImports.ts @@ -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;`, diff --git a/tests/cases/fourslash/completionsImport_default_addToNamespaceImport.ts b/tests/cases/fourslash/completionsImport_default_addToNamespaceImport.ts index b442ed21549..d4a81bc5272 100644 --- a/tests/cases/fourslash/completionsImport_default_addToNamespaceImport.ts +++ b/tests/cases/fourslash/completionsImport_default_addToNamespaceImport.ts @@ -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;`, diff --git a/tests/cases/fourslash/completionsImport_default_alreadyExistedWithRename.ts b/tests/cases/fourslash/completionsImport_default_alreadyExistedWithRename.ts index 3ee4decb0f8..f49f5850043 100644 --- a/tests/cases/fourslash/completionsImport_default_alreadyExistedWithRename.ts +++ b/tests/cases/fourslash/completionsImport_default_alreadyExistedWithRename.ts @@ -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"; diff --git a/tests/cases/fourslash/completionsImport_default_didNotExistBefore.ts b/tests/cases/fourslash/completionsImport_default_didNotExistBefore.ts index 6dbd437d364..fb4d65a1c52 100644 --- a/tests/cases/fourslash/completionsImport_default_didNotExistBefore.ts +++ b/tests/cases/fourslash/completionsImport_default_didNotExistBefore.ts @@ -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 diff --git a/tests/cases/fourslash/completionsImport_fromAmbientModule.ts b/tests/cases/fourslash/completionsImport_fromAmbientModule.ts index b45ad982486..05e7601c690 100644 --- a/tests/cases/fourslash/completionsImport_fromAmbientModule.ts +++ b/tests/cases/fourslash/completionsImport_fromAmbientModule.ts @@ -10,6 +10,7 @@ verify.applyCodeActionFromCompletion("", { name: "x", + source: "m", description: `Import 'x' from "m".`, // TODO: GH#18445 newFileContent: `import { x } from "m";\r diff --git a/tests/cases/fourslash/completionsImport_matching.ts b/tests/cases/fourslash/completionsImport_matching.ts index 3470d59bff2..edecf3592cd 100644 --- a/tests/cases/fourslash/completionsImport_matching.ts +++ b/tests/cases/fourslash/completionsImport_matching.ts @@ -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); diff --git a/tests/cases/fourslash/completionsImport_multipleWithSameName.ts b/tests/cases/fourslash/completionsImport_multipleWithSameName.ts new file mode 100644 index 00000000000..825a4ca0a97 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_multipleWithSameName.ts @@ -0,0 +1,29 @@ +/// + +// @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`, +}); diff --git a/tests/cases/fourslash/completionsImport_named_addToNamedImports.ts b/tests/cases/fourslash/completionsImport_named_addToNamedImports.ts index 23119ad491f..5444b25692a 100644 --- a/tests/cases/fourslash/completionsImport_named_addToNamedImports.ts +++ b/tests/cases/fourslash/completionsImport_named_addToNamedImports.ts @@ -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;`, diff --git a/tests/cases/fourslash/completionsImport_named_didNotExistBefore.ts b/tests/cases/fourslash/completionsImport_named_didNotExistBefore.ts index 95c68c2a05e..809e40e7cf2 100644 --- a/tests/cases/fourslash/completionsImport_named_didNotExistBefore.ts +++ b/tests/cases/fourslash/completionsImport_named_didNotExistBefore.ts @@ -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`, diff --git a/tests/cases/fourslash/completionsImport_named_namespaceImportExists.ts b/tests/cases/fourslash/completionsImport_named_namespaceImportExists.ts index da00907e60f..70cd75a4e41 100644 --- a/tests/cases/fourslash/completionsImport_named_namespaceImportExists.ts +++ b/tests/cases/fourslash/completionsImport_named_namespaceImportExists.ts @@ -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"; diff --git a/tests/cases/fourslash/completionsImport_ofAlias.ts b/tests/cases/fourslash/completionsImport_ofAlias.ts new file mode 100644 index 00000000000..27bc29fa210 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_ofAlias.ts @@ -0,0 +1,30 @@ +/// + +// 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`, +}); diff --git a/tests/cases/fourslash/completionsImport_previousTokenIsSemicolon.ts b/tests/cases/fourslash/completionsImport_previousTokenIsSemicolon.ts index 904de35354a..5eddb5037c1 100644 --- a/tests/cases/fourslash/completionsImport_previousTokenIsSemicolon.ts +++ b/tests/cases/fourslash/completionsImport_previousTokenIsSemicolon.ts @@ -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); diff --git a/tests/cases/fourslash/completionsImport_shadowedByLocal.ts b/tests/cases/fourslash/completionsImport_shadowedByLocal.ts new file mode 100644 index 00000000000..d35a021acc7 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_shadowedByLocal.ts @@ -0,0 +1,12 @@ +/// + +// @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" }); diff --git a/tests/cases/fourslash/completionsJsdocTag.ts b/tests/cases/fourslash/completionsJsdocTag.ts new file mode 100644 index 00000000000..0a8088ebfaa --- /dev/null +++ b/tests/cases/fourslash/completionsJsdocTag.ts @@ -0,0 +1,9 @@ +/// + +/////** +//// * @typedef {object} T +//// * /**/ +//// */ + +goTo.marker(); +verify.completionListContains("@property", "@property", "", "keyword"); diff --git a/tests/cases/fourslash/extract-method14.ts b/tests/cases/fourslash/extract-method14.ts index 11ea1f1b3fd..bdb9727f3d1 100644 --- a/tests/cases/fourslash/extract-method14.ts +++ b/tests/cases/fourslash/extract-method14.ts @@ -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; } ` }); diff --git a/tests/cases/fourslash/extract-method22.ts b/tests/cases/fourslash/extract-method22.ts index 85f88f3952c..4539e5cd470 100644 --- a/tests/cases/fourslash/extract-method22.ts +++ b/tests/cases/fourslash/extract-method22.ts @@ -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') diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index a7c54b5dda6..1dd1675b513 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -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,