From 02d1e7002b6f1ef02334ceca9088a6308555f512 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 31 May 2016 14:52:08 -0700 Subject: [PATCH 001/307] drop unused code --- src/server/editorServices.ts | 113 +++++------ src/server/session.ts | 177 +++++++++--------- .../cases/unittests/cachingInServerLSHost.ts | 14 +- 3 files changed, 145 insertions(+), 159 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 5502f74408e..61c9b602da5 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -16,6 +16,25 @@ namespace ts.server { } const lineCollectionCapacity = 4; + function getDefaultFormatCodeOptions(host: ServerHost): ts.FormatCodeOptions { + return ts.clone({ + IndentSize: 4, + TabSize: 4, + NewLineCharacter: host.newLine || "\n", + ConvertTabsToSpaces: true, + IndentStyle: ts.IndentStyle.Smart, + InsertSpaceAfterCommaDelimiter: true, + InsertSpaceAfterSemicolonInForStatements: true, + InsertSpaceBeforeAndAfterBinaryOperators: true, + InsertSpaceAfterKeywordsInControlFlowStatements: true, + InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false, + InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, + InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, + InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, + PlaceOpenBraceOnNewLineForFunctions: false, + PlaceOpenBraceOnNewLineForControlBlocks: false, + }); + } function mergeFormatOptions(formatCodeOptions: FormatCodeOptions, formatOptions: protocol.FormatOptions): void { const hasOwnProperty = Object.prototype.hasOwnProperty; @@ -32,13 +51,14 @@ namespace ts.server { children: ScriptInfo[] = []; // files referenced by this file defaultProject: Project; // project to use by default for file fileWatcher: FileWatcher; - formatCodeOptions = ts.clone(CompilerService.getDefaultFormatCodeOptions(this.host)); + formatCodeOptions: ts.FormatCodeOptions; path: Path; scriptKind: ScriptKind; constructor(private host: ServerHost, public fileName: string, public content: string, public isOpen = false) { this.path = toPath(fileName, host.getCurrentDirectory(), createGetCanonicalFileName(host.useCaseSensitiveFileNames)); this.svc = ScriptVersionCache.fromString(host, content); + this.formatCodeOptions = getDefaultFormatCodeOptions(this.host); } setFormatOptions(formatOptions: protocol.FormatOptions): void { @@ -51,19 +71,10 @@ namespace ts.server { this.isOpen = false; } - addChild(childInfo: ScriptInfo) { - this.children.push(childInfo); - } - snap() { return this.svc.getSnapshot(); } - getText() { - const snap = this.snap(); - return snap.getText(0, snap.getLength()); - } - getLineInfo(line: number) { const snap = this.snap(); return snap.index.lineNumberToInfo(line); @@ -198,15 +209,6 @@ namespace ts.server { this.resolvedTypeReferenceDirectives.clear(); } - lineAffectsRefs(filename: string, line: number) { - const info = this.getScriptInfo(filename); - const lineInfo = info.getLineInfo(line); - if (lineInfo && lineInfo.text) { - const regex = /reference|import|\/\*|\*\//; - return regex.test(lineInfo.text); - } - } - getCompilationSettings() { // change this to return active project settings for file return this.compilationSettings; @@ -369,7 +371,8 @@ namespace ts.server { } export class Project { - compilerService: CompilerService; + lsHost: LSHost; + languageService: LanguageService; projectFilename: string; projectFileWatcher: FileWatcher; directoryWatcher: FileWatcher; @@ -381,12 +384,22 @@ namespace ts.server { /** Used for configured projects which may have multiple open roots */ openRefCount = 0; - constructor(public projectService: ProjectService, public projectOptions?: ProjectOptions) { + constructor(public projectService: ProjectService, documentRegistry: ts.DocumentRegistry, public projectOptions?: ProjectOptions) { if (projectOptions && projectOptions.files) { // If files are listed explicitly, allow all extensions projectOptions.compilerOptions.allowNonTsExtensions = true; } - this.compilerService = new CompilerService(this, projectOptions && projectOptions.compilerOptions); + this.lsHost = new LSHost(this.projectService.host, this); + if (projectOptions && projectOptions.compilerOptions) { + this.lsHost.setCompilationSettings(projectOptions.compilerOptions); + } + else { + const defaultOpts = ts.getDefaultCompilerOptions(); + defaultOpts.allowNonTsExtensions = true; + defaultOpts.allowJs = true; + this.lsHost.setCompilationSettings(defaultOpts); + } + this.languageService = ts.createLanguageService(this.lsHost, documentRegistry); } addOpenRef() { @@ -403,7 +416,7 @@ namespace ts.server { } getRootFiles() { - return this.compilerService.host.roots.map(info => info.fileName); + return this.lsHost.roots.map(info => info.fileName); } getFileNames() { @@ -425,11 +438,11 @@ namespace ts.server { } isRoot(info: ScriptInfo) { - return this.compilerService.host.roots.some(root => root === info); + return this.lsHost.roots.some(root => root === info); } removeReferencedFile(info: ScriptInfo) { - this.compilerService.host.removeReferencedFile(info); + this.lsHost.removeReferencedFile(info); this.updateGraph(); } @@ -444,11 +457,11 @@ namespace ts.server { finishGraph() { this.updateGraph(); - this.compilerService.languageService.getNavigateToItems(".*"); + this.languageService.getNavigateToItems(".*"); } updateGraph() { - this.program = this.compilerService.languageService.getProgram(); + this.program = this.languageService.getProgram(); this.updateFileMap(); } @@ -458,12 +471,12 @@ namespace ts.server { // add a root file to project addRoot(info: ScriptInfo) { - this.compilerService.host.addRoot(info); + this.lsHost.addRoot(info); } // remove a root file from project removeRoot(info: ScriptInfo) { - this.compilerService.host.removeRoot(info); + this.lsHost.removeRoot(info); } filesToString() { @@ -477,7 +490,7 @@ namespace ts.server { this.projectOptions = projectOptions; if (projectOptions.compilerOptions) { projectOptions.compilerOptions.allowNonTsExtensions = true; - this.compilerService.setCompilerOptions(projectOptions.compilerOptions); + this.lsHost.setCompilationSettings(projectOptions.compilerOptions); } } } @@ -535,14 +548,17 @@ namespace ts.server { hostConfiguration: HostConfiguration; timerForDetectingProjectFileListChanges: Map = {}; + documentRegistry: ts.DocumentRegistry; + constructor(public host: ServerHost, public psLogger: Logger, public eventHandler?: ProjectServiceEventHandler) { // ts.disableIncrementalParsing = true; this.addDefaultHostConfiguration(); + this.documentRegistry = ts.createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); } addDefaultHostConfiguration() { this.hostConfiguration = { - formatCodeOptions: ts.clone(CompilerService.getDefaultFormatCodeOptions(this.host)), + formatCodeOptions: getDefaultFormatCodeOptions(this.host), hostInfo: "Unknown host" }; } @@ -686,7 +702,7 @@ namespace ts.server { } createInferredProject(root: ScriptInfo) { - const project = new Project(this); + const project = new Project(this, this.documentRegistry); project.addRoot(root); let currentPath = ts.getDirectoryPath(root.fileName); @@ -1299,7 +1315,7 @@ namespace ts.server { return errors; } else { - const oldFileNames = project.compilerService.host.roots.map(info => info.fileName); + const oldFileNames = project.lsHost.roots.map(info => info.fileName); const newFileNames = ts.filter(projectOptions.files, f => this.host.fileExists(f)); const fileNamesToRemove = oldFileNames.filter(f => newFileNames.indexOf(f) < 0); const fileNamesToAdd = newFileNames.filter(f => oldFileNames.indexOf(f) < 0); @@ -1343,17 +1359,16 @@ namespace ts.server { } createProject(projectFilename: string, projectOptions?: ProjectOptions) { - const project = new Project(this, projectOptions); + const project = new Project(this, this.documentRegistry, projectOptions); project.projectFilename = projectFilename; return project; } } - export class CompilerService { + export class CompilerServic1e { host: LSHost; languageService: ts.LanguageService; - classifier: ts.Classifier; settings: ts.CompilerOptions; documentRegistry = ts.createDocumentRegistry(); @@ -1369,38 +1384,12 @@ namespace ts.server { this.setCompilerOptions(defaultOpts); } this.languageService = ts.createLanguageService(this.host, this.documentRegistry); - this.classifier = ts.createClassifier(); } setCompilerOptions(opt: ts.CompilerOptions) { this.settings = opt; this.host.setCompilationSettings(opt); } - - isExternalModule(filename: string): boolean { - const sourceFile = this.languageService.getNonBoundSourceFile(filename); - return ts.isExternalModule(sourceFile); - } - - static getDefaultFormatCodeOptions(host: ServerHost): ts.FormatCodeOptions { - return ts.clone({ - IndentSize: 4, - TabSize: 4, - NewLineCharacter: host.newLine || "\n", - ConvertTabsToSpaces: true, - IndentStyle: ts.IndentStyle.Smart, - InsertSpaceAfterCommaDelimiter: true, - InsertSpaceAfterSemicolonInForStatements: true, - InsertSpaceBeforeAndAfterBinaryOperators: true, - InsertSpaceAfterKeywordsInControlFlowStatements: true, - InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false, - InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, - InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, - InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, - PlaceOpenBraceOnNewLineForFunctions: false, - PlaceOpenBraceOnNewLineForControlBlocks: false, - }); - } } export interface LineCollection { diff --git a/src/server/session.ts b/src/server/session.ts index 09b86da48cc..0c90cb830a3 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -70,8 +70,8 @@ namespace ts.server { function formatDiag(fileName: string, project: Project, diag: ts.Diagnostic): protocol.Diagnostic { return { - start: project.compilerService.host.positionToLineOffset(fileName, diag.start), - end: project.compilerService.host.positionToLineOffset(fileName, diag.start + diag.length), + start: project.lsHost.positionToLineOffset(fileName, diag.start), + end: project.lsHost.positionToLineOffset(fileName, diag.start + diag.length), text: ts.flattenDiagnosticMessageText(diag.messageText, "\n") }; } @@ -236,7 +236,7 @@ namespace ts.server { private semanticCheck(file: string, project: Project) { try { - const diags = project.compilerService.languageService.getSemanticDiagnostics(file); + const diags = project.languageService.getSemanticDiagnostics(file); if (diags) { const bakedDiags = diags.map((diag) => formatDiag(file, project, diag)); @@ -250,7 +250,7 @@ namespace ts.server { private syntacticCheck(file: string, project: Project) { try { - const diags = project.compilerService.languageService.getSyntacticDiagnostics(file); + const diags = project.languageService.getSyntacticDiagnostics(file); if (diags) { const bakedDiags = diags.map((diag) => formatDiag(file, project, diag)); this.event({ file: file, diagnostics: bakedDiags }, "syntaxDiag"); @@ -317,18 +317,18 @@ namespace ts.server { throw Errors.NoProject; } - const compilerService = project.compilerService; - const position = compilerService.host.lineOffsetToPosition(file, line, offset); + const lsHost = project.lsHost; + const position = lsHost.lineOffsetToPosition(file, line, offset); - const definitions = compilerService.languageService.getDefinitionAtPosition(file, position); + const definitions = project.languageService.getDefinitionAtPosition(file, position); if (!definitions) { return undefined; } return definitions.map(def => ({ file: def.fileName, - start: compilerService.host.positionToLineOffset(def.fileName, def.textSpan.start), - end: compilerService.host.positionToLineOffset(def.fileName, ts.textSpanEnd(def.textSpan)) + start: lsHost.positionToLineOffset(def.fileName, def.textSpan.start), + end: lsHost.positionToLineOffset(def.fileName, ts.textSpanEnd(def.textSpan)) })); } @@ -339,18 +339,18 @@ namespace ts.server { throw Errors.NoProject; } - const compilerService = project.compilerService; - const position = compilerService.host.lineOffsetToPosition(file, line, offset); + const lsHost = project.lsHost; + const position = lsHost.lineOffsetToPosition(file, line, offset); - const definitions = compilerService.languageService.getTypeDefinitionAtPosition(file, position); + const definitions = project.languageService.getTypeDefinitionAtPosition(file, position); if (!definitions) { return undefined; } return definitions.map(def => ({ file: def.fileName, - start: compilerService.host.positionToLineOffset(def.fileName, def.textSpan.start), - end: compilerService.host.positionToLineOffset(def.fileName, ts.textSpanEnd(def.textSpan)) + start: lsHost.positionToLineOffset(def.fileName, def.textSpan.start), + end: lsHost.positionToLineOffset(def.fileName, ts.textSpanEnd(def.textSpan)) })); } @@ -362,10 +362,10 @@ namespace ts.server { throw Errors.NoProject; } - const { compilerService } = project; - const position = compilerService.host.lineOffsetToPosition(fileName, line, offset); + const { lsHost } = project; + const position = lsHost.lineOffsetToPosition(fileName, line, offset); - const occurrences = compilerService.languageService.getOccurrencesAtPosition(fileName, position); + const occurrences = project.languageService.getOccurrencesAtPosition(fileName, position); if (!occurrences) { return undefined; @@ -373,8 +373,8 @@ namespace ts.server { return occurrences.map(occurrence => { const { fileName, isWriteAccess, textSpan } = occurrence; - const start = compilerService.host.positionToLineOffset(fileName, textSpan.start); - const end = compilerService.host.positionToLineOffset(fileName, ts.textSpanEnd(textSpan)); + const start = lsHost.positionToLineOffset(fileName, textSpan.start); + const end = lsHost.positionToLineOffset(fileName, ts.textSpanEnd(textSpan)); return { start, end, @@ -392,10 +392,10 @@ namespace ts.server { throw Errors.NoProject; } - const { compilerService } = project; - const position = compilerService.host.lineOffsetToPosition(fileName, line, offset); + const { lsHost } = project; + const position = lsHost.lineOffsetToPosition(fileName, line, offset); - const documentHighlights = compilerService.languageService.getDocumentHighlights(fileName, position, filesToSearch); + const documentHighlights = project.languageService.getDocumentHighlights(fileName, position, filesToSearch); if (!documentHighlights) { return undefined; @@ -413,8 +413,8 @@ namespace ts.server { function convertHighlightSpan(highlightSpan: ts.HighlightSpan): ts.server.protocol.HighlightSpan { const { textSpan, kind } = highlightSpan; - const start = compilerService.host.positionToLineOffset(fileName, textSpan.start); - const end = compilerService.host.positionToLineOffset(fileName, ts.textSpanEnd(textSpan)); + const start = lsHost.positionToLineOffset(fileName, textSpan.start); + const end = lsHost.positionToLineOffset(fileName, ts.textSpanEnd(textSpan)); return { start, end, kind }; } } @@ -445,9 +445,9 @@ namespace ts.server { const defaultProject = projects[0]; // The rename info should be the same for every project - const defaultProjectCompilerService = defaultProject.compilerService; - const position = defaultProjectCompilerService.host.lineOffsetToPosition(file, line, offset); - const renameInfo = defaultProjectCompilerService.languageService.getRenameInfo(file, position); + const lsHost = defaultProject.lsHost; + const position = lsHost.lineOffsetToPosition(file, line, offset); + const renameInfo = defaultProject.languageService.getRenameInfo(file, position); if (!renameInfo) { return undefined; } @@ -462,16 +462,15 @@ namespace ts.server { const fileSpans = combineProjectOutput( projects, (project: Project) => { - const compilerService = project.compilerService; - const renameLocations = compilerService.languageService.findRenameLocations(file, position, findInStrings, findInComments); + const renameLocations = project.languageService.findRenameLocations(file, position, findInStrings, findInComments); if (!renameLocations) { return []; } return renameLocations.map(location => ({ file: location.fileName, - start: compilerService.host.positionToLineOffset(location.fileName, location.textSpan.start), - end: compilerService.host.positionToLineOffset(location.fileName, ts.textSpanEnd(location.textSpan)), + start: project.lsHost.positionToLineOffset(location.fileName, location.textSpan.start), + end: project.lsHost.positionToLineOffset(location.fileName, ts.textSpanEnd(location.textSpan)), })); }, compareRenameLocation, @@ -526,35 +525,35 @@ namespace ts.server { } const defaultProject = projects[0]; - const position = defaultProject.compilerService.host.lineOffsetToPosition(file, line, offset); - const nameInfo = defaultProject.compilerService.languageService.getQuickInfoAtPosition(file, position); + const lsHost = defaultProject.lsHost; + const position = lsHost.lineOffsetToPosition(file, line, offset); + const nameInfo = defaultProject.languageService.getQuickInfoAtPosition(file, position); if (!nameInfo) { return undefined; } const displayString = ts.displayPartsToString(nameInfo.displayParts); const nameSpan = nameInfo.textSpan; - const nameColStart = defaultProject.compilerService.host.positionToLineOffset(file, nameSpan.start).offset; - const nameText = defaultProject.compilerService.host.getScriptSnapshot(file).getText(nameSpan.start, ts.textSpanEnd(nameSpan)); + const nameColStart = lsHost.positionToLineOffset(file, nameSpan.start).offset; + const nameText = lsHost.getScriptSnapshot(file).getText(nameSpan.start, ts.textSpanEnd(nameSpan)); const refs = combineProjectOutput( projects, (project: Project) => { - const compilerService = project.compilerService; - const references = compilerService.languageService.getReferencesAtPosition(file, position); + const references = project.languageService.getReferencesAtPosition(file, position); if (!references) { return []; } return references.map(ref => { - const start = compilerService.host.positionToLineOffset(ref.fileName, ref.textSpan.start); - const refLineSpan = compilerService.host.lineToTextSpan(ref.fileName, start.line - 1); - const snap = compilerService.host.getScriptSnapshot(ref.fileName); + const start = project.lsHost.positionToLineOffset(ref.fileName, ref.textSpan.start); + const refLineSpan = project.lsHost.lineToTextSpan(ref.fileName, start.line - 1); + const snap = project.lsHost.getScriptSnapshot(ref.fileName); const lineText = snap.getText(refLineSpan.start, ts.textSpanEnd(refLineSpan)).replace(/\r|\n/g, ""); return { file: ref.fileName, start: start, lineText: lineText, - end: compilerService.host.positionToLineOffset(ref.fileName, ts.textSpanEnd(ref.textSpan)), + end: project.lsHost.positionToLineOffset(ref.fileName, ts.textSpanEnd(ref.textSpan)), isWriteAccess: ref.isWriteAccess }; }); @@ -599,9 +598,9 @@ namespace ts.server { throw Errors.NoProject; } - const compilerService = project.compilerService; - const position = compilerService.host.lineOffsetToPosition(file, line, offset); - const quickInfo = compilerService.languageService.getQuickInfoAtPosition(file, position); + const lsHost = project.lsHost; + const position = lsHost.lineOffsetToPosition(file, line, offset); + const quickInfo = project.languageService.getQuickInfoAtPosition(file, position); if (!quickInfo) { return undefined; } @@ -611,8 +610,8 @@ namespace ts.server { return { kind: quickInfo.kind, kindModifiers: quickInfo.kindModifiers, - start: compilerService.host.positionToLineOffset(file, quickInfo.textSpan.start), - end: compilerService.host.positionToLineOffset(file, ts.textSpanEnd(quickInfo.textSpan)), + start: lsHost.positionToLineOffset(file, quickInfo.textSpan.start), + end: lsHost.positionToLineOffset(file, ts.textSpanEnd(quickInfo.textSpan)), displayString: displayString, documentation: docString, }; @@ -625,12 +624,12 @@ namespace ts.server { throw Errors.NoProject; } - const compilerService = project.compilerService; - const startPosition = compilerService.host.lineOffsetToPosition(file, line, offset); - const endPosition = compilerService.host.lineOffsetToPosition(file, endLine, endOffset); + const lsHost = project.lsHost; + const startPosition = lsHost.lineOffsetToPosition(file, line, offset); + const endPosition = lsHost.lineOffsetToPosition(file, endLine, endOffset); // TODO: avoid duplicate code (with formatonkey) - const edits = compilerService.languageService.getFormattingEditsForRange(file, startPosition, endPosition, + const edits = project.languageService.getFormattingEditsForRange(file, startPosition, endPosition, this.projectService.getFormatCodeOptions(file)); if (!edits) { return undefined; @@ -638,8 +637,8 @@ namespace ts.server { return edits.map((edit) => { return { - start: compilerService.host.positionToLineOffset(file, edit.span.start), - end: compilerService.host.positionToLineOffset(file, ts.textSpanEnd(edit.span)), + start: lsHost.positionToLineOffset(file, edit.span.start), + end: lsHost.positionToLineOffset(file, ts.textSpanEnd(edit.span)), newText: edit.newText ? edit.newText : "" }; }); @@ -653,10 +652,10 @@ namespace ts.server { throw Errors.NoProject; } - const compilerService = project.compilerService; - const position = compilerService.host.lineOffsetToPosition(file, line, offset); + const lsHost = project.lsHost; + const position = lsHost.lineOffsetToPosition(file, line, offset); const formatOptions = this.projectService.getFormatCodeOptions(file); - const edits = compilerService.languageService.getFormattingEditsAfterKeystroke(file, position, key, + const edits = project.languageService.getFormattingEditsAfterKeystroke(file, position, key, formatOptions); // Check whether we should auto-indent. This will be when // the position is on a line containing only whitespace. @@ -665,7 +664,7 @@ namespace ts.server { // only to the previous line. If all this is true, then // add edits necessary to properly indent the current line. if ((key == "\n") && ((!edits) || (edits.length === 0) || allEditsBeforePos(edits, position))) { - const scriptInfo = compilerService.host.getScriptInfo(file); + const scriptInfo = lsHost.getScriptInfo(file); if (scriptInfo) { const lineInfo = scriptInfo.getLineInfo(line); if (lineInfo && (lineInfo.leaf) && (lineInfo.leaf.text)) { @@ -679,7 +678,7 @@ namespace ts.server { ConvertTabsToSpaces: formatOptions.ConvertTabsToSpaces, IndentStyle: ts.IndentStyle.Smart, }; - const preferredIndent = compilerService.languageService.getIndentationAtPosition(file, position, editorOptions); + const preferredIndent = project.languageService.getIndentationAtPosition(file, position, editorOptions); let hasIndent = 0; let i: number, len: number; for (i = 0, len = lineText.length; i < len; i++) { @@ -712,9 +711,9 @@ namespace ts.server { return edits.map((edit) => { return { - start: compilerService.host.positionToLineOffset(file, + start: lsHost.positionToLineOffset(file, edit.span.start), - end: compilerService.host.positionToLineOffset(file, + end: lsHost.positionToLineOffset(file, ts.textSpanEnd(edit.span)), newText: edit.newText ? edit.newText : "" }; @@ -731,10 +730,10 @@ namespace ts.server { throw Errors.NoProject; } - const compilerService = project.compilerService; - const position = compilerService.host.lineOffsetToPosition(file, line, offset); + const lsHost = project.lsHost; + const position = lsHost.lineOffsetToPosition(file, line, offset); - const completions = compilerService.languageService.getCompletionsAtPosition(file, position); + const completions = project.languageService.getCompletionsAtPosition(file, position); if (!completions) { return undefined; } @@ -755,11 +754,11 @@ namespace ts.server { throw Errors.NoProject; } - const compilerService = project.compilerService; - const position = compilerService.host.lineOffsetToPosition(file, line, offset); + const lsHost = project.lsHost; + const position = lsHost.lineOffsetToPosition(file, line, offset); return entryNames.reduce((accum: protocol.CompletionEntryDetails[], entryName: string) => { - const details = compilerService.languageService.getCompletionEntryDetails(file, position, entryName); + const details = project.languageService.getCompletionEntryDetails(file, position, entryName); if (details) { accum.push(details); } @@ -774,9 +773,9 @@ namespace ts.server { throw Errors.NoProject; } - const compilerService = project.compilerService; - const position = compilerService.host.lineOffsetToPosition(file, line, offset); - const helpItems = compilerService.languageService.getSignatureHelpItems(file, position); + const lsHost = project.lsHost; + const position = lsHost.lineOffsetToPosition(file, line, offset); + const helpItems = project.languageService.getSignatureHelpItems(file, position); if (!helpItems) { return undefined; } @@ -785,8 +784,8 @@ namespace ts.server { const result: protocol.SignatureHelpItems = { items: helpItems.items, applicableSpan: { - start: compilerService.host.positionToLineOffset(file, span.start), - end: compilerService.host.positionToLineOffset(file, span.start + span.length) + start: lsHost.positionToLineOffset(file, span.start), + end: lsHost.positionToLineOffset(file, span.start + span.length) }, selectedItemIndex: helpItems.selectedItemIndex, argumentIndex: helpItems.argumentIndex, @@ -815,11 +814,11 @@ namespace ts.server { const file = ts.normalizePath(fileName); const project = this.projectService.getProjectForFile(file); if (project) { - const compilerService = project.compilerService; - const start = compilerService.host.lineOffsetToPosition(file, line, offset); - const end = compilerService.host.lineOffsetToPosition(file, endLine, endOffset); + const lsHost = project.lsHost; + const start = lsHost.lineOffsetToPosition(file, line, offset); + const end = lsHost.lineOffsetToPosition(file, endLine, endOffset); if (start >= 0) { - compilerService.host.editScript(file, start, end, insertString); + lsHost.editScript(file, start, end, insertString); this.changeSeq++; } this.updateProjectStructure(this.changeSeq, (n) => n === this.changeSeq); @@ -833,7 +832,7 @@ namespace ts.server { if (project) { this.changeSeq++; // make sure no changes happen before this one is finished - project.compilerService.host.reloadScript(file, tmpfile, () => { + project.lsHost.reloadScript(file, tmpfile, () => { this.output(undefined, CommandNames.Reload, reqSeq); }); } @@ -845,7 +844,7 @@ namespace ts.server { const project = this.projectService.getProjectForFile(file); if (project) { - project.compilerService.host.saveTo(file, tmpfile); + project.lsHost.saveTo(file, tmpfile); } } @@ -862,15 +861,15 @@ namespace ts.server { return undefined; } - const compilerService = project.compilerService; + const lsHost = project.lsHost; return items.map(item => ({ text: item.text, kind: item.kind, kindModifiers: item.kindModifiers, spans: item.spans.map(span => ({ - start: compilerService.host.positionToLineOffset(fileName, span.start), - end: compilerService.host.positionToLineOffset(fileName, ts.textSpanEnd(span)) + start: lsHost.positionToLineOffset(fileName, span.start), + end: lsHost.positionToLineOffset(fileName, ts.textSpanEnd(span)) })), childItems: this.decorateNavigationBarItem(project, fileName, item.childItems) })); @@ -883,8 +882,7 @@ namespace ts.server { throw Errors.NoProject; } - const compilerService = project.compilerService; - const items = compilerService.languageService.getNavigationBarItems(file); + const items = project.languageService.getNavigationBarItems(file); if (!items) { return undefined; } @@ -904,15 +902,14 @@ namespace ts.server { const allNavToItems = combineProjectOutput( projects, (project: Project) => { - const compilerService = project.compilerService; - const navItems = compilerService.languageService.getNavigateToItems(searchValue, maxResultCount); + const navItems = project.languageService.getNavigateToItems(searchValue, maxResultCount); if (!navItems) { return []; } return navItems.map((navItem) => { - const start = compilerService.host.positionToLineOffset(navItem.fileName, navItem.textSpan.start); - const end = compilerService.host.positionToLineOffset(navItem.fileName, ts.textSpanEnd(navItem.textSpan)); + const start = project.lsHost.positionToLineOffset(navItem.fileName, navItem.textSpan.start); + const end = project.lsHost.positionToLineOffset(navItem.fileName, ts.textSpanEnd(navItem.textSpan)); const bakedItem: protocol.NavtoItem = { name: navItem.name, kind: navItem.kind, @@ -958,17 +955,17 @@ namespace ts.server { throw Errors.NoProject; } - const compilerService = project.compilerService; - const position = compilerService.host.lineOffsetToPosition(file, line, offset); + const lsHost = project.lsHost; + const position = lsHost.lineOffsetToPosition(file, line, offset); - const spans = compilerService.languageService.getBraceMatchingAtPosition(file, position); + const spans = project.languageService.getBraceMatchingAtPosition(file, position); if (!spans) { return undefined; } return spans.map(span => ({ - start: compilerService.host.positionToLineOffset(file, span.start), - end: compilerService.host.positionToLineOffset(file, span.start + span.length) + start: lsHost.positionToLineOffset(file, span.start), + end: lsHost.positionToLineOffset(file, span.start + span.length) })); } diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index bcac28749b6..f0574bbf793 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -105,7 +105,7 @@ namespace ts { const { project, rootScriptInfo } = createProject(root.name, serverHost); // ensure that imported file was found - let diags = project.compilerService.languageService.getSemanticDiagnostics(imported.name); + let diags = project.languageService.getSemanticDiagnostics(imported.name); assert.equal(diags.length, 1); const originalFileExists = serverHost.fileExists; @@ -120,7 +120,7 @@ namespace ts { var x: string = 1;`; rootScriptInfo.editContent(0, rootScriptInfo.content.length, newContent); // trigger synchronization to make sure that import will be fetched from the cache - diags = project.compilerService.languageService.getSemanticDiagnostics(imported.name); + diags = project.languageService.getSemanticDiagnostics(imported.name); // ensure file has correct number of errors after edit assert.equal(diags.length, 1); } @@ -139,7 +139,7 @@ namespace ts { try { // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk - project.compilerService.languageService.getSemanticDiagnostics(imported.name); + project.languageService.getSemanticDiagnostics(imported.name); assert.isTrue(false, `should not find file '${imported.name}'`); } catch (e) { @@ -160,7 +160,7 @@ namespace ts { const newContent = `import {x} from "f1"`; rootScriptInfo.editContent(0, rootScriptInfo.content.length, newContent); - project.compilerService.languageService.getSemanticDiagnostics(imported.name); + project.languageService.getSemanticDiagnostics(imported.name); assert.isTrue(fileExistsCalled); // setting compiler options discards module resolution cache @@ -171,7 +171,7 @@ namespace ts { opts.compilerOptions.target = ts.ScriptTarget.ES5; project.setProjectOptions(opts); - project.compilerService.languageService.getSemanticDiagnostics(imported.name); + project.languageService.getSemanticDiagnostics(imported.name); assert.isTrue(fileExistsCalled); } }); @@ -205,7 +205,7 @@ namespace ts { const { project, rootScriptInfo } = createProject(root.name, serverHost); - let diags = project.compilerService.languageService.getSemanticDiagnostics(root.name); + let diags = project.languageService.getSemanticDiagnostics(root.name); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); assert.isTrue(diags.length === 1, "one diagnostic expected"); assert.isTrue(typeof diags[0].messageText === "string" && ((diags[0].messageText).indexOf("Cannot find module") === 0), "should be 'cannot find module' message"); @@ -215,7 +215,7 @@ namespace ts { fileExistsCalledForBar = false; rootScriptInfo.editContent(0, rootScriptInfo.content.length, `import {y} from "bar"`); - diags = project.compilerService.languageService.getSemanticDiagnostics(root.name); + diags = project.languageService.getSemanticDiagnostics(root.name); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); assert.isTrue(diags.length === 0); }); From 16bfeb49224cf81e6e167bbd5de9627c352bb43c Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 31 May 2016 15:48:19 -0700 Subject: [PATCH 002/307] move some methods to scriptinfo --- src/server/editorServices.ts | 132 ++++++-------------- src/server/session.ts | 228 ++++++++++++++++++----------------- 2 files changed, 158 insertions(+), 202 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 61c9b602da5..f6f9b82d15d 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -84,12 +84,43 @@ namespace ts.server { this.svc.edit(start, end - start, newText); } - getTextChangeRangeBetweenVersions(startVersion: number, endVersion: number): ts.TextChangeRange { - return this.svc.getTextChangesBetweenVersions(startVersion, endVersion); + /** + * @param line 1 based index + */ + lineToTextSpan(line: number) { + const index = this.snap().index; + const lineInfo = index.lineNumberToInfo(line + 1); + let len: number; + if (lineInfo.leaf) { + len = lineInfo.leaf.text.length; + } + else { + const nextLineInfo = index.lineNumberToInfo(line + 2); + len = nextLineInfo.offset - lineInfo.offset; + } + return ts.createTextSpan(lineInfo.offset, len); } - getChangeRange(oldSnapshot: ts.IScriptSnapshot): ts.TextChangeRange { - return this.snap().getChangeRange(oldSnapshot); + /** + * @param line 1 based index + * @param offset 1 based index + */ + lineOffsetToPosition(line: number, offset: number): number { + const index = this.snap().index; + + const lineInfo = index.lineNumberToInfo(line); + // TODO: assert this offset is actually on the line + return (lineInfo.offset + offset - 1); + } + + /** + * @param line 1-based index + * @param offset 1-based index + */ + positionToLineOffset(position: number): ILineInfo { + const index = this.snap().index; + const lineOffset = index.charOffsetToLineNumberAndPos(position); + return { line: lineOffset.line, offset: lineOffset.offset + 1 }; } } @@ -104,7 +135,6 @@ namespace ts.server { } export class LSHost implements ts.LanguageServiceHost { - ls: ts.LanguageService; compilationSettings: ts.CompilerOptions; filenameToScript: ts.FileMap; roots: ScriptInfo[] = []; @@ -195,6 +225,11 @@ namespace ts.server { return ts.combinePaths(nodeModuleBinDir, ts.getDefaultLibFileName(this.compilationSettings)); } + getScriptInfoForRootFile(fileName: string): ScriptInfo { + const path = toPath(fileName, this.host.getCurrentDirectory(), this.getCanonicalFileName); + return this.filenameToScript.get(path); + } + getScriptSnapshot(filename: string): ts.IScriptSnapshot { const scriptInfo = this.getScriptInfo(filename); if (scriptInfo) { @@ -238,10 +273,6 @@ namespace ts.server { return ""; } - getScriptIsOpen(filename: string) { - return this.getScriptInfo(filename).isOpen; - } - removeReferencedFile(info: ScriptInfo) { if (!info.isOpen) { this.filenameToScript.remove(info.path); @@ -316,52 +347,6 @@ namespace ts.server { directoryExists(path: string): boolean { return this.host.directoryExists(path); } - - /** - * @param line 1 based index - */ - lineToTextSpan(filename: string, line: number): ts.TextSpan { - const path = toPath(filename, this.host.getCurrentDirectory(), this.getCanonicalFileName); - const script: ScriptInfo = this.filenameToScript.get(path); - const index = script.snap().index; - - const lineInfo = index.lineNumberToInfo(line + 1); - let len: number; - if (lineInfo.leaf) { - len = lineInfo.leaf.text.length; - } - else { - const nextLineInfo = index.lineNumberToInfo(line + 2); - len = nextLineInfo.offset - lineInfo.offset; - } - return ts.createTextSpan(lineInfo.offset, len); - } - - /** - * @param line 1 based index - * @param offset 1 based index - */ - lineOffsetToPosition(filename: string, line: number, offset: number): number { - const path = toPath(filename, this.host.getCurrentDirectory(), this.getCanonicalFileName); - const script: ScriptInfo = this.filenameToScript.get(path); - const index = script.snap().index; - - const lineInfo = index.lineNumberToInfo(line); - // TODO: assert this offset is actually on the line - return (lineInfo.offset + offset - 1); - } - - /** - * @param line 1-based index - * @param offset 1-based index - */ - positionToLineOffset(filename: string, position: number): ILineInfo { - const path = toPath(filename, this.host.getCurrentDirectory(), this.getCanonicalFileName); - const script: ScriptInfo = this.filenameToScript.get(path); - const index = script.snap().index; - const lineOffset = index.charOffsetToLineNumberAndPos(position); - return { line: lineOffset.line, offset: lineOffset.offset + 1 }; - } } export interface ProjectOptions { @@ -1366,32 +1351,6 @@ namespace ts.server { } - export class CompilerServic1e { - host: LSHost; - languageService: ts.LanguageService; - settings: ts.CompilerOptions; - documentRegistry = ts.createDocumentRegistry(); - - constructor(public project: Project, opt?: ts.CompilerOptions) { - this.host = new LSHost(project.projectService.host, project); - if (opt) { - this.setCompilerOptions(opt); - } - else { - const defaultOpts = ts.getDefaultCompilerOptions(); - defaultOpts.allowNonTsExtensions = true; - defaultOpts.allowJs = true; - this.setCompilerOptions(defaultOpts); - } - this.languageService = ts.createLanguageService(this.host, this.documentRegistry); - } - - setCompilerOptions(opt: ts.CompilerOptions) { - this.settings = opt; - this.host.setCompilationSettings(opt); - } - } - export interface LineCollection { charCount(): number; lineCount(): number; @@ -2305,18 +2264,7 @@ namespace ts.server { } export class LineLeaf implements LineCollection { - udata: any; - constructor(public text: string) { - - } - - setUdata(data: any) { - this.udata = data; - } - - getUdata() { - return this.udata; } isLeaf() { diff --git a/src/server/session.ts b/src/server/session.ts index 0c90cb830a3..99e11d41b89 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -69,9 +69,10 @@ namespace ts.server { } function formatDiag(fileName: string, project: Project, diag: ts.Diagnostic): protocol.Diagnostic { + const scriptInfo = project.lsHost.getScriptInfoForRootFile(fileName); return { - start: project.lsHost.positionToLineOffset(fileName, diag.start), - end: project.lsHost.positionToLineOffset(fileName, diag.start + diag.length), + start: scriptInfo.positionToLineOffset(diag.start), + end: scriptInfo.positionToLineOffset(diag.start + diag.length), text: ts.flattenDiagnosticMessageText(diag.messageText, "\n") }; } @@ -317,19 +318,22 @@ namespace ts.server { throw Errors.NoProject; } - const lsHost = project.lsHost; - const position = lsHost.lineOffsetToPosition(file, line, offset); + const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const position = scriptInfo.lineOffsetToPosition(line, offset); const definitions = project.languageService.getDefinitionAtPosition(file, position); if (!definitions) { return undefined; } - return definitions.map(def => ({ - file: def.fileName, - start: lsHost.positionToLineOffset(def.fileName, def.textSpan.start), - end: lsHost.positionToLineOffset(def.fileName, ts.textSpanEnd(def.textSpan)) - })); + return definitions.map(def => { + const defScriptInfo = project.lsHost.getScriptInfoForRootFile(def.fileName); + return { + file: def.fileName, + start: defScriptInfo.positionToLineOffset(def.textSpan.start), + end: defScriptInfo.positionToLineOffset(ts.textSpanEnd(def.textSpan)) + } + }); } private getTypeDefinition(line: number, offset: number, fileName: string): protocol.FileSpan[] { @@ -339,19 +343,22 @@ namespace ts.server { throw Errors.NoProject; } - const lsHost = project.lsHost; - const position = lsHost.lineOffsetToPosition(file, line, offset); + const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const position = scriptInfo.lineOffsetToPosition(line, offset); const definitions = project.languageService.getTypeDefinitionAtPosition(file, position); if (!definitions) { return undefined; } - return definitions.map(def => ({ - file: def.fileName, - start: lsHost.positionToLineOffset(def.fileName, def.textSpan.start), - end: lsHost.positionToLineOffset(def.fileName, ts.textSpanEnd(def.textSpan)) - })); + return definitions.map(def => { + const defScriptInfo = project.lsHost.getScriptInfoForRootFile(def.fileName); + return { + file: def.fileName, + start: defScriptInfo.positionToLineOffset(def.textSpan.start), + end: defScriptInfo.positionToLineOffset(ts.textSpanEnd(def.textSpan)) + } + }); } private getOccurrences(line: number, offset: number, fileName: string): protocol.OccurrencesResponseItem[] { @@ -362,8 +369,8 @@ namespace ts.server { throw Errors.NoProject; } - const { lsHost } = project; - const position = lsHost.lineOffsetToPosition(fileName, line, offset); + const scriptInfo = project.lsHost.getScriptInfo(fileName); + const position = scriptInfo.lineOffsetToPosition(line, offset); const occurrences = project.languageService.getOccurrencesAtPosition(fileName, position); @@ -373,8 +380,9 @@ namespace ts.server { return occurrences.map(occurrence => { const { fileName, isWriteAccess, textSpan } = occurrence; - const start = lsHost.positionToLineOffset(fileName, textSpan.start); - const end = lsHost.positionToLineOffset(fileName, ts.textSpanEnd(textSpan)); + const scriptInfo = project.lsHost.getScriptInfo(fileName); + const start = scriptInfo.positionToLineOffset(textSpan.start); + const end = scriptInfo.positionToLineOffset(ts.textSpanEnd(textSpan)); return { start, end, @@ -392,8 +400,8 @@ namespace ts.server { throw Errors.NoProject; } - const { lsHost } = project; - const position = lsHost.lineOffsetToPosition(fileName, line, offset); + const scriptInfo = project.lsHost.getScriptInfo(fileName); + const position = scriptInfo.lineOffsetToPosition(line, offset); const documentHighlights = project.languageService.getDocumentHighlights(fileName, position, filesToSearch); @@ -406,6 +414,7 @@ namespace ts.server { function convertToDocumentHighlightsItem(documentHighlights: ts.DocumentHighlights): ts.server.protocol.DocumentHighlightsItem { const { fileName, highlightSpans } = documentHighlights; + const scriptInfo = project.lsHost.getScriptInfo(fileName); return { file: fileName, highlightSpans: highlightSpans.map(convertHighlightSpan) @@ -413,8 +422,8 @@ namespace ts.server { function convertHighlightSpan(highlightSpan: ts.HighlightSpan): ts.server.protocol.HighlightSpan { const { textSpan, kind } = highlightSpan; - const start = lsHost.positionToLineOffset(fileName, textSpan.start); - const end = lsHost.positionToLineOffset(fileName, ts.textSpanEnd(textSpan)); + const start = scriptInfo.positionToLineOffset(textSpan.start); + const end = scriptInfo.positionToLineOffset(ts.textSpanEnd(textSpan)); return { start, end, kind }; } } @@ -445,8 +454,8 @@ namespace ts.server { const defaultProject = projects[0]; // The rename info should be the same for every project - const lsHost = defaultProject.lsHost; - const position = lsHost.lineOffsetToPosition(file, line, offset); + const scriptInfo = defaultProject.lsHost.getScriptInfoForRootFile(file); + const position = scriptInfo.lineOffsetToPosition(line, offset); const renameInfo = defaultProject.languageService.getRenameInfo(file, position); if (!renameInfo) { return undefined; @@ -467,11 +476,14 @@ namespace ts.server { return []; } - return renameLocations.map(location => ({ - file: location.fileName, - start: project.lsHost.positionToLineOffset(location.fileName, location.textSpan.start), - end: project.lsHost.positionToLineOffset(location.fileName, ts.textSpanEnd(location.textSpan)), - })); + return renameLocations.map(location => { + const locationScriptInfo = project.lsHost.getScriptInfoForRootFile(location.fileName); + return { + file: location.fileName, + start: locationScriptInfo.positionToLineOffset(location.textSpan.start), + end: locationScriptInfo.positionToLineOffset(ts.textSpanEnd(location.textSpan)), + } + }); }, compareRenameLocation, (a, b) => a.file === b.file && a.start.line === b.start.line && a.start.offset === b.start.offset @@ -525,8 +537,8 @@ namespace ts.server { } const defaultProject = projects[0]; - const lsHost = defaultProject.lsHost; - const position = lsHost.lineOffsetToPosition(file, line, offset); + const scriptInfo = defaultProject.lsHost.getScriptInfoForRootFile(file); + const position = scriptInfo.lineOffsetToPosition(line, offset); const nameInfo = defaultProject.languageService.getQuickInfoAtPosition(file, position); if (!nameInfo) { return undefined; @@ -534,8 +546,8 @@ namespace ts.server { const displayString = ts.displayPartsToString(nameInfo.displayParts); const nameSpan = nameInfo.textSpan; - const nameColStart = lsHost.positionToLineOffset(file, nameSpan.start).offset; - const nameText = lsHost.getScriptSnapshot(file).getText(nameSpan.start, ts.textSpanEnd(nameSpan)); + const nameColStart = scriptInfo.positionToLineOffset(nameSpan.start).offset; + const nameText = scriptInfo.snap().getText(nameSpan.start, ts.textSpanEnd(nameSpan)); const refs = combineProjectOutput( projects, (project: Project) => { @@ -545,15 +557,15 @@ namespace ts.server { } return references.map(ref => { - const start = project.lsHost.positionToLineOffset(ref.fileName, ref.textSpan.start); - const refLineSpan = project.lsHost.lineToTextSpan(ref.fileName, start.line - 1); - const snap = project.lsHost.getScriptSnapshot(ref.fileName); - const lineText = snap.getText(refLineSpan.start, ts.textSpanEnd(refLineSpan)).replace(/\r|\n/g, ""); + const refScriptInfo = project.lsHost.getScriptInfoForRootFile(ref.fileName); + const start = refScriptInfo.positionToLineOffset(ref.textSpan.start); + const refLineSpan = refScriptInfo.lineToTextSpan(start.line - 1); + const lineText = refScriptInfo.snap().getText(refLineSpan.start, ts.textSpanEnd(refLineSpan)).replace(/\r|\n/g, ""); return { file: ref.fileName, start: start, lineText: lineText, - end: project.lsHost.positionToLineOffset(ref.fileName, ts.textSpanEnd(ref.textSpan)), + end: refScriptInfo.positionToLineOffset(ts.textSpanEnd(ref.textSpan)), isWriteAccess: ref.isWriteAccess }; }); @@ -598,8 +610,8 @@ namespace ts.server { throw Errors.NoProject; } - const lsHost = project.lsHost; - const position = lsHost.lineOffsetToPosition(file, line, offset); + const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const position = scriptInfo.lineOffsetToPosition(line, offset); const quickInfo = project.languageService.getQuickInfoAtPosition(file, position); if (!quickInfo) { return undefined; @@ -610,8 +622,8 @@ namespace ts.server { return { kind: quickInfo.kind, kindModifiers: quickInfo.kindModifiers, - start: lsHost.positionToLineOffset(file, quickInfo.textSpan.start), - end: lsHost.positionToLineOffset(file, ts.textSpanEnd(quickInfo.textSpan)), + start: scriptInfo.positionToLineOffset(quickInfo.textSpan.start), + end: scriptInfo.positionToLineOffset(ts.textSpanEnd(quickInfo.textSpan)), displayString: displayString, documentation: docString, }; @@ -624,9 +636,9 @@ namespace ts.server { throw Errors.NoProject; } - const lsHost = project.lsHost; - const startPosition = lsHost.lineOffsetToPosition(file, line, offset); - const endPosition = lsHost.lineOffsetToPosition(file, endLine, endOffset); + const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const startPosition = scriptInfo.lineOffsetToPosition(line, offset); + const endPosition = scriptInfo.lineOffsetToPosition(endLine, endOffset); // TODO: avoid duplicate code (with formatonkey) const edits = project.languageService.getFormattingEditsForRange(file, startPosition, endPosition, @@ -637,8 +649,8 @@ namespace ts.server { return edits.map((edit) => { return { - start: lsHost.positionToLineOffset(file, edit.span.start), - end: lsHost.positionToLineOffset(file, ts.textSpanEnd(edit.span)), + start: scriptInfo.positionToLineOffset(edit.span.start), + end: scriptInfo.positionToLineOffset(ts.textSpanEnd(edit.span)), newText: edit.newText ? edit.newText : "" }; }); @@ -652,8 +664,8 @@ namespace ts.server { throw Errors.NoProject; } - const lsHost = project.lsHost; - const position = lsHost.lineOffsetToPosition(file, line, offset); + const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const position = scriptInfo.lineOffsetToPosition(line, offset); const formatOptions = this.projectService.getFormatCodeOptions(file); const edits = project.languageService.getFormattingEditsAfterKeystroke(file, position, key, formatOptions); @@ -664,42 +676,39 @@ namespace ts.server { // only to the previous line. If all this is true, then // add edits necessary to properly indent the current line. if ((key == "\n") && ((!edits) || (edits.length === 0) || allEditsBeforePos(edits, position))) { - const scriptInfo = lsHost.getScriptInfo(file); - if (scriptInfo) { - const lineInfo = scriptInfo.getLineInfo(line); - if (lineInfo && (lineInfo.leaf) && (lineInfo.leaf.text)) { - const lineText = lineInfo.leaf.text; - if (lineText.search("\\S") < 0) { - // TODO: get these options from host - const editorOptions: ts.EditorOptions = { - IndentSize: formatOptions.IndentSize, - TabSize: formatOptions.TabSize, - NewLineCharacter: formatOptions.NewLineCharacter, - ConvertTabsToSpaces: formatOptions.ConvertTabsToSpaces, - IndentStyle: ts.IndentStyle.Smart, - }; - const preferredIndent = project.languageService.getIndentationAtPosition(file, position, editorOptions); - let hasIndent = 0; - let i: number, len: number; - for (i = 0, len = lineText.length; i < len; i++) { - if (lineText.charAt(i) == " ") { - hasIndent++; - } - else if (lineText.charAt(i) == "\t") { - hasIndent += editorOptions.TabSize; - } - else { - break; - } + const lineInfo = scriptInfo.getLineInfo(line); + if (lineInfo && (lineInfo.leaf) && (lineInfo.leaf.text)) { + const lineText = lineInfo.leaf.text; + if (lineText.search("\\S") < 0) { + // TODO: get these options from host + const editorOptions: ts.EditorOptions = { + IndentSize: formatOptions.IndentSize, + TabSize: formatOptions.TabSize, + NewLineCharacter: formatOptions.NewLineCharacter, + ConvertTabsToSpaces: formatOptions.ConvertTabsToSpaces, + IndentStyle: ts.IndentStyle.Smart, + }; + const preferredIndent = project.languageService.getIndentationAtPosition(file, position, editorOptions); + let hasIndent = 0; + let i: number, len: number; + for (i = 0, len = lineText.length; i < len; i++) { + if (lineText.charAt(i) == " ") { + hasIndent++; } - // i points to the first non whitespace character - if (preferredIndent !== hasIndent) { - const firstNoWhiteSpacePosition = lineInfo.offset + i; - edits.push({ - span: ts.createTextSpanFromBounds(lineInfo.offset, firstNoWhiteSpacePosition), - newText: generateIndentString(preferredIndent, editorOptions) - }); + else if (lineText.charAt(i) == "\t") { + hasIndent += editorOptions.TabSize; } + else { + break; + } + } + // i points to the first non whitespace character + if (preferredIndent !== hasIndent) { + const firstNoWhiteSpacePosition = lineInfo.offset + i; + edits.push({ + span: ts.createTextSpanFromBounds(lineInfo.offset, firstNoWhiteSpacePosition), + newText: generateIndentString(preferredIndent, editorOptions) + }); } } } @@ -711,10 +720,8 @@ namespace ts.server { return edits.map((edit) => { return { - start: lsHost.positionToLineOffset(file, - edit.span.start), - end: lsHost.positionToLineOffset(file, - ts.textSpanEnd(edit.span)), + start: scriptInfo.positionToLineOffset(edit.span.start), + end: scriptInfo.positionToLineOffset(ts.textSpanEnd(edit.span)), newText: edit.newText ? edit.newText : "" }; }); @@ -730,8 +737,8 @@ namespace ts.server { throw Errors.NoProject; } - const lsHost = project.lsHost; - const position = lsHost.lineOffsetToPosition(file, line, offset); + const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const position = scriptInfo.lineOffsetToPosition(line, offset); const completions = project.languageService.getCompletionsAtPosition(file, position); if (!completions) { @@ -754,8 +761,8 @@ namespace ts.server { throw Errors.NoProject; } - const lsHost = project.lsHost; - const position = lsHost.lineOffsetToPosition(file, line, offset); + const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const position = scriptInfo.lineOffsetToPosition(line, offset); return entryNames.reduce((accum: protocol.CompletionEntryDetails[], entryName: string) => { const details = project.languageService.getCompletionEntryDetails(file, position, entryName); @@ -773,8 +780,8 @@ namespace ts.server { throw Errors.NoProject; } - const lsHost = project.lsHost; - const position = lsHost.lineOffsetToPosition(file, line, offset); + const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const position = scriptInfo.lineOffsetToPosition(line, offset); const helpItems = project.languageService.getSignatureHelpItems(file, position); if (!helpItems) { return undefined; @@ -784,8 +791,8 @@ namespace ts.server { const result: protocol.SignatureHelpItems = { items: helpItems.items, applicableSpan: { - start: lsHost.positionToLineOffset(file, span.start), - end: lsHost.positionToLineOffset(file, span.start + span.length) + start: scriptInfo.positionToLineOffset(span.start), + end: scriptInfo.positionToLineOffset(span.start + span.length) }, selectedItemIndex: helpItems.selectedItemIndex, argumentIndex: helpItems.argumentIndex, @@ -814,11 +821,11 @@ namespace ts.server { const file = ts.normalizePath(fileName); const project = this.projectService.getProjectForFile(file); if (project) { - const lsHost = project.lsHost; - const start = lsHost.lineOffsetToPosition(file, line, offset); - const end = lsHost.lineOffsetToPosition(file, endLine, endOffset); + const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const start = scriptInfo.lineOffsetToPosition(line, offset); + const end = scriptInfo.lineOffsetToPosition(endLine, endOffset); if (start >= 0) { - lsHost.editScript(file, start, end, insertString); + scriptInfo.editContent(start, end, insertString); this.changeSeq++; } this.updateProjectStructure(this.changeSeq, (n) => n === this.changeSeq); @@ -861,15 +868,15 @@ namespace ts.server { return undefined; } - const lsHost = project.lsHost; + const scriptInfo = project.lsHost.getScriptInfoForRootFile(fileName); return items.map(item => ({ text: item.text, kind: item.kind, kindModifiers: item.kindModifiers, spans: item.spans.map(span => ({ - start: lsHost.positionToLineOffset(fileName, span.start), - end: lsHost.positionToLineOffset(fileName, ts.textSpanEnd(span)) + start: scriptInfo.positionToLineOffset(span.start), + end: scriptInfo.positionToLineOffset(ts.textSpanEnd(span)) })), childItems: this.decorateNavigationBarItem(project, fileName, item.childItems) })); @@ -908,8 +915,9 @@ namespace ts.server { } return navItems.map((navItem) => { - const start = project.lsHost.positionToLineOffset(navItem.fileName, navItem.textSpan.start); - const end = project.lsHost.positionToLineOffset(navItem.fileName, ts.textSpanEnd(navItem.textSpan)); + const scriptInfo = project.lsHost.getScriptInfoForRootFile(navItem.fileName); + const start = scriptInfo.positionToLineOffset(navItem.textSpan.start); + const end = scriptInfo.positionToLineOffset(ts.textSpanEnd(navItem.textSpan)); const bakedItem: protocol.NavtoItem = { name: navItem.name, kind: navItem.kind, @@ -955,8 +963,8 @@ namespace ts.server { throw Errors.NoProject; } - const lsHost = project.lsHost; - const position = lsHost.lineOffsetToPosition(file, line, offset); + const scriptInfo = project.lsHost.getScriptInfoForRootFile(fileName); + const position = scriptInfo.lineOffsetToPosition(line, offset); const spans = project.languageService.getBraceMatchingAtPosition(file, position); if (!spans) { @@ -964,8 +972,8 @@ namespace ts.server { } return spans.map(span => ({ - start: lsHost.positionToLineOffset(file, span.start), - end: lsHost.positionToLineOffset(file, span.start + span.length) + start: scriptInfo.positionToLineOffset(span.start), + end: scriptInfo.positionToLineOffset(span.start + span.length) })); } From 0f5e91bfe8dd8c39442855cabecf3545f41478ad Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 31 May 2016 16:34:14 -0700 Subject: [PATCH 003/307] isolate responsibilities of LSHost --- src/server/editorServices.ts | 152 ++++++++++++++++------------------- src/server/session.ts | 56 ++++++------- 2 files changed, 99 insertions(+), 109 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f6f9b82d15d..f2ab320c265 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -48,7 +48,6 @@ namespace ts.server { export class ScriptInfo { svc: ScriptVersionCache; - children: ScriptInfo[] = []; // files referenced by this file defaultProject: Project; // project to use by default for file fileWatcher: FileWatcher; formatCodeOptions: ts.FormatCodeOptions; @@ -135,20 +134,14 @@ namespace ts.server { } export class LSHost implements ts.LanguageServiceHost { - compilationSettings: ts.CompilerOptions; - filenameToScript: ts.FileMap; - roots: ScriptInfo[] = []; - + private compilationSettings: ts.CompilerOptions; private resolvedModuleNames: ts.FileMap>; private resolvedTypeReferenceDirectives: ts.FileMap>; private moduleResolutionHost: ts.ModuleResolutionHost; - private getCanonicalFileName: (fileName: string) => string; - constructor(public host: ServerHost, public project: Project) { - this.getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + constructor(private host: ServerHost, private project: Project) { this.resolvedModuleNames = createFileMap>(); this.resolvedTypeReferenceDirectives = createFileMap>(); - this.filenameToScript = createFileMap(); this.moduleResolutionHost = { fileExists: fileName => this.fileExists(fileName), readFile: fileName => this.host.readFile(fileName), @@ -163,7 +156,7 @@ namespace ts.server { loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T, getResult: (s: T) => R): R[] { - const path = toPath(containingFile, this.host.getCurrentDirectory(), this.getCanonicalFileName); + const path = toPath(containingFile, this.host.getCurrentDirectory(), this.project.getCanonicalFileName); const currentResolutionsInFile = cache.get(path); const newResolutions: Map = {}; @@ -225,13 +218,8 @@ namespace ts.server { return ts.combinePaths(nodeModuleBinDir, ts.getDefaultLibFileName(this.compilationSettings)); } - getScriptInfoForRootFile(fileName: string): ScriptInfo { - const path = toPath(fileName, this.host.getCurrentDirectory(), this.getCanonicalFileName); - return this.filenameToScript.get(path); - } - getScriptSnapshot(filename: string): ts.IScriptSnapshot { - const scriptInfo = this.getScriptInfo(filename); + const scriptInfo = this.project.getScriptInfo(filename); if (scriptInfo) { return scriptInfo.snap(); } @@ -250,11 +238,11 @@ namespace ts.server { } getScriptFileNames() { - return this.roots.map(root => root.fileName); + return this.project.getRootFiles(); } getScriptKind(fileName: string) { - const info = this.getScriptInfo(fileName); + const info = this.project.getScriptInfo(fileName); if (!info) { return undefined; } @@ -266,7 +254,7 @@ namespace ts.server { } getScriptVersion(filename: string) { - return this.getScriptInfo(filename).svc.latestVersion().toString(); + return this.project.getScriptInfo(filename).svc.latestVersion().toString(); } getCurrentDirectory(): string { @@ -275,73 +263,22 @@ namespace ts.server { removeReferencedFile(info: ScriptInfo) { if (!info.isOpen) { - this.filenameToScript.remove(info.path); this.resolvedModuleNames.remove(info.path); this.resolvedTypeReferenceDirectives.remove(info.path); } } - getScriptInfo(filename: string): ScriptInfo { - const path = toPath(filename, this.host.getCurrentDirectory(), this.getCanonicalFileName); - let scriptInfo = this.filenameToScript.get(path); - if (!scriptInfo) { - scriptInfo = this.project.openReferencedFile(filename); - if (scriptInfo) { - this.filenameToScript.set(path, scriptInfo); - } - } - return scriptInfo; - } - - addRoot(info: ScriptInfo) { - if (!this.filenameToScript.contains(info.path)) { - this.filenameToScript.set(info.path, info); - this.roots.push(info); - } - } - removeRoot(info: ScriptInfo) { - if (!this.filenameToScript.contains(info.path)) { - this.filenameToScript.remove(info.path); - this.roots = copyListRemovingItem(info, this.roots); - this.resolvedModuleNames.remove(info.path); - this.resolvedTypeReferenceDirectives.remove(info.path); - } - } - - saveTo(filename: string, tmpfilename: string) { - const script = this.getScriptInfo(filename); - if (script) { - const snap = script.snap(); - this.host.writeFile(tmpfilename, snap.getText(0, snap.getLength())); - } - } - - reloadScript(filename: string, tmpfilename: string, cb: () => any) { - const script = this.getScriptInfo(filename); - if (script) { - script.svc.reloadFromFile(tmpfilename, cb); - } - } - - editScript(filename: string, start: number, end: number, newText: string) { - const script = this.getScriptInfo(filename); - if (script) { - script.editContent(start, end, newText); - return; - } - - throw new Error("No script with name '" + filename + "'"); + this.resolvedModuleNames.remove(info.path); + this.resolvedTypeReferenceDirectives.remove(info.path); } resolvePath(path: string): string { - const result = this.host.resolvePath(path); - return result; + return this.host.resolvePath(path); } fileExists(path: string): boolean { - const result = this.host.fileExists(path); - return result; + return this.host.fileExists(path); } directoryExists(path: string): boolean { @@ -356,7 +293,7 @@ namespace ts.server { } export class Project { - lsHost: LSHost; + private lsHost: LSHost; languageService: LanguageService; projectFilename: string; projectFileWatcher: FileWatcher; @@ -369,7 +306,14 @@ namespace ts.server { /** Used for configured projects which may have multiple open roots */ openRefCount = 0; + getCanonicalFileName: (fileName: string) => string; + + private rootFiles: ScriptInfo[] = []; + private pathToScriptInfo: ts.FileMap; + constructor(public projectService: ProjectService, documentRegistry: ts.DocumentRegistry, public projectOptions?: ProjectOptions) { + this.pathToScriptInfo = ts.createFileMap(); + this.getCanonicalFileName = ts.createGetCanonicalFileName(this.projectService.host.useCaseSensitiveFileNames); if (projectOptions && projectOptions.files) { // If files are listed explicitly, allow all extensions projectOptions.compilerOptions.allowNonTsExtensions = true; @@ -401,7 +345,7 @@ namespace ts.server { } getRootFiles() { - return this.lsHost.roots.map(info => info.fileName); + return this.rootFiles.map(info => info.fileName); } getFileNames() { @@ -423,11 +367,14 @@ namespace ts.server { } isRoot(info: ScriptInfo) { - return this.lsHost.roots.some(root => root === info); + return this.rootFiles.some(root => root === info); } removeReferencedFile(info: ScriptInfo) { - this.lsHost.removeReferencedFile(info); + if (!info.isOpen) { + this.pathToScriptInfo.remove(info.path); + this.lsHost.removeReferencedFile(info); + } this.updateGraph(); } @@ -456,12 +403,31 @@ namespace ts.server { // add a root file to project addRoot(info: ScriptInfo) { - this.lsHost.addRoot(info); + if (!this.pathToScriptInfo.contains(info.path)) { + this.pathToScriptInfo.set(info.path, info); + this.rootFiles.push(info); + } } // remove a root file from project removeRoot(info: ScriptInfo) { - this.lsHost.removeRoot(info); + if (!this.pathToScriptInfo.contains(info.path)) { + this.pathToScriptInfo.remove(info.path); + this.rootFiles = copyListRemovingItem(info, this.rootFiles); + this.lsHost.removeRoot(info); + } + } + + getScriptInfo(fileName: string) { + const path = toPath(fileName, this.projectService.host.getCurrentDirectory(), this.getCanonicalFileName); + let scriptInfo = this.pathToScriptInfo.get(path); + if (!scriptInfo) { + scriptInfo = this.openReferencedFile(fileName); + if (scriptInfo) { + this.pathToScriptInfo.set(path, scriptInfo); + } + } + return scriptInfo; } filesToString() { @@ -478,6 +444,30 @@ namespace ts.server { this.lsHost.setCompilationSettings(projectOptions.compilerOptions); } } + saveTo(filename: string, tmpfilename: string) { + const script = this.getScriptInfo(filename); + if (script) { + const snap = script.snap(); + this.projectService.host.writeFile(tmpfilename, snap.getText(0, snap.getLength())); + } + } + + reloadScript(filename: string, tmpfilename: string, cb: () => any) { + const script = this.getScriptInfo(filename); + if (script) { + script.svc.reloadFromFile(tmpfilename, cb); + } + } + + editScript(filename: string, start: number, end: number, newText: string) { + const script = this.getScriptInfo(filename); + if (script) { + script.editContent(start, end, newText); + return; + } + + throw new Error("No script with name '" + filename + "'"); + } } export interface ProjectOpenResult { @@ -1300,7 +1290,7 @@ namespace ts.server { return errors; } else { - const oldFileNames = project.lsHost.roots.map(info => info.fileName); + const oldFileNames = project.getRootFiles(); const newFileNames = ts.filter(projectOptions.files, f => this.host.fileExists(f)); const fileNamesToRemove = oldFileNames.filter(f => newFileNames.indexOf(f) < 0); const fileNamesToAdd = newFileNames.filter(f => oldFileNames.indexOf(f) < 0); diff --git a/src/server/session.ts b/src/server/session.ts index 99e11d41b89..249a484191a 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -69,7 +69,7 @@ namespace ts.server { } function formatDiag(fileName: string, project: Project, diag: ts.Diagnostic): protocol.Diagnostic { - const scriptInfo = project.lsHost.getScriptInfoForRootFile(fileName); + const scriptInfo = project.getScriptInfo(fileName); return { start: scriptInfo.positionToLineOffset(diag.start), end: scriptInfo.positionToLineOffset(diag.start + diag.length), @@ -318,7 +318,7 @@ namespace ts.server { throw Errors.NoProject; } - const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const scriptInfo = project.getScriptInfo(file); const position = scriptInfo.lineOffsetToPosition(line, offset); const definitions = project.languageService.getDefinitionAtPosition(file, position); @@ -327,12 +327,12 @@ namespace ts.server { } return definitions.map(def => { - const defScriptInfo = project.lsHost.getScriptInfoForRootFile(def.fileName); + const defScriptInfo = project.getScriptInfo(def.fileName); return { file: def.fileName, start: defScriptInfo.positionToLineOffset(def.textSpan.start), end: defScriptInfo.positionToLineOffset(ts.textSpanEnd(def.textSpan)) - } + }; }); } @@ -343,7 +343,7 @@ namespace ts.server { throw Errors.NoProject; } - const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const scriptInfo = project.getScriptInfo(file); const position = scriptInfo.lineOffsetToPosition(line, offset); const definitions = project.languageService.getTypeDefinitionAtPosition(file, position); @@ -352,12 +352,12 @@ namespace ts.server { } return definitions.map(def => { - const defScriptInfo = project.lsHost.getScriptInfoForRootFile(def.fileName); + const defScriptInfo = project.getScriptInfo(def.fileName); return { file: def.fileName, start: defScriptInfo.positionToLineOffset(def.textSpan.start), end: defScriptInfo.positionToLineOffset(ts.textSpanEnd(def.textSpan)) - } + }; }); } @@ -369,7 +369,7 @@ namespace ts.server { throw Errors.NoProject; } - const scriptInfo = project.lsHost.getScriptInfo(fileName); + const scriptInfo = project.getScriptInfo(fileName); const position = scriptInfo.lineOffsetToPosition(line, offset); const occurrences = project.languageService.getOccurrencesAtPosition(fileName, position); @@ -380,7 +380,7 @@ namespace ts.server { return occurrences.map(occurrence => { const { fileName, isWriteAccess, textSpan } = occurrence; - const scriptInfo = project.lsHost.getScriptInfo(fileName); + const scriptInfo = project.getScriptInfo(fileName); const start = scriptInfo.positionToLineOffset(textSpan.start); const end = scriptInfo.positionToLineOffset(ts.textSpanEnd(textSpan)); return { @@ -400,7 +400,7 @@ namespace ts.server { throw Errors.NoProject; } - const scriptInfo = project.lsHost.getScriptInfo(fileName); + const scriptInfo = project.getScriptInfo(fileName); const position = scriptInfo.lineOffsetToPosition(line, offset); const documentHighlights = project.languageService.getDocumentHighlights(fileName, position, filesToSearch); @@ -414,7 +414,7 @@ namespace ts.server { function convertToDocumentHighlightsItem(documentHighlights: ts.DocumentHighlights): ts.server.protocol.DocumentHighlightsItem { const { fileName, highlightSpans } = documentHighlights; - const scriptInfo = project.lsHost.getScriptInfo(fileName); + const scriptInfo = project.getScriptInfo(fileName); return { file: fileName, highlightSpans: highlightSpans.map(convertHighlightSpan) @@ -454,7 +454,7 @@ namespace ts.server { const defaultProject = projects[0]; // The rename info should be the same for every project - const scriptInfo = defaultProject.lsHost.getScriptInfoForRootFile(file); + const scriptInfo = defaultProject.getScriptInfo(file); const position = scriptInfo.lineOffsetToPosition(line, offset); const renameInfo = defaultProject.languageService.getRenameInfo(file, position); if (!renameInfo) { @@ -477,12 +477,12 @@ namespace ts.server { } return renameLocations.map(location => { - const locationScriptInfo = project.lsHost.getScriptInfoForRootFile(location.fileName); + const locationScriptInfo = project.getScriptInfo(location.fileName); return { file: location.fileName, start: locationScriptInfo.positionToLineOffset(location.textSpan.start), end: locationScriptInfo.positionToLineOffset(ts.textSpanEnd(location.textSpan)), - } + }; }); }, compareRenameLocation, @@ -537,7 +537,7 @@ namespace ts.server { } const defaultProject = projects[0]; - const scriptInfo = defaultProject.lsHost.getScriptInfoForRootFile(file); + const scriptInfo = defaultProject.getScriptInfo(file); const position = scriptInfo.lineOffsetToPosition(line, offset); const nameInfo = defaultProject.languageService.getQuickInfoAtPosition(file, position); if (!nameInfo) { @@ -557,7 +557,7 @@ namespace ts.server { } return references.map(ref => { - const refScriptInfo = project.lsHost.getScriptInfoForRootFile(ref.fileName); + const refScriptInfo = project.getScriptInfo(ref.fileName); const start = refScriptInfo.positionToLineOffset(ref.textSpan.start); const refLineSpan = refScriptInfo.lineToTextSpan(start.line - 1); const lineText = refScriptInfo.snap().getText(refLineSpan.start, ts.textSpanEnd(refLineSpan)).replace(/\r|\n/g, ""); @@ -610,7 +610,7 @@ namespace ts.server { throw Errors.NoProject; } - const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const scriptInfo = project.getScriptInfo(file); const position = scriptInfo.lineOffsetToPosition(line, offset); const quickInfo = project.languageService.getQuickInfoAtPosition(file, position); if (!quickInfo) { @@ -636,7 +636,7 @@ namespace ts.server { throw Errors.NoProject; } - const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const scriptInfo = project.getScriptInfo(file); const startPosition = scriptInfo.lineOffsetToPosition(line, offset); const endPosition = scriptInfo.lineOffsetToPosition(endLine, endOffset); @@ -664,7 +664,7 @@ namespace ts.server { throw Errors.NoProject; } - const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const scriptInfo = project.getScriptInfo(file); const position = scriptInfo.lineOffsetToPosition(line, offset); const formatOptions = this.projectService.getFormatCodeOptions(file); const edits = project.languageService.getFormattingEditsAfterKeystroke(file, position, key, @@ -737,7 +737,7 @@ namespace ts.server { throw Errors.NoProject; } - const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const scriptInfo = project.getScriptInfo(file); const position = scriptInfo.lineOffsetToPosition(line, offset); const completions = project.languageService.getCompletionsAtPosition(file, position); @@ -761,7 +761,7 @@ namespace ts.server { throw Errors.NoProject; } - const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const scriptInfo = project.getScriptInfo(file); const position = scriptInfo.lineOffsetToPosition(line, offset); return entryNames.reduce((accum: protocol.CompletionEntryDetails[], entryName: string) => { @@ -780,7 +780,7 @@ namespace ts.server { throw Errors.NoProject; } - const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const scriptInfo = project.getScriptInfo(file); const position = scriptInfo.lineOffsetToPosition(line, offset); const helpItems = project.languageService.getSignatureHelpItems(file, position); if (!helpItems) { @@ -821,7 +821,7 @@ namespace ts.server { const file = ts.normalizePath(fileName); const project = this.projectService.getProjectForFile(file); if (project) { - const scriptInfo = project.lsHost.getScriptInfoForRootFile(file); + const scriptInfo = project.getScriptInfo(file); const start = scriptInfo.lineOffsetToPosition(line, offset); const end = scriptInfo.lineOffsetToPosition(endLine, endOffset); if (start >= 0) { @@ -839,7 +839,7 @@ namespace ts.server { if (project) { this.changeSeq++; // make sure no changes happen before this one is finished - project.lsHost.reloadScript(file, tmpfile, () => { + project.reloadScript(file, tmpfile, () => { this.output(undefined, CommandNames.Reload, reqSeq); }); } @@ -851,7 +851,7 @@ namespace ts.server { const project = this.projectService.getProjectForFile(file); if (project) { - project.lsHost.saveTo(file, tmpfile); + project.saveTo(file, tmpfile); } } @@ -868,7 +868,7 @@ namespace ts.server { return undefined; } - const scriptInfo = project.lsHost.getScriptInfoForRootFile(fileName); + const scriptInfo = project.getScriptInfo(fileName); return items.map(item => ({ text: item.text, @@ -915,7 +915,7 @@ namespace ts.server { } return navItems.map((navItem) => { - const scriptInfo = project.lsHost.getScriptInfoForRootFile(navItem.fileName); + const scriptInfo = project.getScriptInfo(navItem.fileName); const start = scriptInfo.positionToLineOffset(navItem.textSpan.start); const end = scriptInfo.positionToLineOffset(ts.textSpanEnd(navItem.textSpan)); const bakedItem: protocol.NavtoItem = { @@ -963,7 +963,7 @@ namespace ts.server { throw Errors.NoProject; } - const scriptInfo = project.lsHost.getScriptInfoForRootFile(fileName); + const scriptInfo = project.getScriptInfo(fileName); const position = scriptInfo.lineOffsetToPosition(line, offset); const spans = project.languageService.getBraceMatchingAtPosition(file, position); From 34aa907988bacb0cf9927a14f1a667ca4ad05b2f Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 31 May 2016 16:51:00 -0700 Subject: [PATCH 004/307] move script version bits into the separate file --- Jakefile.js | 2 + src/server/editorServices.ts | 939 +----------------------------- src/server/scriptVersionCache.ts | 941 +++++++++++++++++++++++++++++++ 3 files changed, 946 insertions(+), 936 deletions(-) create mode 100644 src/server/scriptVersionCache.ts diff --git a/Jakefile.js b/Jakefile.js index 2f0ce4aa399..501892bf840 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -100,6 +100,7 @@ var servicesSources = [ var serverCoreSources = [ "node.d.ts", + "scriptVersionCache.ts", "editorServices.ts", "protocol.d.ts", "session.ts", @@ -160,6 +161,7 @@ var harnessSources = harnessCoreSources.concat([ "protocol.d.ts", "session.ts", "client.ts", + "scriptVersionCache.ts", "editorServices.ts" ].map(function (f) { return path.join(serverDirectory, f); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f2ab320c265..01f98a2b954 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -2,6 +2,7 @@ /// /// /// +/// namespace ts.server { export interface Logger { @@ -15,7 +16,6 @@ namespace ts.server { msg(s: string, type?: string): void; } - const lineCollectionCapacity = 4; function getDefaultFormatCodeOptions(host: ServerHost): ts.FormatCodeOptions { return ts.clone({ IndentSize: 4, @@ -444,7 +444,8 @@ namespace ts.server { this.lsHost.setCompilationSettings(projectOptions.compilerOptions); } } - saveTo(filename: string, tmpfilename: string) { + + saveTo(filename: string, tmpfilename: string) { const script = this.getScriptInfo(filename); if (script) { const snap = script.snap(); @@ -1338,939 +1339,5 @@ namespace ts.server { project.projectFilename = projectFilename; return project; } - - } - - export interface LineCollection { - charCount(): number; - lineCount(): number; - isLeaf(): boolean; - walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker): void; - } - - export interface ILineInfo { - line: number; - offset: number; - text?: string; - leaf?: LineLeaf; - } - - export enum CharRangeSection { - PreStart, - Start, - Entire, - Mid, - End, - PostEnd - } - - export interface ILineIndexWalker { - goSubtree: boolean; - done: boolean; - leaf(relativeStart: number, relativeLength: number, lineCollection: LineLeaf): void; - pre?(relativeStart: number, relativeLength: number, lineCollection: LineCollection, - parent: LineNode, nodeType: CharRangeSection): LineCollection; - post?(relativeStart: number, relativeLength: number, lineCollection: LineCollection, - parent: LineNode, nodeType: CharRangeSection): LineCollection; - } - - class BaseLineIndexWalker implements ILineIndexWalker { - goSubtree = true; - done = false; - leaf(rangeStart: number, rangeLength: number, ll: LineLeaf) { - } - } - - class EditWalker extends BaseLineIndexWalker { - lineIndex = new LineIndex(); - // path to start of range - startPath: LineCollection[]; - endBranch: LineCollection[] = []; - branchNode: LineNode; - // path to current node - stack: LineNode[]; - state = CharRangeSection.Entire; - lineCollectionAtBranch: LineCollection; - initialText = ""; - trailingText = ""; - suppressTrailingText = false; - - constructor() { - super(); - this.lineIndex.root = new LineNode(); - this.startPath = [this.lineIndex.root]; - this.stack = [this.lineIndex.root]; - } - - insertLines(insertedText: string) { - if (this.suppressTrailingText) { - this.trailingText = ""; - } - if (insertedText) { - insertedText = this.initialText + insertedText + this.trailingText; - } - else { - insertedText = this.initialText + this.trailingText; - } - const lm = LineIndex.linesFromText(insertedText); - const lines = lm.lines; - if (lines.length > 1) { - if (lines[lines.length - 1] == "") { - lines.length--; - } - } - let branchParent: LineNode; - let lastZeroCount: LineCollection; - - for (let k = this.endBranch.length - 1; k >= 0; k--) { - (this.endBranch[k]).updateCounts(); - if (this.endBranch[k].charCount() === 0) { - lastZeroCount = this.endBranch[k]; - if (k > 0) { - branchParent = this.endBranch[k - 1]; - } - else { - branchParent = this.branchNode; - } - } - } - if (lastZeroCount) { - branchParent.remove(lastZeroCount); - } - - // path at least length two (root and leaf) - let insertionNode = this.startPath[this.startPath.length - 2]; - const leafNode = this.startPath[this.startPath.length - 1]; - const len = lines.length; - - if (len > 0) { - leafNode.text = lines[0]; - - if (len > 1) { - let insertedNodes = new Array(len - 1); - let startNode = leafNode; - for (let i = 1, len = lines.length; i < len; i++) { - insertedNodes[i - 1] = new LineLeaf(lines[i]); - } - let pathIndex = this.startPath.length - 2; - while (pathIndex >= 0) { - insertionNode = this.startPath[pathIndex]; - insertedNodes = insertionNode.insertAt(startNode, insertedNodes); - pathIndex--; - startNode = insertionNode; - } - let insertedNodesLen = insertedNodes.length; - while (insertedNodesLen > 0) { - const newRoot = new LineNode(); - newRoot.add(this.lineIndex.root); - insertedNodes = newRoot.insertAt(this.lineIndex.root, insertedNodes); - insertedNodesLen = insertedNodes.length; - this.lineIndex.root = newRoot; - } - this.lineIndex.root.updateCounts(); - } - else { - for (let j = this.startPath.length - 2; j >= 0; j--) { - (this.startPath[j]).updateCounts(); - } - } - } - else { - // no content for leaf node, so delete it - insertionNode.remove(leafNode); - for (let j = this.startPath.length - 2; j >= 0; j--) { - (this.startPath[j]).updateCounts(); - } - } - - return this.lineIndex; - } - - post(relativeStart: number, relativeLength: number, lineCollection: LineCollection, parent: LineCollection, nodeType: CharRangeSection): LineCollection { - // have visited the path for start of range, now looking for end - // if range is on single line, we will never make this state transition - if (lineCollection === this.lineCollectionAtBranch) { - this.state = CharRangeSection.End; - } - // always pop stack because post only called when child has been visited - this.stack.length--; - return undefined; - } - - pre(relativeStart: number, relativeLength: number, lineCollection: LineCollection, parent: LineCollection, nodeType: CharRangeSection) { - // currentNode corresponds to parent, but in the new tree - const currentNode = this.stack[this.stack.length - 1]; - - if ((this.state === CharRangeSection.Entire) && (nodeType === CharRangeSection.Start)) { - // if range is on single line, we will never make this state transition - this.state = CharRangeSection.Start; - this.branchNode = currentNode; - this.lineCollectionAtBranch = lineCollection; - } - - let child: LineCollection; - function fresh(node: LineCollection): LineCollection { - if (node.isLeaf()) { - return new LineLeaf(""); - } - else return new LineNode(); - } - switch (nodeType) { - case CharRangeSection.PreStart: - this.goSubtree = false; - if (this.state !== CharRangeSection.End) { - currentNode.add(lineCollection); - } - break; - case CharRangeSection.Start: - if (this.state === CharRangeSection.End) { - this.goSubtree = false; - } - else { - child = fresh(lineCollection); - currentNode.add(child); - this.startPath[this.startPath.length] = child; - } - break; - case CharRangeSection.Entire: - if (this.state !== CharRangeSection.End) { - child = fresh(lineCollection); - currentNode.add(child); - this.startPath[this.startPath.length] = child; - } - else { - if (!lineCollection.isLeaf()) { - child = fresh(lineCollection); - currentNode.add(child); - this.endBranch[this.endBranch.length] = child; - } - } - break; - case CharRangeSection.Mid: - this.goSubtree = false; - break; - case CharRangeSection.End: - if (this.state !== CharRangeSection.End) { - this.goSubtree = false; - } - else { - if (!lineCollection.isLeaf()) { - child = fresh(lineCollection); - currentNode.add(child); - this.endBranch[this.endBranch.length] = child; - } - } - break; - case CharRangeSection.PostEnd: - this.goSubtree = false; - if (this.state !== CharRangeSection.Start) { - currentNode.add(lineCollection); - } - break; - } - if (this.goSubtree) { - this.stack[this.stack.length] = child; - } - return lineCollection; - } - // just gather text from the leaves - leaf(relativeStart: number, relativeLength: number, ll: LineLeaf) { - if (this.state === CharRangeSection.Start) { - this.initialText = ll.text.substring(0, relativeStart); - } - else if (this.state === CharRangeSection.Entire) { - this.initialText = ll.text.substring(0, relativeStart); - this.trailingText = ll.text.substring(relativeStart + relativeLength); - } - else { - // state is CharRangeSection.End - this.trailingText = ll.text.substring(relativeStart + relativeLength); - } - } - } - - // text change information - export class TextChange { - constructor(public pos: number, public deleteLen: number, public insertedText?: string) { - } - - getTextChangeRange() { - return ts.createTextChangeRange(ts.createTextSpan(this.pos, this.deleteLen), - this.insertedText ? this.insertedText.length : 0); - } - } - - export class ScriptVersionCache { - changes: TextChange[] = []; - versions: LineIndexSnapshot[] = []; - minVersion = 0; // no versions earlier than min version will maintain change history - private currentVersion = 0; - private host: ServerHost; - - static changeNumberThreshold = 8; - static changeLengthThreshold = 256; - static maxVersions = 8; - - // REVIEW: can optimize by coalescing simple edits - edit(pos: number, deleteLen: number, insertedText?: string) { - this.changes[this.changes.length] = new TextChange(pos, deleteLen, insertedText); - if ((this.changes.length > ScriptVersionCache.changeNumberThreshold) || - (deleteLen > ScriptVersionCache.changeLengthThreshold) || - (insertedText && (insertedText.length > ScriptVersionCache.changeLengthThreshold))) { - this.getSnapshot(); - } - } - - latest() { - return this.versions[this.currentVersion]; - } - - latestVersion() { - if (this.changes.length > 0) { - this.getSnapshot(); - } - return this.currentVersion; - } - - reloadFromFile(filename: string, cb?: () => any) { - let content = this.host.readFile(filename); - // If the file doesn't exist or cannot be read, we should - // wipe out its cached content on the server to avoid side effects. - if (!content) { - content = ""; - } - this.reload(content); - if (cb) - cb(); - } - - // reload whole script, leaving no change history behind reload - reload(script: string) { - this.currentVersion++; - this.changes = []; // history wiped out by reload - const snap = new LineIndexSnapshot(this.currentVersion, this); - this.versions[this.currentVersion] = snap; - snap.index = new LineIndex(); - const lm = LineIndex.linesFromText(script); - snap.index.load(lm.lines); - // REVIEW: could use linked list - for (let i = this.minVersion; i < this.currentVersion; i++) { - this.versions[i] = undefined; - } - this.minVersion = this.currentVersion; - - } - - getSnapshot() { - let snap = this.versions[this.currentVersion]; - if (this.changes.length > 0) { - let snapIndex = this.latest().index; - for (let i = 0, len = this.changes.length; i < len; i++) { - const change = this.changes[i]; - snapIndex = snapIndex.edit(change.pos, change.deleteLen, change.insertedText); - } - snap = new LineIndexSnapshot(this.currentVersion + 1, this); - snap.index = snapIndex; - snap.changesSincePreviousVersion = this.changes; - this.currentVersion = snap.version; - this.versions[snap.version] = snap; - this.changes = []; - if ((this.currentVersion - this.minVersion) >= ScriptVersionCache.maxVersions) { - const oldMin = this.minVersion; - this.minVersion = (this.currentVersion - ScriptVersionCache.maxVersions) + 1; - for (let j = oldMin; j < this.minVersion; j++) { - this.versions[j] = undefined; - } - } - } - return snap; - } - - getTextChangesBetweenVersions(oldVersion: number, newVersion: number) { - if (oldVersion < newVersion) { - if (oldVersion >= this.minVersion) { - const textChangeRanges: ts.TextChangeRange[] = []; - for (let i = oldVersion + 1; i <= newVersion; i++) { - const snap = this.versions[i]; - for (let j = 0, len = snap.changesSincePreviousVersion.length; j < len; j++) { - const textChange = snap.changesSincePreviousVersion[j]; - textChangeRanges[textChangeRanges.length] = textChange.getTextChangeRange(); - } - } - return ts.collapseTextChangeRangesAcrossMultipleVersions(textChangeRanges); - } - else { - return undefined; - } - } - else { - return ts.unchangedTextChangeRange; - } - } - - static fromString(host: ServerHost, script: string) { - const svc = new ScriptVersionCache(); - const snap = new LineIndexSnapshot(0, svc); - svc.versions[svc.currentVersion] = snap; - svc.host = host; - snap.index = new LineIndex(); - const lm = LineIndex.linesFromText(script); - snap.index.load(lm.lines); - return svc; - } - } - - export class LineIndexSnapshot implements ts.IScriptSnapshot { - index: LineIndex; - changesSincePreviousVersion: TextChange[] = []; - - constructor(public version: number, public cache: ScriptVersionCache) { - } - - getText(rangeStart: number, rangeEnd: number) { - return this.index.getText(rangeStart, rangeEnd - rangeStart); - } - - getLength() { - return this.index.root.charCount(); - } - - // this requires linear space so don't hold on to these - getLineStartPositions(): number[] { - const starts: number[] = [-1]; - let count = 1; - let pos = 0; - this.index.every((ll, s, len) => { - starts[count] = pos; - count++; - pos += ll.text.length; - return true; - }, 0); - return starts; - } - - getLineMapper() { - return (line: number) => { - return this.index.lineNumberToInfo(line).offset; - }; - } - - getTextChangeRangeSinceVersion(scriptVersion: number) { - if (this.version <= scriptVersion) { - return ts.unchangedTextChangeRange; - } - else { - return this.cache.getTextChangesBetweenVersions(scriptVersion, this.version); - } - } - getChangeRange(oldSnapshot: ts.IScriptSnapshot): ts.TextChangeRange { - const oldSnap = oldSnapshot; - return this.getTextChangeRangeSinceVersion(oldSnap.version); - } - } - - export class LineIndex { - root: LineNode; - // set this to true to check each edit for accuracy - checkEdits = false; - - charOffsetToLineNumberAndPos(charOffset: number) { - return this.root.charOffsetToLineNumberAndPos(1, charOffset); - } - - lineNumberToInfo(lineNumber: number): ILineInfo { - const lineCount = this.root.lineCount(); - if (lineNumber <= lineCount) { - const lineInfo = this.root.lineNumberToInfo(lineNumber, 0); - lineInfo.line = lineNumber; - return lineInfo; - } - else { - return { - line: lineNumber, - offset: this.root.charCount() - }; - } - } - - load(lines: string[]) { - if (lines.length > 0) { - const leaves: LineLeaf[] = []; - for (let i = 0, len = lines.length; i < len; i++) { - leaves[i] = new LineLeaf(lines[i]); - } - this.root = LineIndex.buildTreeFromBottom(leaves); - } - else { - this.root = new LineNode(); - } - } - - walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker) { - this.root.walk(rangeStart, rangeLength, walkFns); - } - - getText(rangeStart: number, rangeLength: number) { - let accum = ""; - if ((rangeLength > 0) && (rangeStart < this.root.charCount())) { - this.walk(rangeStart, rangeLength, { - goSubtree: true, - done: false, - leaf: (relativeStart: number, relativeLength: number, ll: LineLeaf) => { - accum = accum.concat(ll.text.substring(relativeStart, relativeStart + relativeLength)); - } - }); - } - return accum; - } - - getLength(): number { - return this.root.charCount(); - } - - every(f: (ll: LineLeaf, s: number, len: number) => boolean, rangeStart: number, rangeEnd?: number) { - if (!rangeEnd) { - rangeEnd = this.root.charCount(); - } - const walkFns = { - goSubtree: true, - done: false, - leaf: function (relativeStart: number, relativeLength: number, ll: LineLeaf) { - if (!f(ll, relativeStart, relativeLength)) { - this.done = true; - } - } - }; - this.walk(rangeStart, rangeEnd - rangeStart, walkFns); - return !walkFns.done; - } - - edit(pos: number, deleteLength: number, newText?: string) { - function editFlat(source: string, s: number, dl: number, nt = "") { - return source.substring(0, s) + nt + source.substring(s + dl, source.length); - } - if (this.root.charCount() === 0) { - // TODO: assert deleteLength === 0 - if (newText) { - this.load(LineIndex.linesFromText(newText).lines); - return this; - } - } - else { - let checkText: string; - if (this.checkEdits) { - checkText = editFlat(this.getText(0, this.root.charCount()), pos, deleteLength, newText); - } - const walker = new EditWalker(); - if (pos >= this.root.charCount()) { - // insert at end - pos = this.root.charCount() - 1; - const endString = this.getText(pos, 1); - if (newText) { - newText = endString + newText; - } - else { - newText = endString; - } - deleteLength = 0; - walker.suppressTrailingText = true; - } - else if (deleteLength > 0) { - // check whether last characters deleted are line break - const e = pos + deleteLength; - const lineInfo = this.charOffsetToLineNumberAndPos(e); - if ((lineInfo && (lineInfo.offset === 0))) { - // move range end just past line that will merge with previous line - deleteLength += lineInfo.text.length; - // store text by appending to end of insertedText - if (newText) { - newText = newText + lineInfo.text; - } - else { - newText = lineInfo.text; - } - } - } - if (pos < this.root.charCount()) { - this.root.walk(pos, deleteLength, walker); - walker.insertLines(newText); - } - if (this.checkEdits) { - const updatedText = this.getText(0, this.root.charCount()); - Debug.assert(checkText == updatedText, "buffer edit mismatch"); - } - return walker.lineIndex; - } - } - - static buildTreeFromBottom(nodes: LineCollection[]): LineNode { - const nodeCount = Math.ceil(nodes.length / lineCollectionCapacity); - const interiorNodes: LineNode[] = []; - let nodeIndex = 0; - for (let i = 0; i < nodeCount; i++) { - interiorNodes[i] = new LineNode(); - let charCount = 0; - let lineCount = 0; - for (let j = 0; j < lineCollectionCapacity; j++) { - if (nodeIndex < nodes.length) { - interiorNodes[i].add(nodes[nodeIndex]); - charCount += nodes[nodeIndex].charCount(); - lineCount += nodes[nodeIndex].lineCount(); - } - else { - break; - } - nodeIndex++; - } - interiorNodes[i].totalChars = charCount; - interiorNodes[i].totalLines = lineCount; - } - if (interiorNodes.length === 1) { - return interiorNodes[0]; - } - else { - return this.buildTreeFromBottom(interiorNodes); - } - } - - static linesFromText(text: string) { - const lineStarts = ts.computeLineStarts(text); - - if (lineStarts.length === 0) { - return { lines: [], lineMap: lineStarts }; - } - const lines = new Array(lineStarts.length); - const lc = lineStarts.length - 1; - for (let lmi = 0; lmi < lc; lmi++) { - lines[lmi] = text.substring(lineStarts[lmi], lineStarts[lmi + 1]); - } - - const endText = text.substring(lineStarts[lc]); - if (endText.length > 0) { - lines[lc] = endText; - } - else { - lines.length--; - } - return { lines: lines, lineMap: lineStarts }; - } - } - - export class LineNode implements LineCollection { - totalChars = 0; - totalLines = 0; - children: LineCollection[] = []; - - isLeaf() { - return false; - } - - updateCounts() { - this.totalChars = 0; - this.totalLines = 0; - for (let i = 0, len = this.children.length; i < len; i++) { - const child = this.children[i]; - this.totalChars += child.charCount(); - this.totalLines += child.lineCount(); - } - } - - execWalk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker, childIndex: number, nodeType: CharRangeSection) { - if (walkFns.pre) { - walkFns.pre(rangeStart, rangeLength, this.children[childIndex], this, nodeType); - } - if (walkFns.goSubtree) { - this.children[childIndex].walk(rangeStart, rangeLength, walkFns); - if (walkFns.post) { - walkFns.post(rangeStart, rangeLength, this.children[childIndex], this, nodeType); - } - } - else { - walkFns.goSubtree = true; - } - return walkFns.done; - } - - skipChild(relativeStart: number, relativeLength: number, childIndex: number, walkFns: ILineIndexWalker, nodeType: CharRangeSection) { - if (walkFns.pre && (!walkFns.done)) { - walkFns.pre(relativeStart, relativeLength, this.children[childIndex], this, nodeType); - walkFns.goSubtree = true; - } - } - - walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker) { - // assume (rangeStart < this.totalChars) && (rangeLength <= this.totalChars) - let childIndex = 0; - let child = this.children[0]; - let childCharCount = child.charCount(); - // find sub-tree containing start - let adjustedStart = rangeStart; - while (adjustedStart >= childCharCount) { - this.skipChild(adjustedStart, rangeLength, childIndex, walkFns, CharRangeSection.PreStart); - adjustedStart -= childCharCount; - childIndex++; - child = this.children[childIndex]; - childCharCount = child.charCount(); - } - // Case I: both start and end of range in same subtree - if ((adjustedStart + rangeLength) <= childCharCount) { - if (this.execWalk(adjustedStart, rangeLength, walkFns, childIndex, CharRangeSection.Entire)) { - return; - } - } - else { - // Case II: start and end of range in different subtrees (possibly with subtrees in the middle) - if (this.execWalk(adjustedStart, childCharCount - adjustedStart, walkFns, childIndex, CharRangeSection.Start)) { - return; - } - let adjustedLength = rangeLength - (childCharCount - adjustedStart); - childIndex++; - child = this.children[childIndex]; - childCharCount = child.charCount(); - while (adjustedLength > childCharCount) { - if (this.execWalk(0, childCharCount, walkFns, childIndex, CharRangeSection.Mid)) { - return; - } - adjustedLength -= childCharCount; - childIndex++; - child = this.children[childIndex]; - childCharCount = child.charCount(); - } - if (adjustedLength > 0) { - if (this.execWalk(0, adjustedLength, walkFns, childIndex, CharRangeSection.End)) { - return; - } - } - } - // Process any subtrees after the one containing range end - if (walkFns.pre) { - const clen = this.children.length; - if (childIndex < (clen - 1)) { - for (let ej = childIndex + 1; ej < clen; ej++) { - this.skipChild(0, 0, ej, walkFns, CharRangeSection.PostEnd); - } - } - } - } - - charOffsetToLineNumberAndPos(lineNumber: number, charOffset: number): ILineInfo { - const childInfo = this.childFromCharOffset(lineNumber, charOffset); - if (!childInfo.child) { - return { - line: lineNumber, - offset: charOffset, - }; - } - else if (childInfo.childIndex < this.children.length) { - if (childInfo.child.isLeaf()) { - return { - line: childInfo.lineNumber, - offset: childInfo.charOffset, - text: ((childInfo.child)).text, - leaf: ((childInfo.child)) - }; - } - else { - const lineNode = (childInfo.child); - return lineNode.charOffsetToLineNumberAndPos(childInfo.lineNumber, childInfo.charOffset); - } - } - else { - const lineInfo = this.lineNumberToInfo(this.lineCount(), 0); - return { line: this.lineCount(), offset: lineInfo.leaf.charCount() }; - } - } - - lineNumberToInfo(lineNumber: number, charOffset: number): ILineInfo { - const childInfo = this.childFromLineNumber(lineNumber, charOffset); - if (!childInfo.child) { - return { - line: lineNumber, - offset: charOffset - }; - } - else if (childInfo.child.isLeaf()) { - return { - line: lineNumber, - offset: childInfo.charOffset, - text: ((childInfo.child)).text, - leaf: ((childInfo.child)) - }; - } - else { - const lineNode = (childInfo.child); - return lineNode.lineNumberToInfo(childInfo.relativeLineNumber, childInfo.charOffset); - } - } - - childFromLineNumber(lineNumber: number, charOffset: number) { - let child: LineCollection; - let relativeLineNumber = lineNumber; - let i: number; - let len: number; - for (i = 0, len = this.children.length; i < len; i++) { - child = this.children[i]; - const childLineCount = child.lineCount(); - if (childLineCount >= relativeLineNumber) { - break; - } - else { - relativeLineNumber -= childLineCount; - charOffset += child.charCount(); - } - } - return { - child: child, - childIndex: i, - relativeLineNumber: relativeLineNumber, - charOffset: charOffset - }; - } - - childFromCharOffset(lineNumber: number, charOffset: number) { - let child: LineCollection; - let i: number; - let len: number; - for (i = 0, len = this.children.length; i < len; i++) { - child = this.children[i]; - if (child.charCount() > charOffset) { - break; - } - else { - charOffset -= child.charCount(); - lineNumber += child.lineCount(); - } - } - return { - child: child, - childIndex: i, - charOffset: charOffset, - lineNumber: lineNumber - }; - } - - splitAfter(childIndex: number) { - let splitNode: LineNode; - const clen = this.children.length; - childIndex++; - const endLength = childIndex; - if (childIndex < clen) { - splitNode = new LineNode(); - while (childIndex < clen) { - splitNode.add(this.children[childIndex]); - childIndex++; - } - splitNode.updateCounts(); - } - this.children.length = endLength; - return splitNode; - } - - remove(child: LineCollection) { - const childIndex = this.findChildIndex(child); - const clen = this.children.length; - if (childIndex < (clen - 1)) { - for (let i = childIndex; i < (clen - 1); i++) { - this.children[i] = this.children[i + 1]; - } - } - this.children.length--; - } - - findChildIndex(child: LineCollection) { - let childIndex = 0; - const clen = this.children.length; - while ((this.children[childIndex] !== child) && (childIndex < clen)) childIndex++; - return childIndex; - } - - insertAt(child: LineCollection, nodes: LineCollection[]) { - let childIndex = this.findChildIndex(child); - const clen = this.children.length; - const nodeCount = nodes.length; - // if child is last and there is more room and only one node to place, place it - if ((clen < lineCollectionCapacity) && (childIndex === (clen - 1)) && (nodeCount === 1)) { - this.add(nodes[0]); - this.updateCounts(); - return []; - } - else { - const shiftNode = this.splitAfter(childIndex); - let nodeIndex = 0; - childIndex++; - while ((childIndex < lineCollectionCapacity) && (nodeIndex < nodeCount)) { - this.children[childIndex] = nodes[nodeIndex]; - childIndex++; - nodeIndex++; - } - let splitNodes: LineNode[] = []; - let splitNodeCount = 0; - if (nodeIndex < nodeCount) { - splitNodeCount = Math.ceil((nodeCount - nodeIndex) / lineCollectionCapacity); - splitNodes = new Array(splitNodeCount); - let splitNodeIndex = 0; - for (let i = 0; i < splitNodeCount; i++) { - splitNodes[i] = new LineNode(); - } - let splitNode = splitNodes[0]; - while (nodeIndex < nodeCount) { - splitNode.add(nodes[nodeIndex]); - nodeIndex++; - if (splitNode.children.length === lineCollectionCapacity) { - splitNodeIndex++; - splitNode = splitNodes[splitNodeIndex]; - } - } - for (let i = splitNodes.length - 1; i >= 0; i--) { - if (splitNodes[i].children.length === 0) { - splitNodes.length--; - } - } - } - if (shiftNode) { - splitNodes[splitNodes.length] = shiftNode; - } - this.updateCounts(); - for (let i = 0; i < splitNodeCount; i++) { - (splitNodes[i]).updateCounts(); - } - return splitNodes; - } - } - - // assume there is room for the item; return true if more room - add(collection: LineCollection) { - this.children[this.children.length] = collection; - return (this.children.length < lineCollectionCapacity); - } - - charCount() { - return this.totalChars; - } - - lineCount() { - return this.totalLines; - } - } - - export class LineLeaf implements LineCollection { - constructor(public text: string) { - } - - isLeaf() { - return true; - } - - walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker) { - walkFns.leaf(rangeStart, rangeLength, this); - } - - charCount() { - return this.text.length; - } - - lineCount() { - return 1; - } } } diff --git a/src/server/scriptVersionCache.ts b/src/server/scriptVersionCache.ts new file mode 100644 index 00000000000..4acb99b66d2 --- /dev/null +++ b/src/server/scriptVersionCache.ts @@ -0,0 +1,941 @@ +/// +/// +/// +/// + +namespace ts.server { + const lineCollectionCapacity = 4; + + export interface LineCollection { + charCount(): number; + lineCount(): number; + isLeaf(): boolean; + walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker): void; + } + + export interface ILineInfo { + line: number; + offset: number; + text?: string; + leaf?: LineLeaf; + } + + export enum CharRangeSection { + PreStart, + Start, + Entire, + Mid, + End, + PostEnd + } + + export interface ILineIndexWalker { + goSubtree: boolean; + done: boolean; + leaf(relativeStart: number, relativeLength: number, lineCollection: LineLeaf): void; + pre?(relativeStart: number, relativeLength: number, lineCollection: LineCollection, + parent: LineNode, nodeType: CharRangeSection): LineCollection; + post?(relativeStart: number, relativeLength: number, lineCollection: LineCollection, + parent: LineNode, nodeType: CharRangeSection): LineCollection; + } + + class BaseLineIndexWalker implements ILineIndexWalker { + goSubtree = true; + done = false; + leaf(rangeStart: number, rangeLength: number, ll: LineLeaf) { + } + } + + class EditWalker extends BaseLineIndexWalker { + lineIndex = new LineIndex(); + // path to start of range + startPath: LineCollection[]; + endBranch: LineCollection[] = []; + branchNode: LineNode; + // path to current node + stack: LineNode[]; + state = CharRangeSection.Entire; + lineCollectionAtBranch: LineCollection; + initialText = ""; + trailingText = ""; + suppressTrailingText = false; + + constructor() { + super(); + this.lineIndex.root = new LineNode(); + this.startPath = [this.lineIndex.root]; + this.stack = [this.lineIndex.root]; + } + + insertLines(insertedText: string) { + if (this.suppressTrailingText) { + this.trailingText = ""; + } + if (insertedText) { + insertedText = this.initialText + insertedText + this.trailingText; + } + else { + insertedText = this.initialText + this.trailingText; + } + const lm = LineIndex.linesFromText(insertedText); + const lines = lm.lines; + if (lines.length > 1) { + if (lines[lines.length - 1] == "") { + lines.length--; + } + } + let branchParent: LineNode; + let lastZeroCount: LineCollection; + + for (let k = this.endBranch.length - 1; k >= 0; k--) { + (this.endBranch[k]).updateCounts(); + if (this.endBranch[k].charCount() === 0) { + lastZeroCount = this.endBranch[k]; + if (k > 0) { + branchParent = this.endBranch[k - 1]; + } + else { + branchParent = this.branchNode; + } + } + } + if (lastZeroCount) { + branchParent.remove(lastZeroCount); + } + + // path at least length two (root and leaf) + let insertionNode = this.startPath[this.startPath.length - 2]; + const leafNode = this.startPath[this.startPath.length - 1]; + const len = lines.length; + + if (len > 0) { + leafNode.text = lines[0]; + + if (len > 1) { + let insertedNodes = new Array(len - 1); + let startNode = leafNode; + for (let i = 1, len = lines.length; i < len; i++) { + insertedNodes[i - 1] = new LineLeaf(lines[i]); + } + let pathIndex = this.startPath.length - 2; + while (pathIndex >= 0) { + insertionNode = this.startPath[pathIndex]; + insertedNodes = insertionNode.insertAt(startNode, insertedNodes); + pathIndex--; + startNode = insertionNode; + } + let insertedNodesLen = insertedNodes.length; + while (insertedNodesLen > 0) { + const newRoot = new LineNode(); + newRoot.add(this.lineIndex.root); + insertedNodes = newRoot.insertAt(this.lineIndex.root, insertedNodes); + insertedNodesLen = insertedNodes.length; + this.lineIndex.root = newRoot; + } + this.lineIndex.root.updateCounts(); + } + else { + for (let j = this.startPath.length - 2; j >= 0; j--) { + (this.startPath[j]).updateCounts(); + } + } + } + else { + // no content for leaf node, so delete it + insertionNode.remove(leafNode); + for (let j = this.startPath.length - 2; j >= 0; j--) { + (this.startPath[j]).updateCounts(); + } + } + + return this.lineIndex; + } + + post(relativeStart: number, relativeLength: number, lineCollection: LineCollection, parent: LineCollection, nodeType: CharRangeSection): LineCollection { + // have visited the path for start of range, now looking for end + // if range is on single line, we will never make this state transition + if (lineCollection === this.lineCollectionAtBranch) { + this.state = CharRangeSection.End; + } + // always pop stack because post only called when child has been visited + this.stack.length--; + return undefined; + } + + pre(relativeStart: number, relativeLength: number, lineCollection: LineCollection, parent: LineCollection, nodeType: CharRangeSection) { + // currentNode corresponds to parent, but in the new tree + const currentNode = this.stack[this.stack.length - 1]; + + if ((this.state === CharRangeSection.Entire) && (nodeType === CharRangeSection.Start)) { + // if range is on single line, we will never make this state transition + this.state = CharRangeSection.Start; + this.branchNode = currentNode; + this.lineCollectionAtBranch = lineCollection; + } + + let child: LineCollection; + function fresh(node: LineCollection): LineCollection { + if (node.isLeaf()) { + return new LineLeaf(""); + } + else return new LineNode(); + } + switch (nodeType) { + case CharRangeSection.PreStart: + this.goSubtree = false; + if (this.state !== CharRangeSection.End) { + currentNode.add(lineCollection); + } + break; + case CharRangeSection.Start: + if (this.state === CharRangeSection.End) { + this.goSubtree = false; + } + else { + child = fresh(lineCollection); + currentNode.add(child); + this.startPath[this.startPath.length] = child; + } + break; + case CharRangeSection.Entire: + if (this.state !== CharRangeSection.End) { + child = fresh(lineCollection); + currentNode.add(child); + this.startPath[this.startPath.length] = child; + } + else { + if (!lineCollection.isLeaf()) { + child = fresh(lineCollection); + currentNode.add(child); + this.endBranch[this.endBranch.length] = child; + } + } + break; + case CharRangeSection.Mid: + this.goSubtree = false; + break; + case CharRangeSection.End: + if (this.state !== CharRangeSection.End) { + this.goSubtree = false; + } + else { + if (!lineCollection.isLeaf()) { + child = fresh(lineCollection); + currentNode.add(child); + this.endBranch[this.endBranch.length] = child; + } + } + break; + case CharRangeSection.PostEnd: + this.goSubtree = false; + if (this.state !== CharRangeSection.Start) { + currentNode.add(lineCollection); + } + break; + } + if (this.goSubtree) { + this.stack[this.stack.length] = child; + } + return lineCollection; + } + // just gather text from the leaves + leaf(relativeStart: number, relativeLength: number, ll: LineLeaf) { + if (this.state === CharRangeSection.Start) { + this.initialText = ll.text.substring(0, relativeStart); + } + else if (this.state === CharRangeSection.Entire) { + this.initialText = ll.text.substring(0, relativeStart); + this.trailingText = ll.text.substring(relativeStart + relativeLength); + } + else { + // state is CharRangeSection.End + this.trailingText = ll.text.substring(relativeStart + relativeLength); + } + } + } + + // text change information + export class TextChange { + constructor(public pos: number, public deleteLen: number, public insertedText?: string) { + } + + getTextChangeRange() { + return ts.createTextChangeRange(ts.createTextSpan(this.pos, this.deleteLen), + this.insertedText ? this.insertedText.length : 0); + } + } + + export class ScriptVersionCache { + changes: TextChange[] = []; + versions: LineIndexSnapshot[] = []; + minVersion = 0; // no versions earlier than min version will maintain change history + private currentVersion = 0; + private host: ServerHost; + + static changeNumberThreshold = 8; + static changeLengthThreshold = 256; + static maxVersions = 8; + + // REVIEW: can optimize by coalescing simple edits + edit(pos: number, deleteLen: number, insertedText?: string) { + this.changes[this.changes.length] = new TextChange(pos, deleteLen, insertedText); + if ((this.changes.length > ScriptVersionCache.changeNumberThreshold) || + (deleteLen > ScriptVersionCache.changeLengthThreshold) || + (insertedText && (insertedText.length > ScriptVersionCache.changeLengthThreshold))) { + this.getSnapshot(); + } + } + + latest() { + return this.versions[this.currentVersion]; + } + + latestVersion() { + if (this.changes.length > 0) { + this.getSnapshot(); + } + return this.currentVersion; + } + + reloadFromFile(filename: string, cb?: () => any) { + let content = this.host.readFile(filename); + // If the file doesn't exist or cannot be read, we should + // wipe out its cached content on the server to avoid side effects. + if (!content) { + content = ""; + } + this.reload(content); + if (cb) + cb(); + } + + // reload whole script, leaving no change history behind reload + reload(script: string) { + this.currentVersion++; + this.changes = []; // history wiped out by reload + const snap = new LineIndexSnapshot(this.currentVersion, this); + this.versions[this.currentVersion] = snap; + snap.index = new LineIndex(); + const lm = LineIndex.linesFromText(script); + snap.index.load(lm.lines); + // REVIEW: could use linked list + for (let i = this.minVersion; i < this.currentVersion; i++) { + this.versions[i] = undefined; + } + this.minVersion = this.currentVersion; + + } + + getSnapshot() { + let snap = this.versions[this.currentVersion]; + if (this.changes.length > 0) { + let snapIndex = this.latest().index; + for (let i = 0, len = this.changes.length; i < len; i++) { + const change = this.changes[i]; + snapIndex = snapIndex.edit(change.pos, change.deleteLen, change.insertedText); + } + snap = new LineIndexSnapshot(this.currentVersion + 1, this); + snap.index = snapIndex; + snap.changesSincePreviousVersion = this.changes; + this.currentVersion = snap.version; + this.versions[snap.version] = snap; + this.changes = []; + if ((this.currentVersion - this.minVersion) >= ScriptVersionCache.maxVersions) { + const oldMin = this.minVersion; + this.minVersion = (this.currentVersion - ScriptVersionCache.maxVersions) + 1; + for (let j = oldMin; j < this.minVersion; j++) { + this.versions[j] = undefined; + } + } + } + return snap; + } + + getTextChangesBetweenVersions(oldVersion: number, newVersion: number) { + if (oldVersion < newVersion) { + if (oldVersion >= this.minVersion) { + const textChangeRanges: ts.TextChangeRange[] = []; + for (let i = oldVersion + 1; i <= newVersion; i++) { + const snap = this.versions[i]; + for (let j = 0, len = snap.changesSincePreviousVersion.length; j < len; j++) { + const textChange = snap.changesSincePreviousVersion[j]; + textChangeRanges[textChangeRanges.length] = textChange.getTextChangeRange(); + } + } + return ts.collapseTextChangeRangesAcrossMultipleVersions(textChangeRanges); + } + else { + return undefined; + } + } + else { + return ts.unchangedTextChangeRange; + } + } + + static fromString(host: ServerHost, script: string) { + const svc = new ScriptVersionCache(); + const snap = new LineIndexSnapshot(0, svc); + svc.versions[svc.currentVersion] = snap; + svc.host = host; + snap.index = new LineIndex(); + const lm = LineIndex.linesFromText(script); + snap.index.load(lm.lines); + return svc; + } + } + + export class LineIndexSnapshot implements ts.IScriptSnapshot { + index: LineIndex; + changesSincePreviousVersion: TextChange[] = []; + + constructor(public version: number, public cache: ScriptVersionCache) { + } + + getText(rangeStart: number, rangeEnd: number) { + return this.index.getText(rangeStart, rangeEnd - rangeStart); + } + + getLength() { + return this.index.root.charCount(); + } + + // this requires linear space so don't hold on to these + getLineStartPositions(): number[] { + const starts: number[] = [-1]; + let count = 1; + let pos = 0; + this.index.every((ll, s, len) => { + starts[count] = pos; + count++; + pos += ll.text.length; + return true; + }, 0); + return starts; + } + + getLineMapper() { + return (line: number) => { + return this.index.lineNumberToInfo(line).offset; + }; + } + + getTextChangeRangeSinceVersion(scriptVersion: number) { + if (this.version <= scriptVersion) { + return ts.unchangedTextChangeRange; + } + else { + return this.cache.getTextChangesBetweenVersions(scriptVersion, this.version); + } + } + getChangeRange(oldSnapshot: ts.IScriptSnapshot): ts.TextChangeRange { + const oldSnap = oldSnapshot; + return this.getTextChangeRangeSinceVersion(oldSnap.version); + } + } + + export class LineIndex { + root: LineNode; + // set this to true to check each edit for accuracy + checkEdits = false; + + charOffsetToLineNumberAndPos(charOffset: number) { + return this.root.charOffsetToLineNumberAndPos(1, charOffset); + } + + lineNumberToInfo(lineNumber: number): ILineInfo { + const lineCount = this.root.lineCount(); + if (lineNumber <= lineCount) { + const lineInfo = this.root.lineNumberToInfo(lineNumber, 0); + lineInfo.line = lineNumber; + return lineInfo; + } + else { + return { + line: lineNumber, + offset: this.root.charCount() + }; + } + } + + load(lines: string[]) { + if (lines.length > 0) { + const leaves: LineLeaf[] = []; + for (let i = 0, len = lines.length; i < len; i++) { + leaves[i] = new LineLeaf(lines[i]); + } + this.root = LineIndex.buildTreeFromBottom(leaves); + } + else { + this.root = new LineNode(); + } + } + + walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker) { + this.root.walk(rangeStart, rangeLength, walkFns); + } + + getText(rangeStart: number, rangeLength: number) { + let accum = ""; + if ((rangeLength > 0) && (rangeStart < this.root.charCount())) { + this.walk(rangeStart, rangeLength, { + goSubtree: true, + done: false, + leaf: (relativeStart: number, relativeLength: number, ll: LineLeaf) => { + accum = accum.concat(ll.text.substring(relativeStart, relativeStart + relativeLength)); + } + }); + } + return accum; + } + + getLength(): number { + return this.root.charCount(); + } + + every(f: (ll: LineLeaf, s: number, len: number) => boolean, rangeStart: number, rangeEnd?: number) { + if (!rangeEnd) { + rangeEnd = this.root.charCount(); + } + const walkFns = { + goSubtree: true, + done: false, + leaf: function (relativeStart: number, relativeLength: number, ll: LineLeaf) { + if (!f(ll, relativeStart, relativeLength)) { + this.done = true; + } + } + }; + this.walk(rangeStart, rangeEnd - rangeStart, walkFns); + return !walkFns.done; + } + + edit(pos: number, deleteLength: number, newText?: string) { + function editFlat(source: string, s: number, dl: number, nt = "") { + return source.substring(0, s) + nt + source.substring(s + dl, source.length); + } + if (this.root.charCount() === 0) { + // TODO: assert deleteLength === 0 + if (newText) { + this.load(LineIndex.linesFromText(newText).lines); + return this; + } + } + else { + let checkText: string; + if (this.checkEdits) { + checkText = editFlat(this.getText(0, this.root.charCount()), pos, deleteLength, newText); + } + const walker = new EditWalker(); + if (pos >= this.root.charCount()) { + // insert at end + pos = this.root.charCount() - 1; + const endString = this.getText(pos, 1); + if (newText) { + newText = endString + newText; + } + else { + newText = endString; + } + deleteLength = 0; + walker.suppressTrailingText = true; + } + else if (deleteLength > 0) { + // check whether last characters deleted are line break + const e = pos + deleteLength; + const lineInfo = this.charOffsetToLineNumberAndPos(e); + if ((lineInfo && (lineInfo.offset === 0))) { + // move range end just past line that will merge with previous line + deleteLength += lineInfo.text.length; + // store text by appending to end of insertedText + if (newText) { + newText = newText + lineInfo.text; + } + else { + newText = lineInfo.text; + } + } + } + if (pos < this.root.charCount()) { + this.root.walk(pos, deleteLength, walker); + walker.insertLines(newText); + } + if (this.checkEdits) { + const updatedText = this.getText(0, this.root.charCount()); + Debug.assert(checkText == updatedText, "buffer edit mismatch"); + } + return walker.lineIndex; + } + } + + static buildTreeFromBottom(nodes: LineCollection[]): LineNode { + const nodeCount = Math.ceil(nodes.length / lineCollectionCapacity); + const interiorNodes: LineNode[] = []; + let nodeIndex = 0; + for (let i = 0; i < nodeCount; i++) { + interiorNodes[i] = new LineNode(); + let charCount = 0; + let lineCount = 0; + for (let j = 0; j < lineCollectionCapacity; j++) { + if (nodeIndex < nodes.length) { + interiorNodes[i].add(nodes[nodeIndex]); + charCount += nodes[nodeIndex].charCount(); + lineCount += nodes[nodeIndex].lineCount(); + } + else { + break; + } + nodeIndex++; + } + interiorNodes[i].totalChars = charCount; + interiorNodes[i].totalLines = lineCount; + } + if (interiorNodes.length === 1) { + return interiorNodes[0]; + } + else { + return this.buildTreeFromBottom(interiorNodes); + } + } + + static linesFromText(text: string) { + const lineStarts = ts.computeLineStarts(text); + + if (lineStarts.length === 0) { + return { lines: [], lineMap: lineStarts }; + } + const lines = new Array(lineStarts.length); + const lc = lineStarts.length - 1; + for (let lmi = 0; lmi < lc; lmi++) { + lines[lmi] = text.substring(lineStarts[lmi], lineStarts[lmi + 1]); + } + + const endText = text.substring(lineStarts[lc]); + if (endText.length > 0) { + lines[lc] = endText; + } + else { + lines.length--; + } + return { lines: lines, lineMap: lineStarts }; + } + } + + export class LineNode implements LineCollection { + totalChars = 0; + totalLines = 0; + children: LineCollection[] = []; + + isLeaf() { + return false; + } + + updateCounts() { + this.totalChars = 0; + this.totalLines = 0; + for (let i = 0, len = this.children.length; i < len; i++) { + const child = this.children[i]; + this.totalChars += child.charCount(); + this.totalLines += child.lineCount(); + } + } + + execWalk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker, childIndex: number, nodeType: CharRangeSection) { + if (walkFns.pre) { + walkFns.pre(rangeStart, rangeLength, this.children[childIndex], this, nodeType); + } + if (walkFns.goSubtree) { + this.children[childIndex].walk(rangeStart, rangeLength, walkFns); + if (walkFns.post) { + walkFns.post(rangeStart, rangeLength, this.children[childIndex], this, nodeType); + } + } + else { + walkFns.goSubtree = true; + } + return walkFns.done; + } + + skipChild(relativeStart: number, relativeLength: number, childIndex: number, walkFns: ILineIndexWalker, nodeType: CharRangeSection) { + if (walkFns.pre && (!walkFns.done)) { + walkFns.pre(relativeStart, relativeLength, this.children[childIndex], this, nodeType); + walkFns.goSubtree = true; + } + } + + walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker) { + // assume (rangeStart < this.totalChars) && (rangeLength <= this.totalChars) + let childIndex = 0; + let child = this.children[0]; + let childCharCount = child.charCount(); + // find sub-tree containing start + let adjustedStart = rangeStart; + while (adjustedStart >= childCharCount) { + this.skipChild(adjustedStart, rangeLength, childIndex, walkFns, CharRangeSection.PreStart); + adjustedStart -= childCharCount; + childIndex++; + child = this.children[childIndex]; + childCharCount = child.charCount(); + } + // Case I: both start and end of range in same subtree + if ((adjustedStart + rangeLength) <= childCharCount) { + if (this.execWalk(adjustedStart, rangeLength, walkFns, childIndex, CharRangeSection.Entire)) { + return; + } + } + else { + // Case II: start and end of range in different subtrees (possibly with subtrees in the middle) + if (this.execWalk(adjustedStart, childCharCount - adjustedStart, walkFns, childIndex, CharRangeSection.Start)) { + return; + } + let adjustedLength = rangeLength - (childCharCount - adjustedStart); + childIndex++; + child = this.children[childIndex]; + childCharCount = child.charCount(); + while (adjustedLength > childCharCount) { + if (this.execWalk(0, childCharCount, walkFns, childIndex, CharRangeSection.Mid)) { + return; + } + adjustedLength -= childCharCount; + childIndex++; + child = this.children[childIndex]; + childCharCount = child.charCount(); + } + if (adjustedLength > 0) { + if (this.execWalk(0, adjustedLength, walkFns, childIndex, CharRangeSection.End)) { + return; + } + } + } + // Process any subtrees after the one containing range end + if (walkFns.pre) { + const clen = this.children.length; + if (childIndex < (clen - 1)) { + for (let ej = childIndex + 1; ej < clen; ej++) { + this.skipChild(0, 0, ej, walkFns, CharRangeSection.PostEnd); + } + } + } + } + + charOffsetToLineNumberAndPos(lineNumber: number, charOffset: number): ILineInfo { + const childInfo = this.childFromCharOffset(lineNumber, charOffset); + if (!childInfo.child) { + return { + line: lineNumber, + offset: charOffset, + }; + } + else if (childInfo.childIndex < this.children.length) { + if (childInfo.child.isLeaf()) { + return { + line: childInfo.lineNumber, + offset: childInfo.charOffset, + text: ((childInfo.child)).text, + leaf: ((childInfo.child)) + }; + } + else { + const lineNode = (childInfo.child); + return lineNode.charOffsetToLineNumberAndPos(childInfo.lineNumber, childInfo.charOffset); + } + } + else { + const lineInfo = this.lineNumberToInfo(this.lineCount(), 0); + return { line: this.lineCount(), offset: lineInfo.leaf.charCount() }; + } + } + + lineNumberToInfo(lineNumber: number, charOffset: number): ILineInfo { + const childInfo = this.childFromLineNumber(lineNumber, charOffset); + if (!childInfo.child) { + return { + line: lineNumber, + offset: charOffset + }; + } + else if (childInfo.child.isLeaf()) { + return { + line: lineNumber, + offset: childInfo.charOffset, + text: ((childInfo.child)).text, + leaf: ((childInfo.child)) + }; + } + else { + const lineNode = (childInfo.child); + return lineNode.lineNumberToInfo(childInfo.relativeLineNumber, childInfo.charOffset); + } + } + + childFromLineNumber(lineNumber: number, charOffset: number) { + let child: LineCollection; + let relativeLineNumber = lineNumber; + let i: number; + let len: number; + for (i = 0, len = this.children.length; i < len; i++) { + child = this.children[i]; + const childLineCount = child.lineCount(); + if (childLineCount >= relativeLineNumber) { + break; + } + else { + relativeLineNumber -= childLineCount; + charOffset += child.charCount(); + } + } + return { + child: child, + childIndex: i, + relativeLineNumber: relativeLineNumber, + charOffset: charOffset + }; + } + + childFromCharOffset(lineNumber: number, charOffset: number) { + let child: LineCollection; + let i: number; + let len: number; + for (i = 0, len = this.children.length; i < len; i++) { + child = this.children[i]; + if (child.charCount() > charOffset) { + break; + } + else { + charOffset -= child.charCount(); + lineNumber += child.lineCount(); + } + } + return { + child: child, + childIndex: i, + charOffset: charOffset, + lineNumber: lineNumber + }; + } + + splitAfter(childIndex: number) { + let splitNode: LineNode; + const clen = this.children.length; + childIndex++; + const endLength = childIndex; + if (childIndex < clen) { + splitNode = new LineNode(); + while (childIndex < clen) { + splitNode.add(this.children[childIndex]); + childIndex++; + } + splitNode.updateCounts(); + } + this.children.length = endLength; + return splitNode; + } + + remove(child: LineCollection) { + const childIndex = this.findChildIndex(child); + const clen = this.children.length; + if (childIndex < (clen - 1)) { + for (let i = childIndex; i < (clen - 1); i++) { + this.children[i] = this.children[i + 1]; + } + } + this.children.length--; + } + + findChildIndex(child: LineCollection) { + let childIndex = 0; + const clen = this.children.length; + while ((this.children[childIndex] !== child) && (childIndex < clen)) childIndex++; + return childIndex; + } + + insertAt(child: LineCollection, nodes: LineCollection[]) { + let childIndex = this.findChildIndex(child); + const clen = this.children.length; + const nodeCount = nodes.length; + // if child is last and there is more room and only one node to place, place it + if ((clen < lineCollectionCapacity) && (childIndex === (clen - 1)) && (nodeCount === 1)) { + this.add(nodes[0]); + this.updateCounts(); + return []; + } + else { + const shiftNode = this.splitAfter(childIndex); + let nodeIndex = 0; + childIndex++; + while ((childIndex < lineCollectionCapacity) && (nodeIndex < nodeCount)) { + this.children[childIndex] = nodes[nodeIndex]; + childIndex++; + nodeIndex++; + } + let splitNodes: LineNode[] = []; + let splitNodeCount = 0; + if (nodeIndex < nodeCount) { + splitNodeCount = Math.ceil((nodeCount - nodeIndex) / lineCollectionCapacity); + splitNodes = new Array(splitNodeCount); + let splitNodeIndex = 0; + for (let i = 0; i < splitNodeCount; i++) { + splitNodes[i] = new LineNode(); + } + let splitNode = splitNodes[0]; + while (nodeIndex < nodeCount) { + splitNode.add(nodes[nodeIndex]); + nodeIndex++; + if (splitNode.children.length === lineCollectionCapacity) { + splitNodeIndex++; + splitNode = splitNodes[splitNodeIndex]; + } + } + for (let i = splitNodes.length - 1; i >= 0; i--) { + if (splitNodes[i].children.length === 0) { + splitNodes.length--; + } + } + } + if (shiftNode) { + splitNodes[splitNodes.length] = shiftNode; + } + this.updateCounts(); + for (let i = 0; i < splitNodeCount; i++) { + (splitNodes[i]).updateCounts(); + } + return splitNodes; + } + } + + // assume there is room for the item; return true if more room + add(collection: LineCollection) { + this.children[this.children.length] = collection; + return (this.children.length < lineCollectionCapacity); + } + + charCount() { + return this.totalChars; + } + + lineCount() { + return this.totalLines; + } + } + + export class LineLeaf implements LineCollection { + constructor(public text: string) { + } + + isLeaf() { + return true; + } + + walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker) { + walkFns.leaf(rangeStart, rangeLength, this); + } + + charCount() { + return this.text.length; + } + + lineCount() { + return 1; + } + } +} \ No newline at end of file From b66991dc63cb15c0bca4675fe2971301e4f40a84 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 31 May 2016 17:03:12 -0700 Subject: [PATCH 005/307] annotate class fields --- src/server/editorServices.ts | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 01f98a2b954..97cefce53c0 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -293,9 +293,13 @@ namespace ts.server { } export class Project { - private lsHost: LSHost; - languageService: LanguageService; - projectFilename: string; + private rootFiles: ScriptInfo[] = []; + private pathToScriptInfo: ts.FileMap; + private readonly lsHost: LSHost; + + readonly languageService: LanguageService; + readonly getCanonicalFileName: (fileName: string) => string; + projectFileWatcher: FileWatcher; directoryWatcher: FileWatcher; // Used to keep track of what directories are watched for this project @@ -306,12 +310,7 @@ namespace ts.server { /** Used for configured projects which may have multiple open roots */ openRefCount = 0; - getCanonicalFileName: (fileName: string) => string; - - private rootFiles: ScriptInfo[] = []; - private pathToScriptInfo: ts.FileMap; - - constructor(public projectService: ProjectService, documentRegistry: ts.DocumentRegistry, public projectOptions?: ProjectOptions) { + constructor(readonly projectFilename: string, public projectService: ProjectService, documentRegistry: ts.DocumentRegistry, public projectOptions?: ProjectOptions) { this.pathToScriptInfo = ts.createFileMap(); this.getCanonicalFileName = ts.createGetCanonicalFileName(this.projectService.host.useCaseSensitiveFileNames); if (projectOptions && projectOptions.files) { @@ -340,10 +339,6 @@ namespace ts.server { return this.openRefCount; } - openReferencedFile(filename: string) { - return this.projectService.openFile(filename, /*openedByClient*/ false); - } - getRootFiles() { return this.rootFiles.map(info => info.fileName); } @@ -422,7 +417,7 @@ namespace ts.server { const path = toPath(fileName, this.projectService.host.getCurrentDirectory(), this.getCanonicalFileName); let scriptInfo = this.pathToScriptInfo.get(path); if (!scriptInfo) { - scriptInfo = this.openReferencedFile(fileName); + scriptInfo = this.projectService.openFile(fileName, /*openedByClient*/ false); if (scriptInfo) { this.pathToScriptInfo.set(path, scriptInfo); } @@ -678,7 +673,7 @@ namespace ts.server { } createInferredProject(root: ScriptInfo) { - const project = new Project(this, this.documentRegistry); + const project = new Project(/*projectFilename*/ undefined, this, this.documentRegistry); project.addRoot(root); let currentPath = ts.getDirectoryPath(root.fileName); @@ -1335,9 +1330,7 @@ namespace ts.server { } createProject(projectFilename: string, projectOptions?: ProjectOptions) { - const project = new Project(this, this.documentRegistry, projectOptions); - project.projectFilename = projectFilename; - return project; + return new Project(projectFilename, this, this.documentRegistry, projectOptions); } } } From e22e7cc09a6077277b28ac5f6e5a6d89f56dec68 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 31 May 2016 21:10:06 -0700 Subject: [PATCH 006/307] drop Timestamped, create separate classes for different project types --- src/server/editorServices.ts | 113 ++++++++++++++++++----------------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 97cefce53c0..cb74605f468 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -123,25 +123,15 @@ namespace ts.server { } } - interface Timestamped { - lastCheckTime?: number; - } - - interface TimestampedResolvedModule extends ResolvedModuleWithFailedLookupLocations, Timestamped { - } - - interface TimestampedResolvedTypeReferenceDirective extends ResolvedTypeReferenceDirectiveWithFailedLookupLocations, Timestamped { - } - export class LSHost implements ts.LanguageServiceHost { private compilationSettings: ts.CompilerOptions; - private resolvedModuleNames: ts.FileMap>; - private resolvedTypeReferenceDirectives: ts.FileMap>; + private resolvedModuleNames: ts.FileMap>; + private resolvedTypeReferenceDirectives: ts.FileMap>; private moduleResolutionHost: ts.ModuleResolutionHost; constructor(private host: ServerHost, private project: Project) { - this.resolvedModuleNames = createFileMap>(); - this.resolvedTypeReferenceDirectives = createFileMap>(); + this.resolvedModuleNames = createFileMap>(); + this.resolvedTypeReferenceDirectives = createFileMap>(); this.moduleResolutionHost = { fileExists: fileName => this.fileExists(fileName), readFile: fileName => this.host.readFile(fileName), @@ -149,7 +139,7 @@ namespace ts.server { }; } - private resolveNamesWithLocalCache( + private resolveNamesWithLocalCache( names: string[], containingFile: string, cache: ts.FileMap>, @@ -173,9 +163,7 @@ namespace ts.server { resolution = existingResolution; } else { - resolution = loader(name, containingFile, compilerOptions, this.moduleResolutionHost); - resolution.lastCheckTime = Date.now(); - newResolutions[name] = resolution; + newResolutions[name] = resolution = loader(name, containingFile, compilerOptions, this.moduleResolutionHost); } } @@ -195,7 +183,6 @@ namespace ts.server { if (getResult(resolution)) { // TODO: consider checking failedLookupLocations - // TODO: use lastCheckTime to track expiration for module name resolution return true; } @@ -292,31 +279,31 @@ namespace ts.server { compilerOptions?: ts.CompilerOptions; } - export class Project { + export abstract class Project { private rootFiles: ScriptInfo[] = []; private pathToScriptInfo: ts.FileMap; private readonly lsHost: LSHost; + private filenameToSourceFile: ts.Map = {}; readonly languageService: LanguageService; readonly getCanonicalFileName: (fileName: string) => string; - projectFileWatcher: FileWatcher; - directoryWatcher: FileWatcher; - // Used to keep track of what directories are watched for this project - directoriesWatchedForTsconfig: string[] = []; - program: ts.Program; - filenameToSourceFile: ts.Map = {}; - updateGraphSeq = 0; - /** Used for configured projects which may have multiple open roots */ - openRefCount = 0; + private program: ts.Program; + + constructor( + readonly projectFilename: string, + public readonly projectService: ProjectService, + documentRegistry: ts.DocumentRegistry, + public projectOptions?: ProjectOptions) { - constructor(readonly projectFilename: string, public projectService: ProjectService, documentRegistry: ts.DocumentRegistry, public projectOptions?: ProjectOptions) { this.pathToScriptInfo = ts.createFileMap(); this.getCanonicalFileName = ts.createGetCanonicalFileName(this.projectService.host.useCaseSensitiveFileNames); + if (projectOptions && projectOptions.files) { // If files are listed explicitly, allow all extensions projectOptions.compilerOptions.allowNonTsExtensions = true; } + this.lsHost = new LSHost(this.projectService.host, this); if (projectOptions && projectOptions.compilerOptions) { this.lsHost.setCompilationSettings(projectOptions.compilerOptions); @@ -330,15 +317,6 @@ namespace ts.server { this.languageService = ts.createLanguageService(this.lsHost, documentRegistry); } - addOpenRef() { - this.openRefCount++; - } - - deleteOpenRef() { - this.openRefCount--; - return this.openRefCount; - } - getRootFiles() { return this.rootFiles.map(info => info.fileName); } @@ -466,6 +444,35 @@ namespace ts.server { } } + class InferredProject extends Project { + // Used to keep track of what directories are watched for this project + directoriesWatchedForTsconfig: string[] = []; + constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry) { + super(/*projectFilename*/ undefined, projectService, documentRegistry); + } + } + + class ConfiguredProject extends Project { + projectFileWatcher: FileWatcher; + directoryWatcher: FileWatcher; + /** Used for configured projects which may have multiple open roots */ + openRefCount = 0; + + constructor(projectFilename: string, projectService: ProjectService, documentRegistry: ts.DocumentRegistry, projectOptions: ProjectOptions) { + super(projectFilename, projectService, documentRegistry, projectOptions); + } + + addOpenRef() { + this.openRefCount++; + } + + deleteOpenRef() { + this.openRefCount--; + return this.openRefCount; + } + } + + export interface ProjectOpenResult { success?: boolean; errorMsg?: string; @@ -504,9 +511,9 @@ namespace ts.server { // open, non-configured root files openFileRoots: ScriptInfo[] = []; // projects built from openFileRoots - inferredProjects: Project[] = []; + inferredProjects: InferredProject[] = []; // projects specified by a tsconfig.json file - configuredProjects: Project[] = []; + configuredProjects: ConfiguredProject[] = []; // open files referenced by a project openFilesReferenced: ScriptInfo[] = []; // open files that are roots of a configured project @@ -673,7 +680,7 @@ namespace ts.server { } createInferredProject(root: ScriptInfo) { - const project = new Project(/*projectFilename*/ undefined, this, this.documentRegistry); + const project = new InferredProject(this, this.documentRegistry); project.addRoot(root); let currentPath = ts.getDirectoryPath(root.fileName); @@ -733,7 +740,7 @@ namespace ts.server { } updateConfiguredProjectList() { - const configuredProjects: Project[] = []; + const configuredProjects: ConfiguredProject[] = []; for (let i = 0, len = this.configuredProjects.length; i < len; i++) { if (this.configuredProjects[i].openRefCount > 0) { configuredProjects.push(this.configuredProjects[i]); @@ -745,12 +752,12 @@ namespace ts.server { removeProject(project: Project) { this.log("remove project: " + project.getRootFiles().toString()); if (project.isConfiguredProject()) { - project.projectFileWatcher.close(); - project.directoryWatcher.close(); - this.configuredProjects = copyListRemovingItem(project, this.configuredProjects); + (project).projectFileWatcher.close(); + (project).directoryWatcher.close(); + this.configuredProjects = copyListRemovingItem((project), this.configuredProjects); } else { - for (const directory of project.directoriesWatchedForTsconfig) { + for (const directory of (project).directoriesWatchedForTsconfig) { // if the ref count for this directory watcher drops to 0, it's time to close it project.projectService.directoryWatchersRefCount[directory]--; if (!project.projectService.directoryWatchersRefCount[directory]) { @@ -759,7 +766,7 @@ namespace ts.server { delete project.projectService.directoryWatchersForTsconfig[directory]; } } - this.inferredProjects = copyListRemovingItem(project, this.inferredProjects); + this.inferredProjects = copyListRemovingItem((project), this.inferredProjects); } const fileNames = project.getFileNames(); @@ -848,7 +855,7 @@ namespace ts.server { for (let i = 0, len = this.openFileRootsConfigured.length; i < len; i++) { if (info === this.openFileRootsConfigured[i]) { - if (info.defaultProject.deleteOpenRef() === 0) { + if ((info.defaultProject).deleteOpenRef() === 0) { removedProject = info.defaultProject; } } @@ -1246,13 +1253,13 @@ namespace ts.server { } - openConfigFile(configFilename: string, clientFileName?: string): { success: boolean, project?: Project, errors?: Diagnostic[] } { + openConfigFile(configFilename: string, clientFileName?: string): { success: boolean, project?: ConfiguredProject, errors?: Diagnostic[] } { const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(configFilename); if (!succeeded) { return { success: false, errors }; } else { - const project = this.createProject(configFilename, projectOptions); + const project = new ConfiguredProject(configFilename, this, this.documentRegistry, projectOptions); let errors: Diagnostic[]; for (const rootFilename of projectOptions.files) { if (this.host.fileExists(rootFilename)) { @@ -1328,9 +1335,5 @@ namespace ts.server { } } } - - createProject(projectFilename: string, projectOptions?: ProjectOptions) { - return new Project(projectFilename, this, this.documentRegistry, projectOptions); - } } } From fbdee841a614ac19e165d97d6b1d5eb2f12ad7cb Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 31 May 2016 23:47:07 -0700 Subject: [PATCH 007/307] delete redundant fields --- src/server/editorServices.ts | 78 +++++++++++++----------------------- src/server/session.ts | 2 +- 2 files changed, 28 insertions(+), 52 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index cb74605f468..fd893e9a544 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -123,20 +123,16 @@ namespace ts.server { } } - export class LSHost implements ts.LanguageServiceHost { + export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost { private compilationSettings: ts.CompilerOptions; private resolvedModuleNames: ts.FileMap>; private resolvedTypeReferenceDirectives: ts.FileMap>; - private moduleResolutionHost: ts.ModuleResolutionHost; + private getCanonicalFileName: (fileName: string) => string; constructor(private host: ServerHost, private project: Project) { + this.getCanonicalFileName = ts.createGetCanonicalFileName(this.host.useCaseSensitiveFileNames); this.resolvedModuleNames = createFileMap>(); this.resolvedTypeReferenceDirectives = createFileMap>(); - this.moduleResolutionHost = { - fileExists: fileName => this.fileExists(fileName), - readFile: fileName => this.host.readFile(fileName), - directoryExists: directoryName => this.host.directoryExists(directoryName) - }; } private resolveNamesWithLocalCache( @@ -146,7 +142,7 @@ namespace ts.server { loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T, getResult: (s: T) => R): R[] { - const path = toPath(containingFile, this.host.getCurrentDirectory(), this.project.getCanonicalFileName); + const path = toPath(containingFile, this.host.getCurrentDirectory(), this.getCanonicalFileName); const currentResolutionsInFile = cache.get(path); const newResolutions: Map = {}; @@ -163,7 +159,7 @@ namespace ts.server { resolution = existingResolution; } else { - newResolutions[name] = resolution = loader(name, containingFile, compilerOptions, this.moduleResolutionHost); + newResolutions[name] = resolution = loader(name, containingFile, compilerOptions, this); } } @@ -271,6 +267,10 @@ namespace ts.server { directoryExists(path: string): boolean { return this.host.directoryExists(path); } + + readFile(fileName: string): string { + return this.host.readFile(fileName); + } } export interface ProjectOptions { @@ -281,12 +281,9 @@ namespace ts.server { export abstract class Project { private rootFiles: ScriptInfo[] = []; - private pathToScriptInfo: ts.FileMap; private readonly lsHost: LSHost; - private filenameToSourceFile: ts.Map = {}; readonly languageService: LanguageService; - readonly getCanonicalFileName: (fileName: string) => string; private program: ts.Program; @@ -296,9 +293,6 @@ namespace ts.server { documentRegistry: ts.DocumentRegistry, public projectOptions?: ProjectOptions) { - this.pathToScriptInfo = ts.createFileMap(); - this.getCanonicalFileName = ts.createGetCanonicalFileName(this.projectService.host.useCaseSensitiveFileNames); - if (projectOptions && projectOptions.files) { // If files are listed explicitly, allow all extensions projectOptions.compilerOptions.allowNonTsExtensions = true; @@ -326,15 +320,15 @@ namespace ts.server { return sourceFiles.map(sourceFile => sourceFile.fileName); } - getSourceFile(info: ScriptInfo) { - return this.filenameToSourceFile[info.fileName]; + containsScriptInfo(info: ScriptInfo): boolean { + return this.program.getSourceFileByPath(info.path) !== undefined; } - getSourceFileFromName(filename: string, requireOpen?: boolean) { + containsFile(filename: string, requireOpen?: boolean) { const info = this.projectService.getScriptInfo(filename); if (info) { if ((!requireOpen) || info.isOpen) { - return this.getSourceFile(info); + return this.containsScriptInfo(info); } } } @@ -345,21 +339,11 @@ namespace ts.server { removeReferencedFile(info: ScriptInfo) { if (!info.isOpen) { - this.pathToScriptInfo.remove(info.path); this.lsHost.removeReferencedFile(info); } this.updateGraph(); } - updateFileMap() { - this.filenameToSourceFile = {}; - const sourceFiles = this.program.getSourceFiles(); - for (let i = 0, len = sourceFiles.length; i < len; i++) { - const normFilename = ts.normalizePath(sourceFiles[i].fileName); - this.filenameToSourceFile[normFilename] = sourceFiles[i]; - } - } - finishGraph() { this.updateGraph(); this.languageService.getNavigateToItems(".*"); @@ -367,7 +351,6 @@ namespace ts.server { updateGraph() { this.program = this.languageService.getProgram(); - this.updateFileMap(); } isConfiguredProject() { @@ -376,37 +359,31 @@ namespace ts.server { // add a root file to project addRoot(info: ScriptInfo) { - if (!this.pathToScriptInfo.contains(info.path)) { - this.pathToScriptInfo.set(info.path, info); + if (!this.isRoot(info)) { this.rootFiles.push(info); } } // remove a root file from project removeRoot(info: ScriptInfo) { - if (!this.pathToScriptInfo.contains(info.path)) { - this.pathToScriptInfo.remove(info.path); + if (this.isRoot(info)) { this.rootFiles = copyListRemovingItem(info, this.rootFiles); this.lsHost.removeRoot(info); } } getScriptInfo(fileName: string) { - const path = toPath(fileName, this.projectService.host.getCurrentDirectory(), this.getCanonicalFileName); - let scriptInfo = this.pathToScriptInfo.get(path); - if (!scriptInfo) { - scriptInfo = this.projectService.openFile(fileName, /*openedByClient*/ false); - if (scriptInfo) { - this.pathToScriptInfo.set(path, scriptInfo); - } - } - return scriptInfo; + return this.projectService.openFile(fileName, /*openedByClient*/ false); } filesToString() { + if (!this.program) { + return ""; + } let strBuilder = ""; - ts.forEachValue(this.filenameToSourceFile, - sourceFile => { strBuilder += sourceFile.fileName + "\n"; }); + for (const file of this.program.getSourceFiles()) { + strBuilder += `${file.fileName}\n`; + } return strBuilder; } @@ -807,7 +784,7 @@ namespace ts.server { for (let i = 0, len = this.openFileRoots.length; i < len; i++) { const r = this.openFileRoots[i]; // if r referenced by the new project - if (info.defaultProject.getSourceFile(r)) { + if (info.defaultProject.containsScriptInfo(r)) { // remove project rooted at r this.removeProject(r.defaultProject); // put r in referenced open file list @@ -902,7 +879,7 @@ namespace ts.server { const inferredProject = this.inferredProjects[i]; inferredProject.updateGraph(); if (inferredProject !== excludedProject) { - if (inferredProject.getSourceFile(info)) { + if (inferredProject.containsScriptInfo(info)) { info.defaultProject = inferredProject; referencingProjects.push(inferredProject); } @@ -911,7 +888,7 @@ namespace ts.server { for (let i = 0, len = this.configuredProjects.length; i < len; i++) { const configuredProject = this.configuredProjects[i]; configuredProject.updateGraph(); - if (configuredProject.getSourceFile(info)) { + if (configuredProject.containsScriptInfo(info)) { info.defaultProject = configuredProject; referencingProjects.push(configuredProject); } @@ -944,7 +921,7 @@ namespace ts.server { const openFileRootsConfigured: ScriptInfo[] = []; for (const info of this.openFileRootsConfigured) { const project = info.defaultProject; - if (!project || !(project.getSourceFile(info))) { + if (!project || !(project.containsScriptInfo(info))) { info.defaultProject = undefined; unattachedOpenFiles.push(info); } @@ -962,8 +939,7 @@ namespace ts.server { for (let i = 0, len = this.openFilesReferenced.length; i < len; i++) { const referencedFile = this.openFilesReferenced[i]; referencedFile.defaultProject.updateGraph(); - const sourceFile = referencedFile.defaultProject.getSourceFile(referencedFile); - if (sourceFile) { + if (referencedFile.defaultProject.containsScriptInfo(referencedFile)) { openFilesReferenced.push(referencedFile); } else { diff --git a/src/server/session.ts b/src/server/session.ts index 249a484191a..e8675613707 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -291,7 +291,7 @@ namespace ts.server { if (matchSeq(seq)) { const checkSpec = checkList[index]; index++; - if (checkSpec.project.getSourceFileFromName(checkSpec.fileName, requireOpen)) { + if (checkSpec.project.containsFile(checkSpec.fileName, requireOpen)) { this.syntacticCheck(checkSpec.fileName, checkSpec.project); this.immediateId = setImmediate(() => { this.semanticCheck(checkSpec.fileName, checkSpec.project); From 0b7227dce647f13df9419e9ae45405b93605159d Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 2 Jun 2016 16:14:26 -0700 Subject: [PATCH 008/307] remove project options from project --- src/server/editorServices.ts | 240 +++++++++++------- src/server/protocol.d.ts | 21 ++ src/server/session.ts | 5 + .../cases/unittests/cachingInServerLSHost.ts | 9 +- 4 files changed, 182 insertions(+), 93 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index fd893e9a544..b84654f38e1 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -275,6 +275,7 @@ namespace ts.server { export interface ProjectOptions { // these fields can be present in the project file + configHasFilesProperty?: boolean; files?: string[]; compilerOptions?: ts.CompilerOptions; } @@ -284,33 +285,34 @@ namespace ts.server { private readonly lsHost: LSHost; readonly languageService: LanguageService; - private program: ts.Program; constructor( readonly projectFilename: string, public readonly projectService: ProjectService, documentRegistry: ts.DocumentRegistry, - public projectOptions?: ProjectOptions) { + files: string[], + compilerOptions: CompilerOptions) { - if (projectOptions && projectOptions.files) { + if (!compilerOptions) { + compilerOptions = ts.getDefaultCompilerOptions(); + compilerOptions.allowNonTsExtensions = true; + compilerOptions.allowJs = true; + } + else if (files) { // If files are listed explicitly, allow all extensions - projectOptions.compilerOptions.allowNonTsExtensions = true; + compilerOptions.allowNonTsExtensions = true; } this.lsHost = new LSHost(this.projectService.host, this); - if (projectOptions && projectOptions.compilerOptions) { - this.lsHost.setCompilationSettings(projectOptions.compilerOptions); - } - else { - const defaultOpts = ts.getDefaultCompilerOptions(); - defaultOpts.allowNonTsExtensions = true; - defaultOpts.allowJs = true; - this.lsHost.setCompilationSettings(defaultOpts); - } + this.lsHost.setCompilationSettings(compilerOptions); this.languageService = ts.createLanguageService(this.lsHost, documentRegistry); } + getCompilerOptions() { + return this.lsHost.getCompilationSettings(); + } + getRootFiles() { return this.rootFiles.map(info => info.fileName); } @@ -387,11 +389,10 @@ namespace ts.server { return strBuilder; } - setProjectOptions(projectOptions: ProjectOptions) { - this.projectOptions = projectOptions; - if (projectOptions.compilerOptions) { - projectOptions.compilerOptions.allowNonTsExtensions = true; - this.lsHost.setCompilationSettings(projectOptions.compilerOptions); + setCompilerOptions(compilerOptions: CompilerOptions) { + if (compilerOptions) { + compilerOptions.allowNonTsExtensions = true; + this.lsHost.setCompilationSettings(compilerOptions); } } @@ -425,18 +426,40 @@ namespace ts.server { // Used to keep track of what directories are watched for this project directoriesWatchedForTsconfig: string[] = []; constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry) { - super(/*projectFilename*/ undefined, projectService, documentRegistry); + super(/*projectFilename*/ undefined, projectService, documentRegistry, /*files*/ undefined, /*compilerOptions*/ undefined); } } class ConfiguredProject extends Project { - projectFileWatcher: FileWatcher; - directoryWatcher: FileWatcher; + private projectFileWatcher: FileWatcher; + private directoryWatcher: FileWatcher; /** Used for configured projects which may have multiple open roots */ openRefCount = 0; - constructor(projectFilename: string, projectService: ProjectService, documentRegistry: ts.DocumentRegistry, projectOptions: ProjectOptions) { - super(projectFilename, projectService, documentRegistry, projectOptions); + constructor(projectFilename: string, projectService: ProjectService, documentRegistry: ts.DocumentRegistry, files: string[], compilerOptions: CompilerOptions) { + super(projectFilename, projectService, documentRegistry, files, compilerOptions); + } + + watchConfigFile(callback: (project: Project) => void) { + this.projectFileWatcher = this.projectService.host.watchFile(this.projectFilename, _ => callback(this)); + } + + watchConfigDirectory(callback: (project: Project, path: string) => void) { + this.projectService.log("Add recursive watcher for: " + ts.getDirectoryPath(this.projectFilename)); + this.directoryWatcher = this.projectService.host.watchDirectory( + ts.getDirectoryPath(this.projectFilename), + path => callback(this, path), + /*recursive*/ true + ); + } + + close() { + if (this.projectFileWatcher) { + this.projectFileWatcher.close(); + } + if (this.directoryWatcher) { + this.directoryWatcher.close(); + } } addOpenRef() { @@ -554,7 +577,7 @@ namespace ts.server { // If a change was made inside "folder/file", node will trigger the callback twice: // one with the fileName being "folder/file", and the other one with "folder". // We don't respond to the second one. - if (fileName && !ts.isSupportedSourceFileName(fileName, project.projectOptions ? project.projectOptions.compilerOptions : undefined)) { + if (fileName && !ts.isSupportedSourceFileName(fileName, project.isConfiguredProject() && project.getCompilerOptions())) { return; } @@ -729,8 +752,7 @@ namespace ts.server { removeProject(project: Project) { this.log("remove project: " + project.getRootFiles().toString()); if (project.isConfiguredProject()) { - (project).projectFileWatcher.close(); - (project).directoryWatcher.close(); + (project).close(); this.configuredProjects = copyListRemovingItem((project), this.configuredProjects); } else { @@ -1082,6 +1104,7 @@ namespace ts.server { openOrUpdateConfiguredProjectForFile(fileName: string): { configFileName?: string, configFileErrors?: Diagnostic[] } { const searchPath = ts.normalizePath(getDirectoryPath(fileName)); this.log("Search path: " + searchPath, "Info"); + // check if this file is already included in one of external projects const configFileName = this.findConfigFile(searchPath); if (configFileName) { this.log("Config file name: " + configFileName, "Info"); @@ -1185,8 +1208,32 @@ namespace ts.server { this.psLogger.endGroup(); } - configProjectIsActive(fileName: string) { - return this.findConfiguredProjectByConfigFile(fileName) === undefined; + loadExternalProject(externalProject: protocol.ExternalProject): Project { + let project = this.findConfiguredProjectByConfigFile(externalProject.projectFileName); + if (project) { + this.updateConfiguredProjectWorker(project, externalProject.rootFiles, externalProject.options); + } + else { + // TODO: get and handle errors + ({ project } = this.createConfiguredProject( + externalProject.projectFileName, + externalProject.rootFiles, + externalProject.options, + /*watchConfigFile*/ false, + /*watchConfigDirectory*/ false)); + this.configuredProjects.push(project); + } + return project; + } + + loadExternalProjects(externalProjects: protocol.ExternalProject[], openFiles: protocol.OpenFile[]): void { + for (const project of externalProjects) { + this.loadExternalProject(project); + } + for (const openFile of openFiles) { + this.openFile(openFile.fileName, /*openedByClient*/ true, openFile.content); + } + // TODO: return diff } findConfiguredProjectByConfigFile(configFileName: string) { @@ -1208,6 +1255,7 @@ namespace ts.server { return { succeeded: false, errors: [rawConfig.error] }; } else { + const configHasFilesProperty = rawConfig.config["files"] !== undefined; const parsedCommandLine = ts.parseJsonConfigFileContent(rawConfig.config, this.host, dirPath, /*existingOptions*/ {}, configFilename); Debug.assert(!!parsedCommandLine.fileNames); @@ -1221,7 +1269,8 @@ namespace ts.server { else { const projectOptions: ProjectOptions = { files: parsedCommandLine.fileNames, - compilerOptions: parsedCommandLine.options + compilerOptions: parsedCommandLine.options, + configHasFilesProperty }; return { succeeded: true, projectOptions }; } @@ -1229,33 +1278,86 @@ namespace ts.server { } - openConfigFile(configFilename: string, clientFileName?: string): { success: boolean, project?: ConfiguredProject, errors?: Diagnostic[] } { - const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(configFilename); + private createConfiguredProject(configFileName: string, files: string[], compilerOptions: CompilerOptions, watchConfigFile: boolean, watchConfigDirectory: boolean, clientFileName?: string) { + let errors: Diagnostic[]; + const project = new ConfiguredProject(configFileName, this, this.documentRegistry, files, compilerOptions); + for (const rootFilename of files) { + if (this.host.fileExists(rootFilename)) { + const info = this.openFile(rootFilename, /*openedByClient*/ clientFileName == rootFilename); + project.addRoot(info); + } + else { + (errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.File_0_not_found, rootFilename)); + } + } + project.finishGraph(); + if (watchConfigFile) { + project.watchConfigFile(project => this.watchedProjectConfigFileChanged(project)); + } + if (watchConfigDirectory) { + project.watchConfigDirectory((project, path) => this.directoryWatchedForSourceFilesChanged(project, path)); + } + return { project, errors }; + } + + openConfigFile(configFileName: string, clientFileName?: string): { success: boolean, project?: ConfiguredProject, errors?: Diagnostic[] } { + const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(configFileName); if (!succeeded) { return { success: false, errors }; } else { - const project = new ConfiguredProject(configFilename, this, this.documentRegistry, projectOptions); - let errors: Diagnostic[]; - for (const rootFilename of projectOptions.files) { - if (this.host.fileExists(rootFilename)) { - const info = this.openFile(rootFilename, /*openedByClient*/ clientFileName == rootFilename); - project.addRoot(info); - } - else { - (errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.File_0_not_found, rootFilename)); + const { project, errors } = + this.createConfiguredProject(configFileName, + projectOptions.files, + projectOptions.compilerOptions, + /*watchConfigFile*/ true, + /*watchConfigDirectory*/ !projectOptions.configHasFilesProperty, + clientFileName); + + return { success: true, project, errors }; + } + } + + updateConfiguredProjectWorker(project: Project, newFiles: string[], newOptions: CompilerOptions) { + const oldFileNames = project.getRootFiles(); + const newFileNames = ts.filter(newFiles, f => this.host.fileExists(f)); + const fileNamesToRemove = oldFileNames.filter(f => newFileNames.indexOf(f) < 0); + const fileNamesToAdd = newFileNames.filter(f => oldFileNames.indexOf(f) < 0); + + for (const fileName of fileNamesToRemove) { + const info = this.getScriptInfo(fileName); + if (info) { + project.removeRoot(info); + } + } + + for (const fileName of fileNamesToAdd) { + let info = this.getScriptInfo(fileName); + if (!info) { + info = this.openFile(fileName, /*openedByClient*/ false); + } + else { + // if the root file was opened by client, it would belong to either + // openFileRoots or openFileReferenced. + if (info.isOpen) { + if (this.openFileRoots.indexOf(info) >= 0) { + this.openFileRoots = copyListRemovingItem(info, this.openFileRoots); + if (info.defaultProject && !info.defaultProject.isConfiguredProject()) { + this.removeProject(info.defaultProject); + } + } + if (this.openFilesReferenced.indexOf(info) >= 0) { + this.openFilesReferenced = copyListRemovingItem(info, this.openFilesReferenced); + } + this.openFileRootsConfigured.push(info); + info.defaultProject = project; } } - project.finishGraph(); - project.projectFileWatcher = this.host.watchFile(configFilename, _ => this.watchedProjectConfigFileChanged(project)); - this.log("Add recursive watcher for: " + ts.getDirectoryPath(configFilename)); - project.directoryWatcher = this.host.watchDirectory( - ts.getDirectoryPath(configFilename), - path => this.directoryWatchedForSourceFilesChanged(project, path), - /*recursive*/ true - ); - return { success: true, project: project, errors }; + project.addRoot(info); } + + project.setCompilerOptions(newOptions); + project.finishGraph(); } updateConfiguredProject(project: Project) { @@ -1269,45 +1371,7 @@ namespace ts.server { return errors; } else { - const oldFileNames = project.getRootFiles(); - const newFileNames = ts.filter(projectOptions.files, f => this.host.fileExists(f)); - const fileNamesToRemove = oldFileNames.filter(f => newFileNames.indexOf(f) < 0); - const fileNamesToAdd = newFileNames.filter(f => oldFileNames.indexOf(f) < 0); - - for (const fileName of fileNamesToRemove) { - const info = this.getScriptInfo(fileName); - if (info) { - project.removeRoot(info); - } - } - - for (const fileName of fileNamesToAdd) { - let info = this.getScriptInfo(fileName); - if (!info) { - info = this.openFile(fileName, /*openedByClient*/ false); - } - else { - // if the root file was opened by client, it would belong to either - // openFileRoots or openFileReferenced. - if (info.isOpen) { - if (this.openFileRoots.indexOf(info) >= 0) { - this.openFileRoots = copyListRemovingItem(info, this.openFileRoots); - if (info.defaultProject && !info.defaultProject.isConfiguredProject()) { - this.removeProject(info.defaultProject); - } - } - if (this.openFilesReferenced.indexOf(info) >= 0) { - this.openFilesReferenced = copyListRemovingItem(info, this.openFilesReferenced); - } - this.openFileRootsConfigured.push(info); - info.defaultProject = project; - } - } - project.addRoot(info); - } - - project.setProjectOptions(projectOptions); - project.finishGraph(); + this.updateConfiguredProjectWorker(project, projectOptions.files, projectOptions.compilerOptions); } } } diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index f6a418fb578..ad2b7cd07ee 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -418,6 +418,17 @@ declare namespace ts.server.protocol { body?: RenameResponseBody; } + export interface ExternalProject { + projectFileName: string; + rootFiles: string[]; + options: CompilerOptions; + } + + export interface OpenFile { + fileName: string; + content?: string; + } + /** * Editor options */ @@ -537,6 +548,16 @@ declare namespace ts.server.protocol { arguments: OpenRequestArgs; } + type LoadExternalProjectArgs = ExternalProject; + + export interface LoadExternalProject extends Request { + arguments: LoadExternalProjectArgs; + } + + interface LoadExternalProjectResponse extends Response { + files: string[]; + } + /** * Exit request; value of command field is "exit". Ask the server process * to exit. diff --git a/src/server/session.ts b/src/server/session.ts index e8675613707..a3a2917664a 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -127,6 +127,7 @@ namespace ts.server { export const ProjectInfo = "projectInfo"; export const ReloadProjects = "reloadProjects"; export const Unknown = "unknown"; + export const LoadExternalProject = "loadExternalProject"; } namespace Errors { @@ -1027,6 +1028,10 @@ namespace ts.server { } private handlers: Map<(request: protocol.Request) => { response?: any, responseRequired?: boolean }> = { + [CommandNames.LoadExternalProject]: (request: protocol.Request) => { + const project = this.projectService.loadExternalProject(request.arguments); + return { responseRequired: true, response: { files: project.getFileNames() } }; + }, [CommandNames.Exit]: () => { this.exit(); return { responseRequired: false }; diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index f0574bbf793..40c1deb7476 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -82,7 +82,7 @@ namespace ts { const projectService = new server.ProjectService(serverHost, logger); const rootScriptInfo = projectService.openFile(rootFile, /* openedByClient */true); const project = projectService.createInferredProject(rootScriptInfo); - project.setProjectOptions({ files: [rootScriptInfo.fileName], compilerOptions: { module: ts.ModuleKind.AMD } }); + project.setCompilerOptions({ module: ts.ModuleKind.AMD } ); return { project, rootScriptInfo @@ -166,10 +166,9 @@ namespace ts { // setting compiler options discards module resolution cache fileExistsCalled = false; - const opts = ts.clone(project.projectOptions); - opts.compilerOptions = ts.clone(opts.compilerOptions); - opts.compilerOptions.target = ts.ScriptTarget.ES5; - project.setProjectOptions(opts); + const compilerOptions = ts.clone(project.getCompilerOptions()); + compilerOptions.target = ts.ScriptTarget.ES5; + project.setCompilerOptions(compilerOptions); project.languageService.getSemanticDiagnostics(imported.name); assert.isTrue(fileExistsCalled); From 2605fdf276546ffd61fea53730301fb054d5731f Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 2 Jun 2016 18:11:22 -0700 Subject: [PATCH 009/307] move root file to a map, release documents on project close --- src/server/editorServices.ts | 111 ++++++++++++++++++++++------------- src/services/services.ts | 6 +- 2 files changed, 75 insertions(+), 42 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 55079c4896b..39ef3ae55dc 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -274,20 +274,33 @@ namespace ts.server { } export interface ProjectOptions { - // these fields can be present in the project file + /** + * true if config file explicitly listed files + **/ configHasFilesProperty?: boolean; + /** + * these fields can be present in the project file + **/ files?: string[]; compilerOptions?: ts.CompilerOptions; } + export enum ProjectKind { + Inferred, + Configured, + External + } + export abstract class Project { private rootFiles: ScriptInfo[] = []; + private rootFilesMap: FileMap = createFileMap(); private readonly lsHost: LSHost; readonly languageService: LanguageService; private program: ts.Program; constructor( + readonly projectKind: ProjectKind, readonly projectFilename: string, public readonly projectService: ProjectService, documentRegistry: ts.DocumentRegistry, @@ -299,7 +312,7 @@ namespace ts.server { compilerOptions.allowNonTsExtensions = true; compilerOptions.allowJs = true; } - else if (files) { + else if (files && files.length) { // If files are listed explicitly, allow all extensions compilerOptions.allowNonTsExtensions = true; } @@ -309,6 +322,16 @@ namespace ts.server { this.languageService = ts.createLanguageService(this.lsHost, documentRegistry); } + isConfiguredProject() { + // TODO: remove + return this.projectKind !== ProjectKind.Inferred; + } + + close() { + // signal language service to release files acquired from document registry + this.languageService.dispose(); + } + getCompilerOptions() { return this.lsHost.getCompilationSettings(); } @@ -336,7 +359,24 @@ namespace ts.server { } isRoot(info: ScriptInfo) { - return this.rootFiles.some(root => root === info); + return this.rootFilesMap.contains(info.path); + } + + // add a root file to project + addRoot(info: ScriptInfo) { + if (!this.isRoot(info)) { + this.rootFiles.push(info); + this.rootFilesMap.set(info.path, info); + } + } + + // remove a root file from project + removeRoot(info: ScriptInfo) { + if (this.isRoot(info)) { + this.rootFiles = copyListRemovingItem(info, this.rootFiles); + this.rootFilesMap.remove(info.path); + this.lsHost.removeRoot(info); + } } removeReferencedFile(info: ScriptInfo) { @@ -346,34 +386,10 @@ namespace ts.server { this.updateGraph(); } - finishGraph() { - this.updateGraph(); - this.languageService.getNavigateToItems(".*"); - } - updateGraph() { this.program = this.languageService.getProgram(); } - isConfiguredProject() { - return this.projectFilename; - } - - // add a root file to project - addRoot(info: ScriptInfo) { - if (!this.isRoot(info)) { - this.rootFiles.push(info); - } - } - - // remove a root file from project - removeRoot(info: ScriptInfo) { - if (this.isRoot(info)) { - this.rootFiles = copyListRemovingItem(info, this.rootFiles); - this.lsHost.removeRoot(info); - } - } - getScriptInfo(fileName: string) { return this.projectService.openFile(fileName, /*openedByClient*/ false); } @@ -426,7 +442,7 @@ namespace ts.server { // Used to keep track of what directories are watched for this project directoriesWatchedForTsconfig: string[] = []; constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry) { - super(/*projectFilename*/ undefined, projectService, documentRegistry, /*files*/ undefined, /*compilerOptions*/ undefined); + super(ProjectKind.Inferred, /*projectFilename*/ undefined, projectService, documentRegistry, /*files*/ undefined, /*compilerOptions*/ undefined); } } @@ -437,7 +453,7 @@ namespace ts.server { openRefCount = 0; constructor(projectFilename: string, projectService: ProjectService, documentRegistry: ts.DocumentRegistry, files: string[], compilerOptions: CompilerOptions) { - super(projectFilename, projectService, documentRegistry, files, compilerOptions); + super(ProjectKind.Configured, projectFilename, projectService, documentRegistry, files, compilerOptions); } watchConfigFile(callback: (project: Project) => void) { @@ -508,21 +524,36 @@ namespace ts.server { export class ProjectService { filenameToScriptInfo: ts.Map = {}; - // open, non-configured root files + /** + * open, non-configured root files + **/ openFileRoots: ScriptInfo[] = []; - // projects built from openFileRoots + /** + * projects built from openFileRoots + **/ inferredProjects: InferredProject[] = []; - // projects specified by a tsconfig.json file + /** + * projects specified by a tsconfig.json file + **/ configuredProjects: ConfiguredProject[] = []; - // open files referenced by a project + /** + * open files referenced by a project + **/ openFilesReferenced: ScriptInfo[] = []; - // open files that are roots of a configured project + /** + * open files that are roots of a configured project + **/ openFileRootsConfigured: ScriptInfo[] = []; - // a path to directory watcher map that detects added tsconfig files + /** + * a path to directory watcher map that detects added tsconfig files + **/ directoryWatchersForTsconfig: ts.Map = {}; - // count of how many projects are using the directory watcher. If the - // number becomes 0 for a watcher, then we should close it. + /** + * count of how many projects are using the directory watcher. + * If the number becomes 0 for a watcher, then we should close it. + **/ directoryWatchersRefCount: ts.Map = {}; + hostConfiguration: HostConfiguration; timerForDetectingProjectFileListChanges: Map = {}; @@ -700,7 +731,7 @@ namespace ts.server { parentPath = ts.getDirectoryPath(parentPath); } - project.finishGraph(); + project.updateGraph(); this.inferredProjects.push(project); return project; } @@ -1290,7 +1321,7 @@ namespace ts.server { (errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.File_0_not_found, rootFilename)); } } - project.finishGraph(); + project.updateGraph(); if (watchConfigFile) { project.watchConfigFile(project => this.watchedProjectConfigFileChanged(project)); } @@ -1357,7 +1388,7 @@ namespace ts.server { } project.setCompilerOptions(newOptions); - project.finishGraph(); + project.updateGraph(); } updateConfiguredProject(project: Project) { diff --git a/src/services/services.ts b/src/services/services.ts index 90740b257c6..b05ab9d2e32 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -3090,8 +3090,10 @@ namespace ts { function dispose(): void { if (program) { - forEach(program.getSourceFiles(), f => - documentRegistry.releaseDocument(f.fileName, program.getCompilerOptions())); + const key = documentRegistry.getKeyForCompilationSettings(program.getCompilerOptions()); + for (const file of program.getSourceFiles()) { + documentRegistry.releaseDocumentWithKey(file.path, key); + } } } From 23bbbf9819637290f18ef850ddcb526687db23cd Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 3 Jun 2016 00:06:30 -0700 Subject: [PATCH 010/307] added close method --- src/server/editorServices.ts | 44 ++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 39ef3ae55dc..837eeafae53 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -441,9 +441,18 @@ namespace ts.server { class InferredProject extends Project { // Used to keep track of what directories are watched for this project directoriesWatchedForTsconfig: string[] = []; + constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry) { super(ProjectKind.Inferred, /*projectFilename*/ undefined, projectService, documentRegistry, /*files*/ undefined, /*compilerOptions*/ undefined); } + + close() { + super.close(); + + for (const directory of this.directoriesWatchedForTsconfig) { + this.projectService.stopWatchingDirectory(directory); + } + } } class ConfiguredProject extends Project { @@ -470,6 +479,8 @@ namespace ts.server { } close() { + super.close(); + if (this.projectFileWatcher) { this.projectFileWatcher.close(); } @@ -561,11 +572,21 @@ namespace ts.server { constructor(public host: ServerHost, public psLogger: Logger, public eventHandler?: ProjectServiceEventHandler) { // ts.disableIncrementalParsing = true; - this.addDefaultHostConfiguration(); + this.setDefaultHostConfiguration(); this.documentRegistry = ts.createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); } - addDefaultHostConfiguration() { + stopWatchingDirectory(directory: string) { + // if the ref count for this directory watcher drops to 0, it's time to close it + this.directoryWatchersRefCount[directory]--; + if (this.directoryWatchersRefCount[directory] === 0) { + this.log("Close directory watcher for: " + directory); + this.directoryWatchersForTsconfig[directory].close(); + delete this.directoryWatchersForTsconfig[directory]; + } + } + + private setDefaultHostConfiguration() { this.hostConfiguration = { formatCodeOptions: getDefaultFormatCodeOptions(this.host), hostInfo: "Unknown host" @@ -582,7 +603,7 @@ namespace ts.server { return this.hostConfiguration.formatCodeOptions; } - watchedFileChanged(fileName: string) { + private watchedFileChanged(fileName: string) { const info = this.filenameToScriptInfo[fileName]; if (!info) { this.psLogger.info("Error: got watch notification for unknown file: " + fileName); @@ -664,19 +685,19 @@ namespace ts.server { // We should only care about the new tsconfig file if it contains any // opened root files of existing inferred projects for (const openFileRoot of openFileRoots) { - if (rootFilesInTsconfig.indexOf(openFileRoot) >= 0) { + if (contains(rootFilesInTsconfig, openFileRoot)) { this.reloadProjects(); return; } } } - getCanonicalFileName(fileName: string) { + private getCanonicalFileName(fileName: string) { const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); return ts.normalizePath(name); } - watchedProjectConfigFileChanged(project: Project) { + private watchedProjectConfigFileChanged(project: Project) { this.log("Config file changed: " + project.projectFilename); this.updateConfiguredProject(project); this.updateProjectStructure(); @@ -773,8 +794,12 @@ namespace ts.server { updateConfiguredProjectList() { const configuredProjects: ConfiguredProject[] = []; for (let i = 0, len = this.configuredProjects.length; i < len; i++) { - if (this.configuredProjects[i].openRefCount > 0) { - configuredProjects.push(this.configuredProjects[i]); + const proj = this.configuredProjects[i]; + if (proj.openRefCount > 0) { + configuredProjects.push(proj); + } + else { + proj.close(); } } this.configuredProjects = configuredProjects; @@ -782,8 +807,9 @@ namespace ts.server { removeProject(project: Project) { this.log("remove project: " + project.getRootFiles().toString()); + project.close(); + if (project.isConfiguredProject()) { - (project).close(); this.configuredProjects = copyListRemovingItem((project), this.configuredProjects); } else { From 04916c86831dd7420a178cd331d48572fcc41069 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 3 Jun 2016 15:18:48 -0700 Subject: [PATCH 011/307] renames, introduce projectKind --- src/server/editorServices.ts | 456 +++++++++--------- src/server/session.ts | 2 +- .../cases/unittests/cachingInServerLSHost.ts | 2 +- 3 files changed, 234 insertions(+), 226 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 837eeafae53..0d16f3f0722 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -301,10 +301,9 @@ namespace ts.server { constructor( readonly projectKind: ProjectKind, - readonly projectFilename: string, - public readonly projectService: ProjectService, + readonly projectService: ProjectService, documentRegistry: ts.DocumentRegistry, - files: string[], + hasExplicitListOfFiles: boolean, compilerOptions: CompilerOptions) { if (!compilerOptions) { @@ -312,7 +311,7 @@ namespace ts.server { compilerOptions.allowNonTsExtensions = true; compilerOptions.allowJs = true; } - else if (files && files.length) { + else if (hasExplicitListOfFiles) { // If files are listed explicitly, allow all extensions compilerOptions.allowNonTsExtensions = true; } @@ -322,9 +321,8 @@ namespace ts.server { this.languageService = ts.createLanguageService(this.lsHost, documentRegistry); } - isConfiguredProject() { - // TODO: remove - return this.projectKind !== ProjectKind.Inferred; + getProjectFileName(): string { + return undefined; } close() { @@ -391,7 +389,7 @@ namespace ts.server { } getScriptInfo(fileName: string) { - return this.projectService.openFile(fileName, /*openedByClient*/ false); + return this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); } filesToString() { @@ -443,7 +441,7 @@ namespace ts.server { directoriesWatchedForTsconfig: string[] = []; constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry) { - super(ProjectKind.Inferred, /*projectFilename*/ undefined, projectService, documentRegistry, /*files*/ undefined, /*compilerOptions*/ undefined); + super(ProjectKind.Inferred, projectService, documentRegistry, /*files*/ undefined, /*compilerOptions*/ undefined); } close() { @@ -461,21 +459,22 @@ namespace ts.server { /** Used for configured projects which may have multiple open roots */ openRefCount = 0; - constructor(projectFilename: string, projectService: ProjectService, documentRegistry: ts.DocumentRegistry, files: string[], compilerOptions: CompilerOptions) { - super(ProjectKind.Configured, projectFilename, projectService, documentRegistry, files, compilerOptions); + constructor(readonly configFileName: string, projectService: ProjectService, documentRegistry: ts.DocumentRegistry, hasExplicitListOfFiles: boolean, compilerOptions: CompilerOptions) { + super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, compilerOptions); } - watchConfigFile(callback: (project: Project) => void) { - this.projectFileWatcher = this.projectService.host.watchFile(this.projectFilename, _ => callback(this)); + getProjectFileName() { + return this.configFileName; } - watchConfigDirectory(callback: (project: Project, path: string) => void) { - this.projectService.log("Add recursive watcher for: " + ts.getDirectoryPath(this.projectFilename)); - this.directoryWatcher = this.projectService.host.watchDirectory( - ts.getDirectoryPath(this.projectFilename), - path => callback(this, path), - /*recursive*/ true - ); + watchConfigFile(callback: (project: ConfiguredProject) => void) { + this.projectFileWatcher = this.projectService.host.watchFile(this.configFileName, _ => callback(this)); + } + + watchConfigDirectory(callback: (project: ConfiguredProject, path: string) => void) { + const directoryToWatch = ts.getDirectoryPath(this.configFileName); + this.projectService.log(`Add recursive watcher for: ${directoryToWatch}`); + this.directoryWatcher = this.projectService.host.watchDirectory(directoryToWatch, path => callback(this, path), /*recursive*/ true); } close() { @@ -499,6 +498,15 @@ namespace ts.server { } } + class ExternalProject extends Project { + constructor(readonly projectFileName: string, projectService: ProjectService, documentRegistry: ts.DocumentRegistry, compilerOptions: CompilerOptions) { + super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, compilerOptions); + } + + getProjectFileName() { + return this.projectFileName; + } + } export interface ProjectOpenResult { success?: boolean; @@ -534,11 +542,16 @@ namespace ts.server { } export class ProjectService { - filenameToScriptInfo: ts.Map = {}; + private filenameToScriptInfo: ts.Map = {}; /** * open, non-configured root files **/ openFileRoots: ScriptInfo[] = []; + + /** + * external projects (configuration and list of root files is not controlled by tsserver) + */ + externalProjects: ExternalProject[] = []; /** * projects built from openFileRoots **/ @@ -558,17 +571,17 @@ namespace ts.server { /** * a path to directory watcher map that detects added tsconfig files **/ - directoryWatchersForTsconfig: ts.Map = {}; + private directoryWatchersForTsconfig: ts.Map = {}; /** * count of how many projects are using the directory watcher. * If the number becomes 0 for a watcher, then we should close it. **/ - directoryWatchersRefCount: ts.Map = {}; + private directoryWatchersRefCount: ts.Map = {}; - hostConfiguration: HostConfiguration; - timerForDetectingProjectFileListChanges: Map = {}; + private hostConfiguration: HostConfiguration; + private timerForDetectingProjectFileListChanges: Map = {}; - documentRegistry: ts.DocumentRegistry; + private documentRegistry: ts.DocumentRegistry; constructor(public host: ServerHost, public psLogger: Logger, public eventHandler?: ProjectServiceEventHandler) { // ts.disableIncrementalParsing = true; @@ -576,6 +589,13 @@ namespace ts.server { this.documentRegistry = ts.createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); } + private setDefaultHostConfiguration() { + this.hostConfiguration = { + formatCodeOptions: getDefaultFormatCodeOptions(this.host), + hostInfo: "Unknown host" + }; + } + stopWatchingDirectory(directory: string) { // if the ref count for this directory watcher drops to 0, it's time to close it this.directoryWatchersRefCount[directory]--; @@ -586,13 +606,6 @@ namespace ts.server { } } - private setDefaultHostConfiguration() { - this.hostConfiguration = { - formatCodeOptions: getDefaultFormatCodeOptions(this.host), - hostInfo: "Unknown host" - }; - } - getFormatCodeOptions(file?: string) { if (file) { const info = this.filenameToScriptInfo[file]; @@ -603,7 +616,7 @@ namespace ts.server { return this.hostConfiguration.formatCodeOptions; } - private watchedFileChanged(fileName: string) { + private onSourceFileChanged(fileName: string) { const info = this.filenameToScriptInfo[fileName]; if (!info) { this.psLogger.info("Error: got watch notification for unknown file: " + fileName); @@ -611,7 +624,7 @@ namespace ts.server { if (!this.host.fileExists(fileName)) { // File was deleted - this.fileDeletedInFilesystem(info); + this.handleDeletedFile(info); } else { if (info && (!info.isOpen)) { @@ -620,35 +633,65 @@ namespace ts.server { } } + handleDeletedFile(info: ScriptInfo) { + this.psLogger.info(info.fileName + " deleted"); + + if (info.fileWatcher) { + info.fileWatcher.close(); + info.fileWatcher = undefined; + } + + if (!info.isOpen) { + this.filenameToScriptInfo[info.fileName] = undefined; + const referencingProjects = this.findReferencingProjects(info); + if (info.defaultProject) { + info.defaultProject.removeRoot(info); + } + for (let i = 0, len = referencingProjects.length; i < len; i++) { + referencingProjects[i].removeReferencedFile(info); + } + for (let j = 0, flen = this.openFileRoots.length; j < flen; j++) { + const openFile = this.openFileRoots[j]; + if (this.eventHandler) { + this.eventHandler("context", openFile.defaultProject, openFile.fileName); + } + } + for (let j = 0, flen = this.openFilesReferenced.length; j < flen; j++) { + const openFile = this.openFilesReferenced[j]; + if (this.eventHandler) { + this.eventHandler("context", openFile.defaultProject, openFile.fileName); + } + } + } + + this.printProjects(); + } /** * This is the callback function when a watched directory has added or removed source code files. * @param project the project that associates with this directory watcher * @param fileName the absolute file name that changed in watched directory */ - directoryWatchedForSourceFilesChanged(project: Project, fileName: string) { + private onSourceFileInDirectoryChangedForConfiguredProject(project: ConfiguredProject, fileName: string) { // If a change was made inside "folder/file", node will trigger the callback twice: // one with the fileName being "folder/file", and the other one with "folder". // We don't respond to the second one. - if (fileName && !ts.isSupportedSourceFileName(fileName, project.isConfiguredProject() && project.getCompilerOptions())) { + if (fileName && !ts.isSupportedSourceFileName(fileName, project.getCompilerOptions())) { return; } this.log("Detected source file changes: " + fileName); - this.startTimerForDetectingProjectFileListChanges(project); - } - - startTimerForDetectingProjectFileListChanges(project: Project) { - if (this.timerForDetectingProjectFileListChanges[project.projectFilename]) { - this.host.clearTimeout(this.timerForDetectingProjectFileListChanges[project.projectFilename]); + const timeoutId = this.timerForDetectingProjectFileListChanges[project.configFileName]; + if (timeoutId) { + this.host.clearTimeout(timeoutId); } - this.timerForDetectingProjectFileListChanges[project.projectFilename] = this.host.setTimeout( - () => this.handleProjectFileListChanges(project), + this.timerForDetectingProjectFileListChanges[project.configFileName] = this.host.setTimeout( + () => this.handleChangeInSourceFileForConfiguredProject(project), 250 ); } - handleProjectFileListChanges(project: Project) { - const { projectOptions } = this.configFileToProjectOptions(project.projectFilename); + handleChangeInSourceFileForConfiguredProject(project: ConfiguredProject) { + const { projectOptions } = this.configFileToProjectOptions(project.configFileName); const newRootFiles = projectOptions.files.map((f => this.getCanonicalFileName(f))); const currentRootFiles = project.getRootFiles().map((f => this.getCanonicalFileName(f))); @@ -666,10 +709,16 @@ namespace ts.server { } } + private onConfigChangedForConfiguredProject(project: ConfiguredProject) { + this.log("Config file changed: " + project.configFileName); + this.updateConfiguredProject(project); + this.updateProjectStructure(); + } + /** * This is the callback function when a watched directory has an added tsconfig file. */ - directoryWatchedForTsconfigChanged(fileName: string) { + onConfigChangeForInferredProject(fileName: string) { if (ts.getBaseFileName(fileName) != "tsconfig.json") { this.log(fileName + " is not tsconfig.json"); return; @@ -680,12 +729,10 @@ namespace ts.server { const { projectOptions } = this.configFileToProjectOptions(fileName); const rootFilesInTsconfig = projectOptions.files.map(f => this.getCanonicalFileName(f)); - const openFileRoots = this.openFileRoots.map(s => this.getCanonicalFileName(s.fileName)); - // We should only care about the new tsconfig file if it contains any // opened root files of existing inferred projects - for (const openFileRoot of openFileRoots) { - if (contains(rootFilesInTsconfig, openFileRoot)) { + for (const rootFile of this.openFileRoots) { + if (contains(rootFilesInTsconfig, this.getCanonicalFileName(rootFile.fileName))) { this.reloadProjects(); return; } @@ -697,12 +744,6 @@ namespace ts.server { return ts.normalizePath(name); } - private watchedProjectConfigFileChanged(project: Project) { - this.log("Config file changed: " + project.projectFilename); - this.updateConfiguredProject(project); - this.updateProjectStructure(); - } - log(msg: string, type = "Err") { this.psLogger.msg(msg, type); } @@ -738,14 +779,13 @@ namespace ts.server { let currentPath = ts.getDirectoryPath(root.fileName); let parentPath = ts.getDirectoryPath(currentPath); while (currentPath != parentPath) { - if (!project.projectService.directoryWatchersForTsconfig[currentPath]) { + if (!this.directoryWatchersForTsconfig[currentPath]) { this.log("Add watcher for: " + currentPath); - project.projectService.directoryWatchersForTsconfig[currentPath] = - this.host.watchDirectory(currentPath, fileName => this.directoryWatchedForTsconfigChanged(fileName)); - project.projectService.directoryWatchersRefCount[currentPath] = 1; + this.directoryWatchersForTsconfig[currentPath] = this.host.watchDirectory(currentPath, fileName => this.onConfigChangeForInferredProject(fileName)); + this.directoryWatchersRefCount[currentPath] = 1; } else { - project.projectService.directoryWatchersRefCount[currentPath] += 1; + this.directoryWatchersRefCount[currentPath] += 1; } project.directoriesWatchedForTsconfig.push(currentPath); currentPath = parentPath; @@ -757,41 +797,7 @@ namespace ts.server { return project; } - fileDeletedInFilesystem(info: ScriptInfo) { - this.psLogger.info(info.fileName + " deleted"); - - if (info.fileWatcher) { - info.fileWatcher.close(); - info.fileWatcher = undefined; - } - - if (!info.isOpen) { - this.filenameToScriptInfo[info.fileName] = undefined; - const referencingProjects = this.findReferencingProjects(info); - if (info.defaultProject) { - info.defaultProject.removeRoot(info); - } - for (let i = 0, len = referencingProjects.length; i < len; i++) { - referencingProjects[i].removeReferencedFile(info); - } - for (let j = 0, flen = this.openFileRoots.length; j < flen; j++) { - const openFile = this.openFileRoots[j]; - if (this.eventHandler) { - this.eventHandler("context", openFile.defaultProject, openFile.fileName); - } - } - for (let j = 0, flen = this.openFilesReferenced.length; j < flen; j++) { - const openFile = this.openFilesReferenced[j]; - if (this.eventHandler) { - this.eventHandler("context", openFile.defaultProject, openFile.fileName); - } - } - } - - this.printProjects(); - } - - updateConfiguredProjectList() { + refreshConfiguredProjects() { const configuredProjects: ConfiguredProject[] = []; for (let i = 0, len = this.configuredProjects.length; i < len; i++) { const proj = this.configuredProjects[i]; @@ -807,80 +813,76 @@ namespace ts.server { removeProject(project: Project) { this.log("remove project: " + project.getRootFiles().toString()); + project.close(); - if (project.isConfiguredProject()) { - this.configuredProjects = copyListRemovingItem((project), this.configuredProjects); - } - else { - for (const directory of (project).directoriesWatchedForTsconfig) { - // if the ref count for this directory watcher drops to 0, it's time to close it - project.projectService.directoryWatchersRefCount[directory]--; - if (!project.projectService.directoryWatchersRefCount[directory]) { - this.log("Close directory watcher for: " + directory); - project.projectService.directoryWatchersForTsconfig[directory].close(); - delete project.projectService.directoryWatchersForTsconfig[directory]; - } - } - this.inferredProjects = copyListRemovingItem((project), this.inferredProjects); + switch (project.projectKind) { + case ProjectKind.External: + this.externalProjects = copyListRemovingItem(project, this.externalProjects); + break; + case ProjectKind.Configured: + this.configuredProjects = copyListRemovingItem((project), this.configuredProjects); + break; + case ProjectKind.Inferred: + this.inferredProjects = copyListRemovingItem((project), this.inferredProjects); + break; } - const fileNames = project.getFileNames(); - for (const fileName of fileNames) { + for (const fileName of project.getFileNames()) { const info = this.getScriptInfo(fileName); - if (info.defaultProject == project) { + if (info.defaultProject === project) { info.defaultProject = undefined; } } } - setConfiguredProjectRoot(info: ScriptInfo) { - for (let i = 0, len = this.configuredProjects.length; i < len; i++) { - const configuredProject = this.configuredProjects[i]; - if (configuredProject.isRoot(info)) { - info.defaultProject = configuredProject; - configuredProject.addOpenRef(); - return true; + findContainingConfiguredProject(info: ScriptInfo): ConfiguredProject { + for (const proj of this.configuredProjects) { + if (proj.isRoot(info)) { + return proj; } } - return false; + return undefined; } addOpenFile(info: ScriptInfo) { - if (this.setConfiguredProjectRoot(info)) { - this.openFileRootsConfigured.push(info); + const configuredProject = this.findContainingConfiguredProject(info); + if (configuredProject) { + info.defaultProject = configuredProject; + configuredProject.addOpenRef(); + + // ?? better do this on file close + this.refreshConfiguredProjects(); + return; + } + + this.findReferencingProjects(info); + if (info.defaultProject) { + this.openFilesReferenced.push(info); } else { - this.findReferencingProjects(info); - if (info.defaultProject) { - this.openFilesReferenced.push(info); - } - else { - // create new inferred project p with the newly opened file as root - info.defaultProject = this.createInferredProject(info); - const openFileRoots: ScriptInfo[] = []; - // for each inferred project root r - for (let i = 0, len = this.openFileRoots.length; i < len; i++) { - const r = this.openFileRoots[i]; - // if r referenced by the new project - if (info.defaultProject.containsScriptInfo(r)) { - // remove project rooted at r - this.removeProject(r.defaultProject); - // put r in referenced open file list - this.openFilesReferenced.push(r); - // set default project of r to the new project - r.defaultProject = info.defaultProject; - } - else { - // otherwise, keep r as root of inferred project - openFileRoots.push(r); - } + // create new inferred project p with the newly opened file as root + info.defaultProject = this.createInferredProject(info); + const openFileRoots: ScriptInfo[] = []; + // for each inferred project root r + for (const rootFile of this.openFileRoots) { + // if r referenced by the new project + if (info.defaultProject.containsScriptInfo(rootFile)) { + // remove project rooted at r + this.removeProject(rootFile.defaultProject); + // put r in referenced open file list + this.openFilesReferenced.push(rootFile); + // set default project of r to the new project + rootFile.defaultProject = info.defaultProject; + } + else { + // otherwise, keep r as root of inferred project + openFileRoots.push(rootFile); } - this.openFileRoots = openFileRoots; - this.openFileRoots.push(info); } + this.openFileRoots = openFileRoots; + this.openFileRoots.push(info); } - this.updateConfiguredProjectList(); } /** @@ -895,28 +897,29 @@ namespace ts.server { const openFileRoots: ScriptInfo[] = []; let removedProject: Project; - for (let i = 0, len = this.openFileRoots.length; i < len; i++) { + for (const rootFile of this.openFileRoots) { // if closed file is root of project - if (info === this.openFileRoots[i]) { + if (info === rootFile) { // remove that project and remember it removedProject = info.defaultProject; } else { - openFileRoots.push(this.openFileRoots[i]); + openFileRoots.push(rootFile); } } + this.openFileRoots = openFileRoots; if (!removedProject) { const openFileRootsConfigured: ScriptInfo[] = []; - for (let i = 0, len = this.openFileRootsConfigured.length; i < len; i++) { - if (info === this.openFileRootsConfigured[i]) { + for (const configuredRoot of this.openFileRootsConfigured) { + if (info === configuredRoot) { if ((info.defaultProject).deleteOpenRef() === 0) { removedProject = info.defaultProject; } } else { - openFileRootsConfigured.push(this.openFileRootsConfigured[i]); + openFileRootsConfigured.push(configuredRoot); } } @@ -927,8 +930,7 @@ namespace ts.server { const openFilesReferenced: ScriptInfo[] = []; const orphanFiles: ScriptInfo[] = []; // for all open, referenced files f - for (let i = 0, len = this.openFilesReferenced.length; i < len; i++) { - const f = this.openFilesReferenced[i]; + for (const f of this.openFilesReferenced) { // if f was referenced by the removed project, remember it if (f.defaultProject === removedProject || !f.defaultProject) { f.defaultProject = undefined; @@ -1015,8 +1017,7 @@ namespace ts.server { // references that file. If so, then just keep the file in the referenced list. // If not, add the file to an unattached list, to be rechecked later. const openFilesReferenced: ScriptInfo[] = []; - for (let i = 0, len = this.openFilesReferenced.length; i < len; i++) { - const referencedFile = this.openFilesReferenced[i]; + for (const referencedFile of this.openFilesReferenced) { referencedFile.defaultProject.updateGraph(); if (referencedFile.defaultProject.containsScriptInfo(referencedFile)) { openFilesReferenced.push(referencedFile); @@ -1035,15 +1036,14 @@ namespace ts.server { // inferred projects list (since it is no longer a root) and add // the file to the open, referenced file list. const openFileRoots: ScriptInfo[] = []; - for (let i = 0, len = this.openFileRoots.length; i < len; i++) { - const rootFile = this.openFileRoots[i]; + for (const rootFile of this.openFileRoots) { const rootedProject = rootFile.defaultProject; const referencingProjects = this.findReferencingProjects(rootFile, rootedProject); - if (rootFile.defaultProject && rootFile.defaultProject.isConfiguredProject()) { + if (rootFile.defaultProject && rootFile.defaultProject.projectKind !== ProjectKind.Inferred) { // If the root file has already been added into a configured project, // meaning the original inferred project is gone already. - if (!rootedProject.isConfiguredProject()) { + if (rootedProject.projectKind === ProjectKind.Inferred) { this.removeProject(rootedProject); } this.openFileRootsConfigured.push(rootFile); @@ -1080,7 +1080,7 @@ namespace ts.server { * @param filename is absolute pathname * @param fileContent is a known version of the file content that is more up to date than the one on disk */ - openFile(fileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { + getOrCreateScriptInfo(fileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { fileName = ts.normalizePath(fileName); let info = ts.lookUp(this.filenameToScriptInfo, fileName); if (!info) { @@ -1099,7 +1099,7 @@ namespace ts.server { info.setFormatOptions(this.getFormatCodeOptions()); this.filenameToScriptInfo[fileName] = info; if (!info.isOpen) { - info.fileWatcher = this.host.watchFile(fileName, _ => { this.watchedFileChanged(fileName); }); + info.fileWatcher = this.host.watchFile(fileName, _ => { this.onSourceFileChanged(fileName); }); } } } @@ -1147,7 +1147,7 @@ namespace ts.server { */ openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind): { configFileName?: string, configFileErrors?: Diagnostic[] } { const { configFileName, configFileErrors } = this.openOrUpdateConfiguredProjectForFile(fileName); - const info = this.openFile(fileName, /*openedByClient*/ true, fileContent, scriptKind); + const info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ true, fileContent, scriptKind); this.addOpenFile(info); this.printProjects(); return { configFileName, configFileErrors }; @@ -1167,17 +1167,16 @@ namespace ts.server { this.log("Config file name: " + configFileName, "Info"); const project = this.findConfiguredProjectByConfigFile(configFileName); if (!project) { - const configResult = this.openConfigFile(configFileName, fileName); - if (!configResult.success) { - return { configFileName, configFileErrors: configResult.errors }; + const { success, errors } = this.openConfigFile(configFileName, fileName); + if (!success) { + return { configFileName, configFileErrors: errors }; } else { // even if opening config file was successful, it could still // contain errors that were tolerated. this.log("Opened configuration file " + configFileName, "Info"); - this.configuredProjects.push(configResult.project); - if (configResult.errors && configResult.errors.length > 0) { - return { configFileName, configFileErrors: configResult.errors }; + if (errors && errors.length > 0) { + return { configFileName, configFileErrors: errors }; } } } @@ -1251,11 +1250,8 @@ namespace ts.server { this.psLogger.info(this.openFileRoots[i].fileName); } this.psLogger.info("Open files referenced by inferred or configured projects: "); - for (let i = 0, len = this.openFilesReferenced.length; i < len; i++) { - let fileInfo = this.openFilesReferenced[i].fileName; - if (this.openFilesReferenced[i].defaultProject.isConfiguredProject()) { - fileInfo += " (configured)"; - } + for (const referencedFile of this.openFilesReferenced) { + const fileInfo = `${referencedFile.fileName} ${ProjectKind[referencedFile.defaultProject.projectKind]}`; this.psLogger.info(fileInfo); } this.psLogger.info("Open file roots of configured projects: "); @@ -1266,21 +1262,29 @@ namespace ts.server { } loadExternalProject(externalProject: protocol.ExternalProject): Project { - let project = this.findConfiguredProjectByConfigFile(externalProject.projectFileName); + const project = this.findConfiguredProjectByConfigFile(externalProject.projectFileName); if (project) { this.updateConfiguredProjectWorker(project, externalProject.rootFiles, externalProject.options); + return project; } else { - // TODO: get and handle errors - ({ project } = this.createConfiguredProject( - externalProject.projectFileName, - externalProject.rootFiles, - externalProject.options, - /*watchConfigFile*/ false, - /*watchConfigDirectory*/ false)); - this.configuredProjects.push(project); + // check if root files contain tsconfig.json + // if yes - treat project as configured + const tsconfigFile = forEach(externalProject.rootFiles, f => getBaseFileName(f) === "tsconfig.json" && f); + if (tsconfigFile) { + const newRootFiles = copyListRemovingItem(tsconfigFile, externalProject.rootFiles); + const { success, project, errors } = this.openConfigFile(tsconfigFile); + if (success) { + // keep project alive + project.addOpenRef(); + } + return project; + } + else { + const { project, errors } = this.createAndAddExternalProject(externalProject.projectFileName, externalProject.rootFiles, externalProject.options); + return project; + } } - return project; } loadExternalProjects(externalProjects: protocol.ExternalProject[], openFiles: protocol.OpenFile[]): void { @@ -1288,15 +1292,15 @@ namespace ts.server { this.loadExternalProject(project); } for (const openFile of openFiles) { - this.openFile(openFile.fileName, /*openedByClient*/ true, openFile.content); + this.getOrCreateScriptInfo(openFile.fileName, /*openedByClient*/ true, openFile.content); } // TODO: return diff } findConfiguredProjectByConfigFile(configFileName: string) { - for (let i = 0, len = this.configuredProjects.length; i < len; i++) { - if (this.configuredProjects[i].projectFilename == configFileName) { - return this.configuredProjects[i]; + for (const configuredProject of this.configuredProjects) { + if (configuredProject.configFileName === configFileName) { + return configuredProject; } } return undefined; @@ -1335,12 +1339,29 @@ namespace ts.server { } - private createConfiguredProject(configFileName: string, files: string[], compilerOptions: CompilerOptions, watchConfigFile: boolean, watchConfigDirectory: boolean, clientFileName?: string) { + private createAndAddExternalProject(projectFileName: string, files: string[], compilerOptions: CompilerOptions, clientFileName?: string) { + const project = new ExternalProject(projectFileName, this, this.documentRegistry, compilerOptions); + const errors = this.addFilesToProject(project, files, clientFileName); + this.externalProjects.push(project); + return { project, errors }; + } + + private createAndAddConfiguredProject(configFileName: string, projectOptions: ProjectOptions, clientFileName?: string) { + const project = new ConfiguredProject(configFileName, this, this.documentRegistry, projectOptions.configHasFilesProperty, projectOptions.compilerOptions); + const errors = this.addFilesToProject(project, projectOptions.files, clientFileName); + project.watchConfigFile(project => this.onConfigChangedForConfiguredProject(project)); + if (!projectOptions.configHasFilesProperty) { + project.watchConfigDirectory((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path)); + } + this.configuredProjects.push(project); + return { project, errors }; + } + + private addFilesToProject(project: ConfiguredProject | ExternalProject, files: string[], clientFileName: string): Diagnostic[] { let errors: Diagnostic[]; - const project = new ConfiguredProject(configFileName, this, this.documentRegistry, files, compilerOptions); for (const rootFilename of files) { if (this.host.fileExists(rootFilename)) { - const info = this.openFile(rootFilename, /*openedByClient*/ clientFileName == rootFilename); + const info = this.getOrCreateScriptInfo(rootFilename, /*openedByClient*/ clientFileName == rootFilename); project.addRoot(info); } else { @@ -1348,13 +1369,7 @@ namespace ts.server { } } project.updateGraph(); - if (watchConfigFile) { - project.watchConfigFile(project => this.watchedProjectConfigFileChanged(project)); - } - if (watchConfigDirectory) { - project.watchConfigDirectory((project, path) => this.directoryWatchedForSourceFilesChanged(project, path)); - } - return { project, errors }; + return errors; } openConfigFile(configFileName: string, clientFileName?: string): { success: boolean, project?: ConfiguredProject, errors?: Diagnostic[] } { @@ -1363,23 +1378,16 @@ namespace ts.server { return { success: false, errors }; } else { - const { project, errors } = - this.createConfiguredProject(configFileName, - projectOptions.files, - projectOptions.compilerOptions, - /*watchConfigFile*/ true, - /*watchConfigDirectory*/ !projectOptions.configHasFilesProperty, - clientFileName); - + const { project, errors } = this.createAndAddConfiguredProject(configFileName, projectOptions, clientFileName); return { success: true, project, errors }; } } - updateConfiguredProjectWorker(project: Project, newFiles: string[], newOptions: CompilerOptions) { - const oldFileNames = project.getRootFiles(); - const newFileNames = ts.filter(newFiles, f => this.host.fileExists(f)); - const fileNamesToRemove = oldFileNames.filter(f => newFileNames.indexOf(f) < 0); - const fileNamesToAdd = newFileNames.filter(f => oldFileNames.indexOf(f) < 0); + updateConfiguredProjectWorker(project: ConfiguredProject, newRootFiles: string[], newOptions: CompilerOptions) { + const oldRootFiles = project.getRootFiles(); + const newFileNames = ts.filter(newRootFiles, f => this.host.fileExists(f)); + const fileNamesToRemove = oldRootFiles.filter(f => !contains(newFileNames, f)); + const fileNamesToAdd = newFileNames.filter(f => !contains(oldRootFiles, f)); for (const fileName of fileNamesToRemove) { const info = this.getScriptInfo(fileName); @@ -1391,19 +1399,19 @@ namespace ts.server { for (const fileName of fileNamesToAdd) { let info = this.getScriptInfo(fileName); if (!info) { - info = this.openFile(fileName, /*openedByClient*/ false); + info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); } else { // if the root file was opened by client, it would belong to either // openFileRoots or openFileReferenced. if (info.isOpen) { - if (this.openFileRoots.indexOf(info) >= 0) { + if (contains(this.openFileRoots, info)) { this.openFileRoots = copyListRemovingItem(info, this.openFileRoots); - if (info.defaultProject && !info.defaultProject.isConfiguredProject()) { + if (info.defaultProject && info.defaultProject.projectKind === ProjectKind.Inferred) { this.removeProject(info.defaultProject); } } - if (this.openFilesReferenced.indexOf(info) >= 0) { + if (contains(this.openFilesReferenced, info)) { this.openFilesReferenced = copyListRemovingItem(info, this.openFilesReferenced); } this.openFileRootsConfigured.push(info); @@ -1417,13 +1425,13 @@ namespace ts.server { project.updateGraph(); } - updateConfiguredProject(project: Project) { - if (!this.host.fileExists(project.projectFilename)) { + updateConfiguredProject(project: ConfiguredProject) { + if (!this.host.fileExists(project.configFileName)) { this.log("Config file deleted"); this.removeProject(project); } else { - const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(project.projectFilename); + const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(project.configFileName); if (!succeeded) { return errors; } diff --git a/src/server/session.ts b/src/server/session.ts index a3a2917664a..de7d9ec059d 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -435,7 +435,7 @@ namespace ts.server { const project = this.projectService.getProjectForFile(fileName); const projectInfo: protocol.ProjectInfo = { - configFileName: project.projectFilename + configFileName: project.getProjectFileName() }; if (needFileNameList) { diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index 40c1deb7476..36bdaad1cf3 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -80,7 +80,7 @@ namespace ts { }; const projectService = new server.ProjectService(serverHost, logger); - const rootScriptInfo = projectService.openFile(rootFile, /* openedByClient */true); + const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */true); const project = projectService.createInferredProject(rootScriptInfo); project.setCompilerOptions({ module: ts.ModuleKind.AMD } ); return { From 1498676df4daffca9bfa6e0871d45be5750df4b2 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 3 Jun 2016 18:07:28 -0700 Subject: [PATCH 012/307] added delta computation --- src/server/editorServices.ts | 121 ++++++++++++++++++++++++++++++----- src/server/session.ts | 4 +- 2 files changed, 107 insertions(+), 18 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 0d16f3f0722..82c5dd70ab9 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -297,7 +297,7 @@ namespace ts.server { private readonly lsHost: LSHost; readonly languageService: LanguageService; - private program: ts.Program; + protected program: ts.Program; constructor( readonly projectKind: ProjectKind, @@ -453,7 +453,86 @@ namespace ts.server { } } - class ConfiguredProject extends Project { + interface Delta { + addedFiles: string[]; + removedFiles: string[]; + replacedFiles: string[]; + projectName: string; + version: number; + } + + const id = (x: any) => x; + + abstract class VersionedProject extends Project { + + private lastReportedFileNames: Map; + private lastReportedVersion: number = 0; + private currentVersion: number = 1; + + updateGraph() { + const oldProgram = this.program; + + super.updateGraph(); + + if (!oldProgram || !oldProgram.structureIsReused) { + this.currentVersion++; + } + } + + getDeltaFromVersion(lastKnownVersion?: number): Delta { + if (this.lastReportedVersion === this.currentVersion) { + return { + projectName: this.getProjectFileName(), + addedFiles: [], + removedFiles: [], + version: this.currentVersion, + replacedFiles: [] + }; + } + if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) { + const lastReportedFileNames = this.lastReportedFileNames; + const currentFiles = arrayToMap(this.getFileNames(), id); + + const addedFiles: string[] = []; + const removedFiles: string[] = []; + for (const id in currentFiles) { + if (hasProperty(currentFiles, id) && !hasProperty(lastReportedFileNames, id)) { + addedFiles.push(id); + } + } + for (const id in lastReportedFileNames) { + if (hasProperty(lastReportedFileNames, id) && !hasProperty(currentFiles, id)) { + removedFiles.push(id); + } + } + this.lastReportedFileNames = currentFiles; + + this.lastReportedFileNames = currentFiles; + this.lastReportedVersion = this.currentVersion; + return { + projectName: this.getProjectFileName(), + addedFiles, + removedFiles, + version: this.currentVersion, + replacedFiles: [] + }; + } + else { + // unknown version - return everything + const projectFileNames = this.getFileNames(); + this.lastReportedFileNames = arrayToMap(projectFileNames, id); + return { + projectName: this.getProjectFileName(), + addedFiles: [], + removedFiles: [], + version: this.currentVersion, + replacedFiles: projectFileNames + }; + } + } + } + + class ConfiguredProject extends VersionedProject { private projectFileWatcher: FileWatcher; private directoryWatcher: FileWatcher; /** Used for configured projects which may have multiple open roots */ @@ -498,7 +577,7 @@ namespace ts.server { } } - class ExternalProject extends Project { + class ExternalProject extends VersionedProject { constructor(readonly projectFileName: string, projectService: ProjectService, documentRegistry: ts.DocumentRegistry, compilerOptions: CompilerOptions) { super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, compilerOptions); } @@ -1261,28 +1340,38 @@ namespace ts.server { this.psLogger.endGroup(); } - loadExternalProject(externalProject: protocol.ExternalProject): Project { + loadExternalProject(externalProject: protocol.ExternalProject): Delta[] { const project = this.findConfiguredProjectByConfigFile(externalProject.projectFileName); if (project) { this.updateConfiguredProjectWorker(project, externalProject.rootFiles, externalProject.options); - return project; + return [project.getDeltaFromVersion()]; } else { - // check if root files contain tsconfig.json - // if yes - treat project as configured - const tsconfigFile = forEach(externalProject.rootFiles, f => getBaseFileName(f) === "tsconfig.json" && f); - if (tsconfigFile) { - const newRootFiles = copyListRemovingItem(tsconfigFile, externalProject.rootFiles); - const { success, project, errors } = this.openConfigFile(tsconfigFile); - if (success) { - // keep project alive - project.addOpenRef(); + let tsConfigFiles: string[]; + const rootFiles: string[] = []; + for (const file of externalProject.rootFiles) { + if (getBaseFileName(file) === "tsconfig.json") { + (tsConfigFiles || (tsConfigFiles = [])).push(file); } - return project; + else { + rootFiles.push(file); + } + } + if (tsConfigFiles) { + const deltas: Delta[] = []; + for (const tsconfigFile of tsConfigFiles) { + const { success, project, errors } = this.openConfigFile(tsconfigFile); + if (success) { + // keep project alive + project.addOpenRef(); + deltas.push(project.getDeltaFromVersion()); + } + } + return deltas; } else { const { project, errors } = this.createAndAddExternalProject(externalProject.projectFileName, externalProject.rootFiles, externalProject.options); - return project; + return [project.getDeltaFromVersion()]; } } } diff --git a/src/server/session.ts b/src/server/session.ts index de7d9ec059d..d943acc0ef1 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1029,8 +1029,8 @@ namespace ts.server { private handlers: Map<(request: protocol.Request) => { response?: any, responseRequired?: boolean }> = { [CommandNames.LoadExternalProject]: (request: protocol.Request) => { - const project = this.projectService.loadExternalProject(request.arguments); - return { responseRequired: true, response: { files: project.getFileNames() } }; + const deltas = this.projectService.loadExternalProject(request.arguments); + return { responseRequired: true, response: { files: deltas } }; }, [CommandNames.Exit]: () => { this.exit(); From bcf58bf9e87e6e4436afb60953bd00799ace380d Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 7 Jun 2016 13:53:33 -0700 Subject: [PATCH 013/307] group members based on accessibility --- src/server/editorServices.ts | 707 +++++++++--------- .../cases/unittests/cachingInServerLSHost.ts | 2 +- 2 files changed, 350 insertions(+), 359 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 82c5dd70ab9..01cdbee8091 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -712,7 +712,7 @@ namespace ts.server { } } - handleDeletedFile(info: ScriptInfo) { + private handleDeletedFile(info: ScriptInfo) { this.psLogger.info(info.fileName + " deleted"); if (info.fileWatcher) { @@ -769,7 +769,7 @@ namespace ts.server { ); } - handleChangeInSourceFileForConfiguredProject(project: ConfiguredProject) { + private handleChangeInSourceFileForConfiguredProject(project: ConfiguredProject) { const { projectOptions } = this.configFileToProjectOptions(project.configFileName); const newRootFiles = projectOptions.files.map((f => this.getCanonicalFileName(f))); @@ -797,7 +797,7 @@ namespace ts.server { /** * This is the callback function when a watched directory has an added tsconfig file. */ - onConfigChangeForInferredProject(fileName: string) { + private onConfigChangeForInferredProject(fileName: string) { if (ts.getBaseFileName(fileName) != "tsconfig.json") { this.log(fileName + " is not tsconfig.json"); return; @@ -823,60 +823,7 @@ namespace ts.server { return ts.normalizePath(name); } - log(msg: string, type = "Err") { - this.psLogger.msg(msg, type); - } - - setHostConfiguration(args: ts.server.protocol.ConfigureRequestArguments) { - if (args.file) { - const info = this.filenameToScriptInfo[args.file]; - if (info) { - info.setFormatOptions(args.formatOptions); - this.log("Host configuration update for file " + args.file, "Info"); - } - } - else { - if (args.hostInfo !== undefined) { - this.hostConfiguration.hostInfo = args.hostInfo; - this.log("Host information " + args.hostInfo, "Info"); - } - if (args.formatOptions) { - mergeFormatOptions(this.hostConfiguration.formatCodeOptions, args.formatOptions); - this.log("Format host information updated", "Info"); - } - } - } - - closeLog() { - this.psLogger.close(); - } - - createInferredProject(root: ScriptInfo) { - const project = new InferredProject(this, this.documentRegistry); - project.addRoot(root); - - let currentPath = ts.getDirectoryPath(root.fileName); - let parentPath = ts.getDirectoryPath(currentPath); - while (currentPath != parentPath) { - if (!this.directoryWatchersForTsconfig[currentPath]) { - this.log("Add watcher for: " + currentPath); - this.directoryWatchersForTsconfig[currentPath] = this.host.watchDirectory(currentPath, fileName => this.onConfigChangeForInferredProject(fileName)); - this.directoryWatchersRefCount[currentPath] = 1; - } - else { - this.directoryWatchersRefCount[currentPath] += 1; - } - project.directoriesWatchedForTsconfig.push(currentPath); - currentPath = parentPath; - parentPath = ts.getDirectoryPath(parentPath); - } - - project.updateGraph(); - this.inferredProjects.push(project); - return project; - } - - refreshConfiguredProjects() { + private refreshConfiguredProjects() { const configuredProjects: ConfiguredProject[] = []; for (let i = 0, len = this.configuredProjects.length; i < len; i++) { const proj = this.configuredProjects[i]; @@ -890,7 +837,7 @@ namespace ts.server { this.configuredProjects = configuredProjects; } - removeProject(project: Project) { + private removeProject(project: Project) { this.log("remove project: " + project.getRootFiles().toString()); project.close(); @@ -915,7 +862,7 @@ namespace ts.server { } } - findContainingConfiguredProject(info: ScriptInfo): ConfiguredProject { + private findContainingConfiguredProject(info: ScriptInfo): ConfiguredProject { for (const proj of this.configuredProjects) { if (proj.isRoot(info)) { return proj; @@ -924,7 +871,7 @@ namespace ts.server { return undefined; } - addOpenFile(info: ScriptInfo) { + private addOpenFile(info: ScriptInfo) { const configuredProject = this.findContainingConfiguredProject(info); if (configuredProject) { info.defaultProject = configuredProject; @@ -941,7 +888,7 @@ namespace ts.server { } else { // create new inferred project p with the newly opened file as root - info.defaultProject = this.createInferredProject(info); + info.defaultProject = this.createAndAddInferredProject(info); const openFileRoots: ScriptInfo[] = []; // for each inferred project root r for (const rootFile of this.openFileRoots) { @@ -968,7 +915,7 @@ namespace ts.server { * Remove this file from the set of open, non-configured files. * @param info The file that has been closed or newly configured */ - closeOpenFile(info: ScriptInfo) { + private closeOpenFile(info: ScriptInfo) { // Closing file should trigger re-reading the file content from disk. This is // because the user may chose to discard the buffer content before saving // to the disk, and the server's version of the file can be out of sync. @@ -1032,6 +979,346 @@ namespace ts.server { info.close(); } + /** + * This function 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; otherwise we create a new one. + */ + private openOrUpdateConfiguredProjectForFile(fileName: string): { configFileName?: string, configFileErrors?: Diagnostic[] } { + const searchPath = ts.normalizePath(getDirectoryPath(fileName)); + this.log("Search path: " + searchPath, "Info"); + // check if this file is already included in one of external projects + const configFileName = this.findConfigFile(searchPath); + if (configFileName) { + this.log("Config file name: " + configFileName, "Info"); + const project = this.findConfiguredProjectByConfigFile(configFileName); + if (!project) { + const { success, errors } = this.openConfigFile(configFileName, fileName); + if (!success) { + return { configFileName, configFileErrors: errors }; + } + else { + // even if opening config file was successful, it could still + // contain errors that were tolerated. + this.log("Opened configuration file " + configFileName, "Info"); + if (errors && errors.length > 0) { + return { configFileName, configFileErrors: errors }; + } + } + } + else { + this.updateConfiguredProject(project); + } + } + else { + this.log("No config files found."); + } + return configFileName ? { configFileName } : {}; + } + + // This is different from the method the compiler uses because + // the compiler can assume it will always start searching in the + // current directory (the directory in which tsc was invoked). + // The server must start searching from the directory containing + // the newly opened file. + private findConfigFile(searchPath: string): string { + while (true) { + const tsconfigFileName = ts.combinePaths(searchPath, "tsconfig.json"); + if (this.host.fileExists(tsconfigFileName)) { + return tsconfigFileName; + } + + const jsconfigFileName = ts.combinePaths(searchPath, "jsconfig.json"); + if (this.host.fileExists(jsconfigFileName)) { + return jsconfigFileName; + } + + const parentPath = ts.getDirectoryPath(searchPath); + if (parentPath === searchPath) { + break; + } + searchPath = parentPath; + } + return undefined; + } + + private printProjects() { + if (!this.psLogger.isVerbose()) { + return; + } + this.psLogger.startGroup(); + for (let i = 0, len = this.inferredProjects.length; i < len; i++) { + const project = this.inferredProjects[i]; + project.updateGraph(); + this.psLogger.info("Project " + i.toString()); + this.psLogger.info(project.filesToString()); + this.psLogger.info("-----------------------------------------------"); + } + for (let i = 0, len = this.configuredProjects.length; i < len; i++) { + const project = this.configuredProjects[i]; + project.updateGraph(); + this.psLogger.info("Project (configured) " + (i + this.inferredProjects.length).toString()); + this.psLogger.info(project.filesToString()); + this.psLogger.info("-----------------------------------------------"); + } + this.psLogger.info("Open file roots of inferred projects: "); + for (let i = 0, len = this.openFileRoots.length; i < len; i++) { + this.psLogger.info(this.openFileRoots[i].fileName); + } + this.psLogger.info("Open files referenced by inferred or configured projects: "); + for (const referencedFile of this.openFilesReferenced) { + const fileInfo = `${referencedFile.fileName} ${ProjectKind[referencedFile.defaultProject.projectKind]}`; + this.psLogger.info(fileInfo); + } + this.psLogger.info("Open file roots of configured projects: "); + for (let i = 0, len = this.openFileRootsConfigured.length; i < len; i++) { + this.psLogger.info(this.openFileRootsConfigured[i].fileName); + } + this.psLogger.endGroup(); + } + + private findConfiguredProjectByConfigFile(configFileName: string) { + for (const configuredProject of this.configuredProjects) { + if (configuredProject.configFileName === configFileName) { + return configuredProject; + } + } + return undefined; + } + + private configFileToProjectOptions(configFilename: string): { succeeded: boolean, projectOptions?: ProjectOptions, errors?: Diagnostic[] } { + configFilename = ts.normalizePath(configFilename); + // file references will be relative to dirPath (or absolute) + const dirPath = ts.getDirectoryPath(configFilename); + const contents = this.host.readFile(configFilename); + const rawConfig: { config?: ProjectOptions; error?: Diagnostic; } = ts.parseConfigFileTextToJson(configFilename, contents); + if (rawConfig.error) { + return { succeeded: false, errors: [rawConfig.error] }; + } + else { + const configHasFilesProperty = rawConfig.config["files"] !== undefined; + const parsedCommandLine = ts.parseJsonConfigFileContent(rawConfig.config, this.host, dirPath, /*existingOptions*/ {}, configFilename); + Debug.assert(!!parsedCommandLine.fileNames); + + if (parsedCommandLine.errors && (parsedCommandLine.errors.length > 0)) { + return { succeeded: false, errors: parsedCommandLine.errors }; + } + else if (parsedCommandLine.fileNames.length === 0) { + const error = createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename); + return { succeeded: false, errors: [error] }; + } + else { + const projectOptions: ProjectOptions = { + files: parsedCommandLine.fileNames, + compilerOptions: parsedCommandLine.options, + configHasFilesProperty + }; + return { succeeded: true, projectOptions }; + } + } + + } + + private createAndAddExternalProject(projectFileName: string, files: string[], compilerOptions: CompilerOptions, clientFileName?: string) { + const project = new ExternalProject(projectFileName, this, this.documentRegistry, compilerOptions); + const errors = this.addFilesToProject(project, files, clientFileName); + this.externalProjects.push(project); + return { project, errors }; + } + + private createAndAddConfiguredProject(configFileName: string, projectOptions: ProjectOptions, clientFileName?: string) { + const project = new ConfiguredProject(configFileName, this, this.documentRegistry, projectOptions.configHasFilesProperty, projectOptions.compilerOptions); + const errors = this.addFilesToProject(project, projectOptions.files, clientFileName); + project.watchConfigFile(project => this.onConfigChangedForConfiguredProject(project)); + if (!projectOptions.configHasFilesProperty) { + project.watchConfigDirectory((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path)); + } + this.configuredProjects.push(project); + return { project, errors }; + } + + private addFilesToProject(project: ConfiguredProject | ExternalProject, files: string[], clientFileName: string): Diagnostic[] { + let errors: Diagnostic[]; + for (const rootFilename of files) { + if (this.host.fileExists(rootFilename)) { + const info = this.getOrCreateScriptInfo(rootFilename, /*openedByClient*/ clientFileName == rootFilename); + project.addRoot(info); + } + else { + (errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.File_0_not_found, rootFilename)); + } + } + project.updateGraph(); + return errors; + } + + private openConfigFile(configFileName: string, clientFileName?: string): { success: boolean, project?: ConfiguredProject, errors?: Diagnostic[] } { + const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(configFileName); + if (!succeeded) { + return { success: false, errors }; + } + else { + const { project, errors } = this.createAndAddConfiguredProject(configFileName, projectOptions, clientFileName); + return { success: true, project, errors }; + } + } + + private updateConfiguredProjectWorker(project: ConfiguredProject, newRootFiles: string[], newOptions: CompilerOptions) { + const oldRootFiles = project.getRootFiles(); + const newFileNames = ts.filter(newRootFiles, f => this.host.fileExists(f)); + const fileNamesToRemove = oldRootFiles.filter(f => !contains(newFileNames, f)); + const fileNamesToAdd = newFileNames.filter(f => !contains(oldRootFiles, f)); + + for (const fileName of fileNamesToRemove) { + const info = this.getScriptInfo(fileName); + if (info) { + project.removeRoot(info); + } + } + + for (const fileName of fileNamesToAdd) { + let info = this.getScriptInfo(fileName); + if (!info) { + info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); + } + else { + // if the root file was opened by client, it would belong to either + // openFileRoots or openFileReferenced. + if (info.isOpen) { + if (contains(this.openFileRoots, info)) { + this.openFileRoots = copyListRemovingItem(info, this.openFileRoots); + if (info.defaultProject && info.defaultProject.projectKind === ProjectKind.Inferred) { + this.removeProject(info.defaultProject); + } + } + if (contains(this.openFilesReferenced, info)) { + this.openFilesReferenced = copyListRemovingItem(info, this.openFilesReferenced); + } + this.openFileRootsConfigured.push(info); + info.defaultProject = project; + } + } + project.addRoot(info); + } + + project.setCompilerOptions(newOptions); + project.updateGraph(); + } + + private updateConfiguredProject(project: ConfiguredProject) { + if (!this.host.fileExists(project.configFileName)) { + this.log("Config file deleted"); + this.removeProject(project); + } + else { + const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(project.configFileName); + if (!succeeded) { + return errors; + } + else { + this.updateConfiguredProjectWorker(project, projectOptions.files, projectOptions.compilerOptions); + } + } + } + + createAndAddInferredProject(root: ScriptInfo) { + const project = new InferredProject(this, this.documentRegistry); + project.addRoot(root); + + let currentPath = ts.getDirectoryPath(root.fileName); + let parentPath = ts.getDirectoryPath(currentPath); + while (currentPath != parentPath) { + if (!this.directoryWatchersForTsconfig[currentPath]) { + this.log("Add watcher for: " + currentPath); + this.directoryWatchersForTsconfig[currentPath] = this.host.watchDirectory(currentPath, fileName => this.onConfigChangeForInferredProject(fileName)); + this.directoryWatchersRefCount[currentPath] = 1; + } + else { + this.directoryWatchersRefCount[currentPath] += 1; + } + project.directoriesWatchedForTsconfig.push(currentPath); + currentPath = parentPath; + parentPath = ts.getDirectoryPath(parentPath); + } + + project.updateGraph(); + this.inferredProjects.push(project); + return project; + } + + /** + * @param filename is absolute pathname + * @param fileContent is a known version of the file content that is more up to date than the one on disk + */ + getOrCreateScriptInfo(fileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { + fileName = ts.normalizePath(fileName); + let info = ts.lookUp(this.filenameToScriptInfo, fileName); + if (!info) { + let content: string; + if (this.host.fileExists(fileName)) { + content = fileContent || this.host.readFile(fileName); + } + if (!content) { + if (openedByClient) { + content = ""; + } + } + if (content !== undefined) { + info = new ScriptInfo(this.host, fileName, content, openedByClient); + info.scriptKind = scriptKind; + info.setFormatOptions(this.getFormatCodeOptions()); + this.filenameToScriptInfo[fileName] = info; + if (!info.isOpen) { + info.fileWatcher = this.host.watchFile(fileName, _ => { this.onSourceFileChanged(fileName); }); + } + } + } + if (info) { + if (fileContent) { + info.svc.reload(fileContent); + } + if (openedByClient) { + info.isOpen = true; + } + } + return info; + } + + openExternalProject(proj: protocol.ExternalProject) { + } + + closeExternalProject(proj: protocol.ExternalProject) { + // TODO: save mapping from external project name to set of configured projects + } + + log(msg: string, type = "Err") { + this.psLogger.msg(msg, type); + } + + setHostConfiguration(args: ts.server.protocol.ConfigureRequestArguments) { + if (args.file) { + const info = this.filenameToScriptInfo[args.file]; + if (info) { + info.setFormatOptions(args.formatOptions); + this.log("Host configuration update for file " + args.file, "Info"); + } + } + else { + if (args.hostInfo !== undefined) { + this.hostConfiguration.hostInfo = args.hostInfo; + this.log("Host information " + args.hostInfo, "Info"); + } + if (args.formatOptions) { + mergeFormatOptions(this.hostConfiguration.formatCodeOptions, args.formatOptions); + this.log("Format host information updated", "Info"); + } + } + } + + closeLog() { + this.psLogger.close(); + } + findReferencingProjects(info: ScriptInfo, excludedProject?: Project) { const referencingProjects: Project[] = []; info.defaultProject = undefined; @@ -1155,70 +1442,6 @@ namespace ts.server { return ts.lookUp(this.filenameToScriptInfo, filename); } - /** - * @param filename is absolute pathname - * @param fileContent is a known version of the file content that is more up to date than the one on disk - */ - getOrCreateScriptInfo(fileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { - fileName = ts.normalizePath(fileName); - let info = ts.lookUp(this.filenameToScriptInfo, fileName); - if (!info) { - let content: string; - if (this.host.fileExists(fileName)) { - content = fileContent || this.host.readFile(fileName); - } - if (!content) { - if (openedByClient) { - content = ""; - } - } - if (content !== undefined) { - info = new ScriptInfo(this.host, fileName, content, openedByClient); - info.scriptKind = scriptKind; - info.setFormatOptions(this.getFormatCodeOptions()); - this.filenameToScriptInfo[fileName] = info; - if (!info.isOpen) { - info.fileWatcher = this.host.watchFile(fileName, _ => { this.onSourceFileChanged(fileName); }); - } - } - } - if (info) { - if (fileContent) { - info.svc.reload(fileContent); - } - if (openedByClient) { - info.isOpen = true; - } - } - return info; - } - - // This is different from the method the compiler uses because - // the compiler can assume it will always start searching in the - // current directory (the directory in which tsc was invoked). - // The server must start searching from the directory containing - // the newly opened file. - findConfigFile(searchPath: string): string { - while (true) { - const tsconfigFileName = ts.combinePaths(searchPath, "tsconfig.json"); - if (this.host.fileExists(tsconfigFileName)) { - return tsconfigFileName; - } - - const jsconfigFileName = ts.combinePaths(searchPath, "jsconfig.json"); - if (this.host.fileExists(jsconfigFileName)) { - return jsconfigFileName; - } - - const parentPath = ts.getDirectoryPath(searchPath); - if (parentPath === searchPath) { - break; - } - searchPath = parentPath; - } - return undefined; - } - /** * Open file whose contents is managed by the client * @param filename is absolute pathname @@ -1232,43 +1455,6 @@ namespace ts.server { return { configFileName, configFileErrors }; } - /** - * This function 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; otherwise we create a new one. - */ - openOrUpdateConfiguredProjectForFile(fileName: string): { configFileName?: string, configFileErrors?: Diagnostic[] } { - const searchPath = ts.normalizePath(getDirectoryPath(fileName)); - this.log("Search path: " + searchPath, "Info"); - // check if this file is already included in one of external projects - const configFileName = this.findConfigFile(searchPath); - if (configFileName) { - this.log("Config file name: " + configFileName, "Info"); - const project = this.findConfiguredProjectByConfigFile(configFileName); - if (!project) { - const { success, errors } = this.openConfigFile(configFileName, fileName); - if (!success) { - return { configFileName, configFileErrors: errors }; - } - else { - // even if opening config file was successful, it could still - // contain errors that were tolerated. - this.log("Opened configuration file " + configFileName, "Info"); - if (errors && errors.length > 0) { - return { configFileName, configFileErrors: errors }; - } - } - } - else { - this.updateConfiguredProject(project); - } - } - else { - this.log("No config files found."); - } - return configFileName ? { configFileName } : {}; - } - /** * Close file whose contents is managed by the client * @param filename is absolute pathname @@ -1289,57 +1475,6 @@ namespace ts.server { } } - printProjectsForFile(filename: string) { - const scriptInfo = ts.lookUp(this.filenameToScriptInfo, filename); - if (scriptInfo) { - this.psLogger.startGroup(); - this.psLogger.info("Projects for " + filename); - const projects = this.findReferencingProjects(scriptInfo); - for (let i = 0, len = projects.length; i < len; i++) { - this.psLogger.info("Project " + i.toString()); - } - this.psLogger.endGroup(); - } - else { - this.psLogger.info(filename + " not in any project"); - } - } - - printProjects() { - if (!this.psLogger.isVerbose()) { - return; - } - this.psLogger.startGroup(); - for (let i = 0, len = this.inferredProjects.length; i < len; i++) { - const project = this.inferredProjects[i]; - project.updateGraph(); - this.psLogger.info("Project " + i.toString()); - this.psLogger.info(project.filesToString()); - this.psLogger.info("-----------------------------------------------"); - } - for (let i = 0, len = this.configuredProjects.length; i < len; i++) { - const project = this.configuredProjects[i]; - project.updateGraph(); - this.psLogger.info("Project (configured) " + (i + this.inferredProjects.length).toString()); - this.psLogger.info(project.filesToString()); - this.psLogger.info("-----------------------------------------------"); - } - this.psLogger.info("Open file roots of inferred projects: "); - for (let i = 0, len = this.openFileRoots.length; i < len; i++) { - this.psLogger.info(this.openFileRoots[i].fileName); - } - this.psLogger.info("Open files referenced by inferred or configured projects: "); - for (const referencedFile of this.openFilesReferenced) { - const fileInfo = `${referencedFile.fileName} ${ProjectKind[referencedFile.defaultProject.projectKind]}`; - this.psLogger.info(fileInfo); - } - this.psLogger.info("Open file roots of configured projects: "); - for (let i = 0, len = this.openFileRootsConfigured.length; i < len; i++) { - this.psLogger.info(this.openFileRootsConfigured[i].fileName); - } - this.psLogger.endGroup(); - } - loadExternalProject(externalProject: protocol.ExternalProject): Delta[] { const project = this.findConfiguredProjectByConfigFile(externalProject.projectFileName); if (project) { @@ -1385,149 +1520,5 @@ namespace ts.server { } // TODO: return diff } - - findConfiguredProjectByConfigFile(configFileName: string) { - for (const configuredProject of this.configuredProjects) { - if (configuredProject.configFileName === configFileName) { - return configuredProject; - } - } - return undefined; - } - - configFileToProjectOptions(configFilename: string): { succeeded: boolean, projectOptions?: ProjectOptions, errors?: Diagnostic[] } { - configFilename = ts.normalizePath(configFilename); - // file references will be relative to dirPath (or absolute) - const dirPath = ts.getDirectoryPath(configFilename); - const contents = this.host.readFile(configFilename); - const rawConfig: { config?: ProjectOptions; error?: Diagnostic; } = ts.parseConfigFileTextToJson(configFilename, contents); - if (rawConfig.error) { - return { succeeded: false, errors: [rawConfig.error] }; - } - else { - const configHasFilesProperty = rawConfig.config["files"] !== undefined; - const parsedCommandLine = ts.parseJsonConfigFileContent(rawConfig.config, this.host, dirPath, /*existingOptions*/ {}, configFilename); - Debug.assert(!!parsedCommandLine.fileNames); - - if (parsedCommandLine.errors && (parsedCommandLine.errors.length > 0)) { - return { succeeded: false, errors: parsedCommandLine.errors }; - } - else if (parsedCommandLine.fileNames.length === 0) { - const error = createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename); - return { succeeded: false, errors: [error] }; - } - else { - const projectOptions: ProjectOptions = { - files: parsedCommandLine.fileNames, - compilerOptions: parsedCommandLine.options, - configHasFilesProperty - }; - return { succeeded: true, projectOptions }; - } - } - - } - - private createAndAddExternalProject(projectFileName: string, files: string[], compilerOptions: CompilerOptions, clientFileName?: string) { - const project = new ExternalProject(projectFileName, this, this.documentRegistry, compilerOptions); - const errors = this.addFilesToProject(project, files, clientFileName); - this.externalProjects.push(project); - return { project, errors }; - } - - private createAndAddConfiguredProject(configFileName: string, projectOptions: ProjectOptions, clientFileName?: string) { - const project = new ConfiguredProject(configFileName, this, this.documentRegistry, projectOptions.configHasFilesProperty, projectOptions.compilerOptions); - const errors = this.addFilesToProject(project, projectOptions.files, clientFileName); - project.watchConfigFile(project => this.onConfigChangedForConfiguredProject(project)); - if (!projectOptions.configHasFilesProperty) { - project.watchConfigDirectory((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path)); - } - this.configuredProjects.push(project); - return { project, errors }; - } - - private addFilesToProject(project: ConfiguredProject | ExternalProject, files: string[], clientFileName: string): Diagnostic[] { - let errors: Diagnostic[]; - for (const rootFilename of files) { - if (this.host.fileExists(rootFilename)) { - const info = this.getOrCreateScriptInfo(rootFilename, /*openedByClient*/ clientFileName == rootFilename); - project.addRoot(info); - } - else { - (errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.File_0_not_found, rootFilename)); - } - } - project.updateGraph(); - return errors; - } - - openConfigFile(configFileName: string, clientFileName?: string): { success: boolean, project?: ConfiguredProject, errors?: Diagnostic[] } { - const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(configFileName); - if (!succeeded) { - return { success: false, errors }; - } - else { - const { project, errors } = this.createAndAddConfiguredProject(configFileName, projectOptions, clientFileName); - return { success: true, project, errors }; - } - } - - updateConfiguredProjectWorker(project: ConfiguredProject, newRootFiles: string[], newOptions: CompilerOptions) { - const oldRootFiles = project.getRootFiles(); - const newFileNames = ts.filter(newRootFiles, f => this.host.fileExists(f)); - const fileNamesToRemove = oldRootFiles.filter(f => !contains(newFileNames, f)); - const fileNamesToAdd = newFileNames.filter(f => !contains(oldRootFiles, f)); - - for (const fileName of fileNamesToRemove) { - const info = this.getScriptInfo(fileName); - if (info) { - project.removeRoot(info); - } - } - - for (const fileName of fileNamesToAdd) { - let info = this.getScriptInfo(fileName); - if (!info) { - info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); - } - else { - // if the root file was opened by client, it would belong to either - // openFileRoots or openFileReferenced. - if (info.isOpen) { - if (contains(this.openFileRoots, info)) { - this.openFileRoots = copyListRemovingItem(info, this.openFileRoots); - if (info.defaultProject && info.defaultProject.projectKind === ProjectKind.Inferred) { - this.removeProject(info.defaultProject); - } - } - if (contains(this.openFilesReferenced, info)) { - this.openFilesReferenced = copyListRemovingItem(info, this.openFilesReferenced); - } - this.openFileRootsConfigured.push(info); - info.defaultProject = project; - } - } - project.addRoot(info); - } - - project.setCompilerOptions(newOptions); - project.updateGraph(); - } - - updateConfiguredProject(project: ConfiguredProject) { - if (!this.host.fileExists(project.configFileName)) { - this.log("Config file deleted"); - this.removeProject(project); - } - else { - const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(project.configFileName); - if (!succeeded) { - return errors; - } - else { - this.updateConfiguredProjectWorker(project, projectOptions.files, projectOptions.compilerOptions); - } - } - } } } diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index 36bdaad1cf3..31bd89955c7 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -81,7 +81,7 @@ namespace ts { const projectService = new server.ProjectService(serverHost, logger); const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */true); - const project = projectService.createInferredProject(rootScriptInfo); + const project = projectService.createAndAddInferredProject(rootScriptInfo); project.setCompilerOptions({ module: ts.ModuleKind.AMD } ); return { project, From 00f35d1934ae86a0cabc21f3ec04e37fddc68395 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 7 Jun 2016 16:46:40 -0700 Subject: [PATCH 014/307] added message handlers --- src/server/editorServices.ts | 170 +++++++++++++++++++---------------- src/server/protocol.d.ts | 59 ++++++++++-- src/server/session.ts | 49 +++++++--- 3 files changed, 183 insertions(+), 95 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 01cdbee8091..c5c099ce161 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -424,16 +424,6 @@ namespace ts.server { script.svc.reloadFromFile(tmpfilename, cb); } } - - editScript(filename: string, start: number, end: number, newText: string) { - const script = this.getScriptInfo(filename); - if (script) { - script.editContent(start, end, newText); - return; - } - - throw new Error("No script with name '" + filename + "'"); - } } class InferredProject extends Project { @@ -453,21 +443,19 @@ namespace ts.server { } } - interface Delta { - addedFiles: string[]; - removedFiles: string[]; - replacedFiles: string[]; - projectName: string; - version: number; + function findVersionedProjectByFileName(projectFileName: string, projects: T[]): T { + for (const proj of projects) { + if (proj.getProjectFileName() === projectFileName) { + return proj; + } + } } - const id = (x: any) => x; - abstract class VersionedProject extends Project { private lastReportedFileNames: Map; private lastReportedVersion: number = 0; - private currentVersion: number = 1; + currentVersion: number = 1; updateGraph() { const oldProgram = this.program; @@ -479,55 +467,41 @@ namespace ts.server { } } - getDeltaFromVersion(lastKnownVersion?: number): Delta { + getChangesSinceVersion(lastKnownVersion?: number): protocol.ExternalProjectFiles { + const info = { + projectFileName: this.getProjectFileName(), + version: this.currentVersion + }; if (this.lastReportedVersion === this.currentVersion) { - return { - projectName: this.getProjectFileName(), - addedFiles: [], - removedFiles: [], - version: this.currentVersion, - replacedFiles: [] - }; + return { info }; } if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) { const lastReportedFileNames = this.lastReportedFileNames; - const currentFiles = arrayToMap(this.getFileNames(), id); + const currentFiles = arrayToMap(this.getFileNames(), x => x); - const addedFiles: string[] = []; - const removedFiles: string[] = []; + const added: string[] = []; + const removed: string[] = []; for (const id in currentFiles) { if (hasProperty(currentFiles, id) && !hasProperty(lastReportedFileNames, id)) { - addedFiles.push(id); + added.push(id); } } for (const id in lastReportedFileNames) { if (hasProperty(lastReportedFileNames, id) && !hasProperty(currentFiles, id)) { - removedFiles.push(id); + removed.push(id); } } this.lastReportedFileNames = currentFiles; this.lastReportedFileNames = currentFiles; this.lastReportedVersion = this.currentVersion; - return { - projectName: this.getProjectFileName(), - addedFiles, - removedFiles, - version: this.currentVersion, - replacedFiles: [] - }; + return { info, changes: { added, removed } }; } else { // unknown version - return everything const projectFileNames = this.getFileNames(); - this.lastReportedFileNames = arrayToMap(projectFileNames, id); - return { - projectName: this.getProjectFileName(), - addedFiles: [], - removedFiles: [], - version: this.currentVersion, - replacedFiles: projectFileNames - }; + this.lastReportedFileNames = arrayToMap(projectFileNames, x => x); + return { info, files: projectFileNames }; } } } @@ -627,6 +601,11 @@ namespace ts.server { **/ openFileRoots: ScriptInfo[] = []; + /** + * maps external project file name to list of config files that were the part of this project + */ + externalProjectToConfiguredProjectMap: ts.Map = {}; + /** * external projects (configuration and list of root files is not controlled by tsserver) */ @@ -1078,12 +1057,11 @@ namespace ts.server { } private findConfiguredProjectByConfigFile(configFileName: string) { - for (const configuredProject of this.configuredProjects) { - if (configuredProject.configFileName === configFileName) { - return configuredProject; - } - } - return undefined; + return findVersionedProjectByFileName(configFileName, this.configuredProjects); + } + + private findExternalProjectByProjectFileName(projectFileName: string) { + return findVersionedProjectByFileName(projectFileName, this.externalProjects); } private configFileToProjectOptions(configFilename: string): { succeeded: boolean, projectOptions?: ProjectOptions, errors?: Diagnostic[] } { @@ -1284,13 +1262,6 @@ namespace ts.server { return info; } - openExternalProject(proj: protocol.ExternalProject) { - } - - closeExternalProject(proj: protocol.ExternalProject) { - // TODO: save mapping from external project name to set of configured projects - } - log(msg: string, type = "Err") { this.psLogger.msg(msg, type); } @@ -1475,11 +1446,70 @@ namespace ts.server { } } - loadExternalProject(externalProject: protocol.ExternalProject): Delta[] { + private addExternalProjectFilesForVersionedProjects(knownProjects: protocol.ExternalProjectInfo[], projects: VersionedProject[], result: protocol.ExternalProjectFiles[]): void { + for (const proj of projects) { + const knownProject = ts.forEach(knownProjects, p => p.projectFileName === proj.getProjectFileName() && p); + result.push(proj.getChangesSinceVersion(knownProjects && knownProject.version)); + } + } + + synchronizeProjectList(knownProjects: protocol.ExternalProjectInfo[]): protocol.ExternalProjectFiles[] { + const files: protocol.ExternalProjectFiles[] = []; + this.addExternalProjectFilesForVersionedProjects(knownProjects, this.externalProjects, files); + this.addExternalProjectFilesForVersionedProjects(knownProjects, this.configuredProjects, files); + for (const inferredProject of this.inferredProjects) { + files.push({ files: inferredProject.getFileNames() }); + } + return files; + } + + applyChangesInOpenFiles(openFiles: protocol.OpenFile[], closedFiles: string[]): void { + for (const file of openFiles) { + const scriptInfo = this.getScriptInfo(file.fileName); + if (!scriptInfo) { + Debug.assert(!!file.content); + this.openClientFile(file.fileName, file.content); + } + else { + Debug.assert(!!file.textChanges); + for (const change of file.textChanges) { + scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); + } + } + } + + for (const file of closedFiles) { + this.closeClientFile(file); + } + + this.updateProjectStructure(); + } + + closeExternalProject(fileName: string): void { + fileName = normalizePath(fileName); + const configFiles = this.externalProjectToConfiguredProjectMap[fileName]; + if (configFiles) { + for (const configFile of configFiles) { + const configuredProject = this.findConfiguredProjectByConfigFile(configFile); + if (configuredProject) { + this.removeProject(configuredProject); + } + } + } + else { + // close external project + const externalProject = this.findExternalProjectByProjectFileName(fileName); + if (externalProject) { + this.removeProject(externalProject); + } + } + this.updateProjectStructure(); + } + + openExternalProject(externalProject: protocol.ExternalProject): void { const project = this.findConfiguredProjectByConfigFile(externalProject.projectFileName); if (project) { this.updateConfiguredProjectWorker(project, externalProject.rootFiles, externalProject.options); - return [project.getDeltaFromVersion()]; } else { let tsConfigFiles: string[]; @@ -1493,32 +1523,18 @@ namespace ts.server { } } if (tsConfigFiles) { - const deltas: Delta[] = []; for (const tsconfigFile of tsConfigFiles) { const { success, project, errors } = this.openConfigFile(tsconfigFile); if (success) { // keep project alive project.addOpenRef(); - deltas.push(project.getDeltaFromVersion()); } } - return deltas; } else { - const { project, errors } = this.createAndAddExternalProject(externalProject.projectFileName, externalProject.rootFiles, externalProject.options); - return [project.getDeltaFromVersion()]; + this.createAndAddExternalProject(externalProject.projectFileName, externalProject.rootFiles, externalProject.options); } } } - - loadExternalProjects(externalProjects: protocol.ExternalProject[], openFiles: protocol.OpenFile[]): void { - for (const project of externalProjects) { - this.loadExternalProject(project); - } - for (const openFile of openFiles) { - this.getOrCreateScriptInfo(openFile.fileName, /*openedByClient*/ true, openFile.content); - } - // TODO: return diff - } } } diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 4702265e663..e04c033398b 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -424,9 +424,37 @@ declare namespace ts.server.protocol { options: CompilerOptions; } + export interface ExternalProjectInfo { + projectFileName: string; + version: number; + } + + export interface ExternalProjectChanges { + added: string[]; + removed: string[]; + } + + /** + * Describes set of files in the project. + * info might be omitted in case of inferred projects + * if files is set - then this is the entire set of files in the project + * if changes is set - then this is the set of changes that should be applied to existing project + * otherwise - assume that nothing is changed + */ + export interface ExternalProjectFiles { + info?: ExternalProjectInfo; + files?: string[]; + changes?: ExternalProjectChanges; + } + + /** + * Represents a set of changes for open document with a given file name. + * Either content of textChanges should be present. + */ export interface OpenFile { fileName: string; content?: string; + textChanges?: ts.TextChange[]; } /** @@ -548,14 +576,35 @@ declare namespace ts.server.protocol { arguments: OpenRequestArgs; } - type LoadExternalProjectArgs = ExternalProject; + type OpenExternalProjectArgs = ExternalProject; - export interface LoadExternalProject extends Request { - arguments: LoadExternalProjectArgs; + export interface OpenExternalProjectRequest extends Request { + arguments: OpenExternalProjectArgs; } - interface LoadExternalProjectResponse extends Response { - files: string[]; + export interface CloseExternalProjectRequestArgs { + projectFileName: string; + } + + export interface CloseExternalProjectRequest extends Request { + arguments: CloseExternalProjectRequestArgs; + } + + export interface SynchronizeProjectListRequest extends Request { + arguments: SynchronizeProjectListRequestArgs; + } + + export interface SynchronizeProjectListRequestArgs { + knownProjects: protocol.ExternalProjectInfo[]; + } + + export interface ApplyChangedToOpenFilesRequest extends Request { + arguments: ApplyChangedToOpenFilesRequestArgs; + } + + export interface ApplyChangedToOpenFilesRequestArgs { + openFiles: OpenFile[]; + closedFiles: string[]; } /** diff --git a/src/server/session.ts b/src/server/session.ts index c7f3a08dea0..a424d94f2db 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -127,7 +127,10 @@ namespace ts.server { export const ProjectInfo = "projectInfo"; export const ReloadProjects = "reloadProjects"; export const Unknown = "unknown"; - export const LoadExternalProject = "loadExternalProject"; + export const OpenExternalProject = "openExternalProject"; + export const CloseExternalProject = "closeExternalProject"; + export const SynchronizeProjectList = "synchronizeProjectList"; + export const ApplyChangedToOpenFiles = "applyChangedToOpenFiles"; } namespace Errors { @@ -268,7 +271,7 @@ namespace ts.server { } private updateProjectStructure(seq: number, matchSeq: (seq: number) => boolean, ms = 1500) { - setTimeout(() => { + this.host.setTimeout(() => { if (matchSeq(seq)) { this.projectService.updateProjectStructure(); } @@ -298,7 +301,7 @@ namespace ts.server { this.semanticCheck(checkSpec.fileName, checkSpec.project); this.immediateId = undefined; if (checkList.length > index) { - this.errorTimer = setTimeout(checkOne, followMs); + this.errorTimer = this.host.setTimeout(checkOne, followMs); } else { this.errorTimer = undefined; @@ -308,7 +311,7 @@ namespace ts.server { } }; if ((checkList.length > index) && (matchSeq(seq))) { - this.errorTimer = setTimeout(checkOne, ms); + this.errorTimer = this.host.setTimeout(checkOne, ms); } } @@ -1028,30 +1031,50 @@ namespace ts.server { exit() { } + private notRequired() { + return { responseRequired: false }; + } + + private requiredResponse(response: any) { + return { response, responseRequired: true }; + } + private handlers: Map<(request: protocol.Request) => { response?: any, responseRequired?: boolean }> = { - [CommandNames.LoadExternalProject]: (request: protocol.Request) => { - const deltas = this.projectService.loadExternalProject(request.arguments); - return { responseRequired: true, response: { files: deltas } }; + [CommandNames.OpenExternalProject]: (request: protocol.OpenExternalProjectRequest) => { + const deltas = this.projectService.openExternalProject(request.arguments); + return this.notRequired(); + }, + [CommandNames.CloseExternalProject]: (request: protocol.CloseExternalProjectRequest) => { + this.projectService.closeExternalProject(request.arguments.projectFileName); + return this.notRequired(); + }, + [CommandNames.SynchronizeProjectList]: (request: protocol.SynchronizeProjectListRequest) => { + const result = this.projectService.synchronizeProjectList(request.arguments.knownProjects); + return this.requiredResponse(result); + }, + [CommandNames.ApplyChangedToOpenFiles]: (request: protocol.ApplyChangedToOpenFilesRequest) => { + this.projectService.applyChangesInOpenFiles(request.arguments.openFiles, request.arguments.closedFiles); + return this.notRequired(); }, [CommandNames.Exit]: () => { this.exit(); - return { responseRequired: false }; + return this.notRequired(); }, [CommandNames.Definition]: (request: protocol.Request) => { const defArgs = request.arguments; - return { response: this.getDefinition(defArgs.line, defArgs.offset, defArgs.file), responseRequired: true }; + return this.requiredResponse(this.getDefinition(defArgs.line, defArgs.offset, defArgs.file)); }, [CommandNames.TypeDefinition]: (request: protocol.Request) => { const defArgs = request.arguments; - return { response: this.getTypeDefinition(defArgs.line, defArgs.offset, defArgs.file), responseRequired: true }; + return this.requiredResponse(this.getTypeDefinition(defArgs.line, defArgs.offset, defArgs.file)); }, [CommandNames.References]: (request: protocol.Request) => { const defArgs = request.arguments; - return { response: this.getReferences(defArgs.line, defArgs.offset, defArgs.file), responseRequired: true }; + return this.requiredResponse(this.getReferences(defArgs.line, defArgs.offset, defArgs.file)); }, [CommandNames.Rename]: (request: protocol.Request) => { const renameArgs = request.arguments; - return { response: this.getRenameLocations(renameArgs.line, renameArgs.offset, renameArgs.file, renameArgs.findInComments, renameArgs.findInStrings), responseRequired: true }; + return this.requiredResponse(this.getRenameLocations(renameArgs.line, renameArgs.offset, renameArgs.file, renameArgs.findInComments, renameArgs.findInStrings)); }, [CommandNames.Open]: (request: protocol.Request) => { const openArgs = request.arguments; @@ -1071,7 +1094,7 @@ namespace ts.server { break; } this.openClientFile(openArgs.file, openArgs.fileContent, scriptKind); - return { responseRequired: false }; + return this.notRequired(); }, [CommandNames.Quickinfo]: (request: protocol.Request) => { const quickinfoArgs = request.arguments; From 697661a4be3f1fe86490fe8074f59a95b4fad05f Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 7 Jun 2016 17:45:22 -0700 Subject: [PATCH 015/307] added openExternalProjects method --- src/server/protocol.d.ts | 8 ++++++++ src/server/session.ts | 9 ++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index e04c033398b..0ca5163ac17 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -586,6 +586,14 @@ declare namespace ts.server.protocol { projectFileName: string; } + export interface OpenExternalProjectsRequest extends Request { + arguments: OpenExternalProjectsArgs; + } + + export interface OpenExternalProjectsArgs { + projects: ExternalProject[]; + } + export interface CloseExternalProjectRequest extends Request { arguments: CloseExternalProjectRequestArgs; } diff --git a/src/server/session.ts b/src/server/session.ts index a424d94f2db..5823deac08b 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -128,6 +128,7 @@ namespace ts.server { export const ReloadProjects = "reloadProjects"; export const Unknown = "unknown"; export const OpenExternalProject = "openExternalProject"; + export const OpenExternalProjects = "openExternalProjects"; export const CloseExternalProject = "closeExternalProject"; export const SynchronizeProjectList = "synchronizeProjectList"; export const ApplyChangedToOpenFiles = "applyChangedToOpenFiles"; @@ -1041,7 +1042,13 @@ namespace ts.server { private handlers: Map<(request: protocol.Request) => { response?: any, responseRequired?: boolean }> = { [CommandNames.OpenExternalProject]: (request: protocol.OpenExternalProjectRequest) => { - const deltas = this.projectService.openExternalProject(request.arguments); + this.projectService.openExternalProject(request.arguments); + return this.notRequired(); + }, + [CommandNames.OpenExternalProjects]: (request: protocol.OpenExternalProjectsRequest) => { + for (const proj of request.arguments.projects) { + this.projectService.openExternalProject(proj); + } return this.notRequired(); }, [CommandNames.CloseExternalProject]: (request: protocol.CloseExternalProjectRequest) => { From 0cd40095a53bacc9113c8feac314a159dc7ece20 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 8 Jun 2016 13:21:59 -0700 Subject: [PATCH 016/307] WIP --- src/server/editorServices.ts | 48 +++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index c5c099ce161..f1fbd6a581f 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -851,6 +851,11 @@ namespace ts.server { } private addOpenFile(info: ScriptInfo) { + const externalProject = this.findContainingExternalProject(info.fileName); + if (externalProject) { + info.defaultProject = externalProject; + return; + } const configuredProject = this.findContainingConfiguredProject(info); if (configuredProject) { info.defaultProject = configuredProject; @@ -958,6 +963,16 @@ namespace ts.server { info.close(); } + private findContainingExternalProject(fileName: string): ExternalProject { + fileName = normalizePath(fileName); + for (const proj of this.externalProjects) { + if (proj.containsFile(fileName)) { + return proj; + } + } + return undefined; + } + /** * This function 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 @@ -1141,7 +1156,7 @@ namespace ts.server { } } - private updateConfiguredProjectWorker(project: ConfiguredProject, newRootFiles: string[], newOptions: CompilerOptions) { + private updateVersionedProjectWorker(project: VersionedProject, newRootFiles: string[], newOptions: CompilerOptions) { const oldRootFiles = project.getRootFiles(); const newFileNames = ts.filter(newRootFiles, f => this.host.fileExists(f)); const fileNamesToRemove = oldRootFiles.filter(f => !contains(newFileNames, f)); @@ -1172,7 +1187,9 @@ namespace ts.server { if (contains(this.openFilesReferenced, info)) { this.openFilesReferenced = copyListRemovingItem(info, this.openFilesReferenced); } - this.openFileRootsConfigured.push(info); + if (project.projectKind === ProjectKind.Configured) { + this.openFileRootsConfigured.push(info); + } info.defaultProject = project; } } @@ -1194,7 +1211,7 @@ namespace ts.server { return errors; } else { - this.updateConfiguredProjectWorker(project, projectOptions.files, projectOptions.compilerOptions); + this.updateVersionedProjectWorker(project, projectOptions.files, projectOptions.compilerOptions); } } } @@ -1419,7 +1436,11 @@ namespace ts.server { * @param fileContent is a known version of the file content that is more up to date than the one on disk */ openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind): { configFileName?: string, configFileErrors?: Diagnostic[] } { - const { configFileName, configFileErrors } = this.openOrUpdateConfiguredProjectForFile(fileName); + let configFileName: string; + let configFileErrors: Diagnostic[]; + if (!this.findContainingExternalProject(fileName)) { + ({ configFileName, configFileErrors } = this.openOrUpdateConfiguredProjectForFile(fileName)); + } const info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ true, fileContent, scriptKind); this.addOpenFile(info); this.printProjects(); @@ -1493,6 +1514,7 @@ namespace ts.server { const configuredProject = this.findConfiguredProjectByConfigFile(configFile); if (configuredProject) { this.removeProject(configuredProject); + this.updateProjectStructure(); } } } @@ -1501,20 +1523,20 @@ namespace ts.server { const externalProject = this.findExternalProjectByProjectFileName(fileName); if (externalProject) { this.removeProject(externalProject); + this.updateProjectStructure(); } } - this.updateProjectStructure(); } - openExternalProject(externalProject: protocol.ExternalProject): void { - const project = this.findConfiguredProjectByConfigFile(externalProject.projectFileName); - if (project) { - this.updateConfiguredProjectWorker(project, externalProject.rootFiles, externalProject.options); + openExternalProject(proj: protocol.ExternalProject): void { + const externalProject = this.findExternalProjectByProjectFileName(proj.projectFileName); + if (proj) { + this.updateVersionedProjectWorker(externalProject, proj.rootFiles, proj.options); } else { let tsConfigFiles: string[]; const rootFiles: string[] = []; - for (const file of externalProject.rootFiles) { + for (const file of proj.rootFiles) { if (getBaseFileName(file) === "tsconfig.json") { (tsConfigFiles || (tsConfigFiles = [])).push(file); } @@ -1523,16 +1545,18 @@ namespace ts.server { } } if (tsConfigFiles) { + // store the list of tsconfig files that belong to the external project + this.externalProjectToConfiguredProjectMap[proj.projectFileName] = tsConfigFiles; for (const tsconfigFile of tsConfigFiles) { const { success, project, errors } = this.openConfigFile(tsconfigFile); if (success) { - // keep project alive + // keep project alive - its lifetime is bound to the lifetime of containing external project project.addOpenRef(); } } } else { - this.createAndAddExternalProject(externalProject.projectFileName, externalProject.rootFiles, externalProject.options); + this.createAndAddExternalProject(proj.projectFileName, proj.rootFiles, proj.options); } } } From 38b5eed062297d0ce13d6ff740b1248367e0d328 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 8 Jun 2016 16:48:14 -0700 Subject: [PATCH 017/307] [WIP] fix typo, make response mandatory --- src/server/editorServices.ts | 2 +- src/server/session.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f1fbd6a581f..2817196c136 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1530,7 +1530,7 @@ namespace ts.server { openExternalProject(proj: protocol.ExternalProject): void { const externalProject = this.findExternalProjectByProjectFileName(proj.projectFileName); - if (proj) { + if (externalProject) { this.updateVersionedProjectWorker(externalProject, proj.rootFiles, proj.options); } else { diff --git a/src/server/session.ts b/src/server/session.ts index 2f9982a6a57..5c148fac25a 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1043,17 +1043,20 @@ namespace ts.server { private handlers: Map<(request: protocol.Request) => { response?: any, responseRequired?: boolean }> = { [CommandNames.OpenExternalProject]: (request: protocol.OpenExternalProjectRequest) => { this.projectService.openExternalProject(request.arguments); - return this.notRequired(); + // TODO: report errors + return this.requiredResponse(true); }, [CommandNames.OpenExternalProjects]: (request: protocol.OpenExternalProjectsRequest) => { for (const proj of request.arguments.projects) { this.projectService.openExternalProject(proj); } - return this.notRequired(); + // TODO: report errors + return this.requiredResponse(true); }, [CommandNames.CloseExternalProject]: (request: protocol.CloseExternalProjectRequest) => { this.projectService.closeExternalProject(request.arguments.projectFileName); - return this.notRequired(); + // TODO: report errors + return this.requiredResponse(true); }, [CommandNames.SynchronizeProjectList]: (request: protocol.SynchronizeProjectListRequest) => { const result = this.projectService.synchronizeProjectList(request.arguments.knownProjects); @@ -1061,7 +1064,8 @@ namespace ts.server { }, [CommandNames.ApplyChangedToOpenFiles]: (request: protocol.ApplyChangedToOpenFilesRequest) => { this.projectService.applyChangesInOpenFiles(request.arguments.openFiles, request.arguments.closedFiles); - return this.notRequired(); + // TODO: report errors + return this.requiredResponse(true); }, [CommandNames.Exit]: () => { this.exit(); From 8ff0febabeec88ab8f52d16a46381c08d6aed4b2 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 8 Jun 2016 18:40:40 -0700 Subject: [PATCH 018/307] adjust check when content should be used --- src/server/editorServices.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 2817196c136..75764e015d4 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1487,7 +1487,7 @@ namespace ts.server { applyChangesInOpenFiles(openFiles: protocol.OpenFile[], closedFiles: string[]): void { for (const file of openFiles) { const scriptInfo = this.getScriptInfo(file.fileName); - if (!scriptInfo) { + if (!scriptInfo || !scriptInfo.isOpen) { Debug.assert(!!file.content); this.openClientFile(file.fileName, file.content); } From e817faabfe68a4b94d82b810710937c9b1be1213 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 8 Jun 2016 19:15:56 -0700 Subject: [PATCH 019/307] fix typo --- src/server/editorServices.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 75764e015d4..37e5208f86a 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1470,7 +1470,7 @@ namespace ts.server { private addExternalProjectFilesForVersionedProjects(knownProjects: protocol.ExternalProjectInfo[], projects: VersionedProject[], result: protocol.ExternalProjectFiles[]): void { for (const proj of projects) { const knownProject = ts.forEach(knownProjects, p => p.projectFileName === proj.getProjectFileName() && p); - result.push(proj.getChangesSinceVersion(knownProjects && knownProject.version)); + result.push(proj.getChangesSinceVersion(knownProject && knownProject.version)); } } From b9729a79fc04ed74e88318c7f43ba10dc5171c99 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 9 Jun 2016 13:55:08 -0700 Subject: [PATCH 020/307] switch to use explicit list of open files --- src/server/editorServices.ts | 20 ++++++++++---------- src/server/protocol.d.ts | 13 +++++++++---- src/server/session.ts | 2 +- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 37e5208f86a..1447b621ff3 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1484,18 +1484,18 @@ namespace ts.server { return files; } - applyChangesInOpenFiles(openFiles: protocol.OpenFile[], closedFiles: string[]): void { + applyChangesInOpenFiles(openFiles: protocol.NewOpenFile[], changedFiles: protocol.ChangedOpenFile[], closedFiles: string[]): void { for (const file of openFiles) { const scriptInfo = this.getScriptInfo(file.fileName); - if (!scriptInfo || !scriptInfo.isOpen) { - Debug.assert(!!file.content); - this.openClientFile(file.fileName, file.content); - } - else { - Debug.assert(!!file.textChanges); - for (const change of file.textChanges) { - scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); - } + Debug.assert(!scriptInfo || !scriptInfo.isOpen); + this.openClientFile(file.fileName, file.content); + } + + for (const file of changedFiles) { + const scriptInfo = this.getScriptInfo(file.fileName); + Debug.assert(!!scriptInfo); + for (const change of file.changes) { + scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); } } diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 0ca5163ac17..c3f40cc5258 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -451,10 +451,14 @@ declare namespace ts.server.protocol { * Represents a set of changes for open document with a given file name. * Either content of textChanges should be present. */ - export interface OpenFile { + export interface NewOpenFile { fileName: string; - content?: string; - textChanges?: ts.TextChange[]; + content: string; + } + + export interface ChangedOpenFile { + fileName: string; + changes: ts.TextChange[]; } /** @@ -611,7 +615,8 @@ declare namespace ts.server.protocol { } export interface ApplyChangedToOpenFilesRequestArgs { - openFiles: OpenFile[]; + openFiles: NewOpenFile[]; + changedFiles: ChangedOpenFile[]; closedFiles: string[]; } diff --git a/src/server/session.ts b/src/server/session.ts index 5c148fac25a..936f7a71740 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1063,7 +1063,7 @@ namespace ts.server { return this.requiredResponse(result); }, [CommandNames.ApplyChangedToOpenFiles]: (request: protocol.ApplyChangedToOpenFilesRequest) => { - this.projectService.applyChangesInOpenFiles(request.arguments.openFiles, request.arguments.closedFiles); + this.projectService.applyChangesInOpenFiles(request.arguments.openFiles, request.arguments.changedFiles, request.arguments.closedFiles); // TODO: report errors return this.requiredResponse(true); }, From 81905cd29e0a0de2cb85a2f37eabf4e1f1cc08b1 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 10 Jun 2016 15:38:11 -0700 Subject: [PATCH 021/307] fix version related issues --- src/server/editorServices.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 1447b621ff3..f2e5eabb4e3 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -472,10 +472,10 @@ namespace ts.server { projectFileName: this.getProjectFileName(), version: this.currentVersion }; - if (this.lastReportedVersion === this.currentVersion) { - return { info }; - } if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) { + if (this.currentVersion == this.lastReportedVersion) { + return { info }; + } const lastReportedFileNames = this.lastReportedFileNames; const currentFiles = arrayToMap(this.getFileNames(), x => x); @@ -501,6 +501,7 @@ namespace ts.server { // unknown version - return everything const projectFileNames = this.getFileNames(); this.lastReportedFileNames = arrayToMap(projectFileNames, x => x); + this.lastReportedVersion = this.currentVersion; return { info, files: projectFileNames }; } } From c14398317a03aee46faa45f34dfb2a2973e7d96e Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 10 Jun 2016 16:56:15 -0700 Subject: [PATCH 022/307] WIP - quickinfo --- src/server/protocol.d.ts | 9 +++++++-- src/server/session.ts | 15 +++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index c3f40cc5258..923203c7f55 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -147,12 +147,17 @@ declare namespace ts.server.protocol { /** * The line number for the request (1-based). */ - line: number; + line?: number; /** * The character offset (on the line) for the request (1-based). */ - offset: number; + offset?: number; + + /** + * Position (can be specified instead of line/offset pair) + */ + position?: number; } /** diff --git a/src/server/session.ts b/src/server/session.ts index 936f7a71740..5119e536f72 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -608,19 +608,23 @@ namespace ts.server { } } - private getQuickInfo(line: number, offset: number, fileName: string): protocol.QuickInfoResponseBody { - const file = ts.normalizePath(fileName); + private getQuickInfo(args: protocol.FileLocationRequestArgs): protocol.QuickInfoResponseBody { + const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); if (!project) { throw Errors.NoProject; } const scriptInfo = project.getScriptInfo(file); - const position = scriptInfo.lineOffsetToPosition(line, offset); + const position = args.position !== undefined ? args.position : scriptInfo.lineOffsetToPosition(args.line, args.offset); const quickInfo = project.languageService.getQuickInfoAtPosition(file, position); if (!quickInfo) { return undefined; } + if (args.position !== undefined) { + // TODO: fixme + return quickInfo; + } const displayString = ts.displayPartsToString(quickInfo.displayParts); const docString = ts.displayPartsToString(quickInfo.documentation); @@ -1107,9 +1111,8 @@ namespace ts.server { this.openClientFile(openArgs.file, openArgs.fileContent, scriptKind); return this.notRequired(); }, - [CommandNames.Quickinfo]: (request: protocol.Request) => { - const quickinfoArgs = request.arguments; - return { response: this.getQuickInfo(quickinfoArgs.line, quickinfoArgs.offset, quickinfoArgs.file), responseRequired: true }; + [CommandNames.Quickinfo]: (request: protocol.QuickInfoRequest) => { + return this.requiredResponse(this.getQuickInfo(request.arguments)); }, [CommandNames.Format]: (request: protocol.Request) => { const formatArgs = request.arguments; From 9a1790e996b285bbfcd0675c6be1a975966bca5f Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 13 Jun 2016 10:54:45 -0700 Subject: [PATCH 023/307] added logging to stderr, add command for full quickinfo --- src/server/server.ts | 32 +++++++++++++++++++++++--------- src/server/session.ts | 37 +++++++++++++++++++++---------------- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index 767793024c2..efd01741f86 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -14,12 +14,14 @@ namespace ts.server { }); class Logger implements ts.server.Logger { - fd = -1; - seq = 0; - inGroup = false; - firstInGroup = true; + private fd = -1; + private seq = 0; + private inGroup = false; + private firstInGroup = true; - constructor(public logFilename: string, public level: string) { + constructor(private readonly logFilename: string, + private readonly traceToConsole: boolean, + private readonly level: string) { } static padStringRight(str: string, padding: string) { @@ -52,7 +54,7 @@ namespace ts.server { } loggingEnabled() { - return !!this.logFilename; + return !!this.logFilename || this.traceToConsole; } isVerbose() { @@ -66,7 +68,7 @@ namespace ts.server { this.fd = fs.openSync(this.logFilename, "w"); } } - if (this.fd >= 0) { + if (this.fd >= 0 || this.traceToConsole) { s = s + "\n"; const prefix = Logger.padStringRight(type + " " + this.seq.toString(), " "); if (this.firstInGroup) { @@ -78,7 +80,13 @@ namespace ts.server { this.firstInGroup = true; } const buf = new Buffer(s); - fs.writeSync(this.fd, buf, 0, buf.length, null); + if (this.fd >= 0) { + fs.writeSync(this.fd, buf, 0, buf.length, null); + } + if (this.traceToConsole) + { + console.warn(s); + } } } } @@ -109,6 +117,7 @@ namespace ts.server { interface LogOptions { file?: string; detailLevel?: string; + traceToConsole?: boolean; } function parseLoggingEnvironmentString(logEnvStr: string): LogOptions { @@ -125,6 +134,9 @@ namespace ts.server { case "-level": logEnv.detailLevel = value; break; + case "-traceToConsole": + logEnv.traceToConsole = value.toLowerCase() === "true"; + break; } } } @@ -135,6 +147,7 @@ namespace ts.server { function createLoggerFromEnv() { let fileName: string = undefined; let detailLevel = "normal"; + let traceToConsole = false; const logEnvStr = process.env["TSS_LOG"]; if (logEnvStr) { const logEnv = parseLoggingEnvironmentString(logEnvStr); @@ -147,8 +160,9 @@ namespace ts.server { if (logEnv.detailLevel) { detailLevel = logEnv.detailLevel; } + traceToConsole = logEnv.traceToConsole; } - return new Logger(fileName, detailLevel); + return new Logger(fileName, traceToConsole, detailLevel); } // This places log file in the directory containing editorServices.js // TODO: check that this location is writable diff --git a/src/server/session.ts b/src/server/session.ts index 5119e536f72..a20ef018a87 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -118,6 +118,7 @@ namespace ts.server { export const DocumentHighlights = "documentHighlights"; export const Open = "open"; export const Quickinfo = "quickinfo"; + export const QuickinfoFull = "quickinfo-full"; export const References = "references"; export const Reload = "reload"; export const Rename = "rename"; @@ -608,7 +609,7 @@ namespace ts.server { } } - private getQuickInfo(args: protocol.FileLocationRequestArgs): protocol.QuickInfoResponseBody { + private getQuickInfoWorker(args: protocol.FileLocationRequestArgs, simplified: boolean): protocol.QuickInfoResponseBody | QuickInfo { const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); if (!project) { @@ -621,21 +622,22 @@ namespace ts.server { if (!quickInfo) { return undefined; } - if (args.position !== undefined) { - // TODO: fixme - return quickInfo; - } - const displayString = ts.displayPartsToString(quickInfo.displayParts); - const docString = ts.displayPartsToString(quickInfo.documentation); - return { - kind: quickInfo.kind, - kindModifiers: quickInfo.kindModifiers, - start: scriptInfo.positionToLineOffset(quickInfo.textSpan.start), - end: scriptInfo.positionToLineOffset(ts.textSpanEnd(quickInfo.textSpan)), - displayString: displayString, - documentation: docString, - }; + if (simplified) { + const displayString = ts.displayPartsToString(quickInfo.displayParts); + const docString = ts.displayPartsToString(quickInfo.documentation); + return { + kind: quickInfo.kind, + kindModifiers: quickInfo.kindModifiers, + start: scriptInfo.positionToLineOffset(quickInfo.textSpan.start), + end: scriptInfo.positionToLineOffset(ts.textSpanEnd(quickInfo.textSpan)), + displayString: displayString, + documentation: docString, + }; + } + else { + return quickInfo; + } } private getFormattingEditsForRange(line: number, offset: number, endLine: number, endOffset: number, fileName: string): protocol.CodeEdit[] { @@ -1112,7 +1114,10 @@ namespace ts.server { return this.notRequired(); }, [CommandNames.Quickinfo]: (request: protocol.QuickInfoRequest) => { - return this.requiredResponse(this.getQuickInfo(request.arguments)); + return this.requiredResponse(this.getQuickInfoWorker(request.arguments, /*simplified*/ true)); + }, + [CommandNames.QuickinfoFull]: (request: protocol.QuickInfoRequest) => { + return this.requiredResponse(this.getQuickInfoWorker(request.arguments, /*simplified*/ false)); }, [CommandNames.Format]: (request: protocol.Request) => { const formatArgs = request.arguments; From 7b53afa0bfd9d290e2feeaeef5a609756adfa5a6 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 13 Jun 2016 13:49:29 -0700 Subject: [PATCH 024/307] [WIP] completions --- src/server/session.ts | 60 +++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index a20ef018a87..178da750ed1 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -104,6 +104,7 @@ namespace ts.server { export const Change = "change"; export const Close = "close"; export const Completions = "completions"; + export const CompletionsFull = "completions-full"; export const CompletionDetails = "completionEntryDetails"; export const Configure = "configure"; export const Definition = "definition"; @@ -609,6 +610,10 @@ namespace ts.server { } } + private getPosition(args: protocol.FileLocationRequestArgs, scriptInfo: ScriptInfo): number { + return args.position !== undefined ? args.position : scriptInfo.lineOffsetToPosition(args.line, args.offset); + } + private getQuickInfoWorker(args: protocol.FileLocationRequestArgs, simplified: boolean): protocol.QuickInfoResponseBody | QuickInfo { const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); @@ -617,8 +622,7 @@ namespace ts.server { } const scriptInfo = project.getScriptInfo(file); - const position = args.position !== undefined ? args.position : scriptInfo.lineOffsetToPosition(args.line, args.offset); - const quickInfo = project.languageService.getQuickInfoAtPosition(file, position); + const quickInfo = project.languageService.getQuickInfoAtPosition(file, this.getPosition(args, scriptInfo)); if (!quickInfo) { return undefined; } @@ -738,44 +742,46 @@ namespace ts.server { }); } - private getCompletions(line: number, offset: number, prefix: string, fileName: string): protocol.CompletionEntry[] { - if (!prefix) { - prefix = ""; - } - const file = ts.normalizePath(fileName); + private getCompletionsWorker(args: protocol.CompletionsRequestArgs, simplifiedResult: boolean): protocol.CompletionEntry[] | CompletionInfo { + const prefix = args.prefix || ""; + const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); if (!project) { throw Errors.NoProject; } const scriptInfo = project.getScriptInfo(file); - const position = scriptInfo.lineOffsetToPosition(line, offset); + const position = this.getPosition(args, scriptInfo) const completions = project.languageService.getCompletionsAtPosition(file, position); if (!completions) { return undefined; } + if (!simplifiedResult) { + return completions; + } + else { + return completions.entries.reduce((result: protocol.CompletionEntry[], entry: ts.CompletionEntry) => { + if (completions.isMemberCompletion || (entry.name.toLowerCase().indexOf(prefix.toLowerCase()) === 0)) { + result.push(entry); + } + return result; + }, []).sort((a, b) => a.name.localeCompare(b.name)); + } - return completions.entries.reduce((result: protocol.CompletionEntry[], entry: ts.CompletionEntry) => { - if (completions.isMemberCompletion || (entry.name.toLowerCase().indexOf(prefix.toLowerCase()) === 0)) { - result.push(entry); - } - return result; - }, []).sort((a, b) => a.name.localeCompare(b.name)); } - private getCompletionEntryDetails(line: number, offset: number, - entryNames: string[], fileName: string): protocol.CompletionEntryDetails[] { - const file = ts.normalizePath(fileName); + private getCompletionEntryDetails(args: protocol.CompletionDetailsRequestArgs): protocol.CompletionEntryDetails[] { + const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); if (!project) { throw Errors.NoProject; } const scriptInfo = project.getScriptInfo(file); - const position = scriptInfo.lineOffsetToPosition(line, offset); + const position = this.getPosition(args, scriptInfo); - return entryNames.reduce((accum: protocol.CompletionEntryDetails[], entryName: string) => { + return args.entryNames.reduce((accum: protocol.CompletionEntryDetails[], entryName: string) => { const details = project.languageService.getCompletionEntryDetails(file, position, entryName); if (details) { accum.push(details); @@ -1127,16 +1133,14 @@ namespace ts.server { const formatOnKeyArgs = request.arguments; return { response: this.getFormattingEditsAfterKeystroke(formatOnKeyArgs.line, formatOnKeyArgs.offset, formatOnKeyArgs.key, formatOnKeyArgs.file), responseRequired: true }; }, - [CommandNames.Completions]: (request: protocol.Request) => { - const completionsArgs = request.arguments; - return { response: this.getCompletions(completionsArgs.line, completionsArgs.offset, completionsArgs.prefix, completionsArgs.file), responseRequired: true }; + [CommandNames.Completions]: (request: protocol.CompletionDetailsRequest) => { + return this.requiredResponse(this.getCompletionsWorker(request.arguments, /*simplifiedResult*/ true)); }, - [CommandNames.CompletionDetails]: (request: protocol.Request) => { - const completionDetailsArgs = request.arguments; - return { - response: this.getCompletionEntryDetails(completionDetailsArgs.line, completionDetailsArgs.offset, - completionDetailsArgs.entryNames, completionDetailsArgs.file), responseRequired: true - }; + [CommandNames.CompletionsFull]: (request: protocol.CompletionDetailsRequest) => { + return this.requiredResponse(this.getCompletionsWorker(request.arguments, /*simplifiedResult*/ false)); + }, + [CommandNames.CompletionDetails]: (request: protocol.CompletionDetailsRequest) => { + return this.requiredResponse(this.getCompletionEntryDetails(request.arguments)) }, [CommandNames.SignatureHelp]: (request: protocol.Request) => { const signatureHelpArgs = request.arguments; From 23c6fac592e055ecf564750e7ced48eccaa84c3e Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 13 Jun 2016 15:02:54 -0700 Subject: [PATCH 025/307] added sighelp --- src/server/protocol.d.ts | 2 ++ src/server/session.ts | 61 ++++++++++++++++++++++------------------ 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 923203c7f55..833b0064a1a 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -1187,6 +1187,8 @@ declare namespace ts.server.protocol { * Optional limit on the number of items to return. */ maxResultCount?: number; + + projectFileName?: string; } /** diff --git a/src/server/session.ts b/src/server/session.ts index 178da750ed1..6b0dc2f22e9 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -125,6 +125,7 @@ namespace ts.server { export const Rename = "rename"; export const Saveto = "saveto"; export const SignatureHelp = "signatureHelp"; + export const SignatureHelpFull = "signatureHelp-full"; export const TypeDefinition = "typeDefinition"; export const ProjectInfo = "projectInfo"; export const ReloadProjects = "reloadProjects"; @@ -614,7 +615,7 @@ namespace ts.server { return args.position !== undefined ? args.position : scriptInfo.lineOffsetToPosition(args.line, args.offset); } - private getQuickInfoWorker(args: protocol.FileLocationRequestArgs, simplified: boolean): protocol.QuickInfoResponseBody | QuickInfo { + private getQuickInfoWorker(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.QuickInfoResponseBody | QuickInfo { const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); if (!project) { @@ -627,7 +628,7 @@ namespace ts.server { return undefined; } - if (simplified) { + if (simplifiedResult) { const displayString = ts.displayPartsToString(quickInfo.displayParts); const docString = ts.displayPartsToString(quickInfo.documentation); return { @@ -757,10 +758,7 @@ namespace ts.server { if (!completions) { return undefined; } - if (!simplifiedResult) { - return completions; - } - else { + if (simplifiedResult) { return completions.entries.reduce((result: protocol.CompletionEntry[], entry: ts.CompletionEntry) => { if (completions.isMemberCompletion || (entry.name.toLowerCase().indexOf(prefix.toLowerCase()) === 0)) { result.push(entry); @@ -768,7 +766,9 @@ namespace ts.server { return result; }, []).sort((a, b) => a.name.localeCompare(b.name)); } - + else { + return completions; + } } private getCompletionEntryDetails(args: protocol.CompletionDetailsRequestArgs): protocol.CompletionEntryDetails[] { @@ -790,33 +790,36 @@ namespace ts.server { }, []); } - private getSignatureHelpItems(line: number, offset: number, fileName: string): protocol.SignatureHelpItems { - const file = ts.normalizePath(fileName); + private getSignatureHelpItems(args: protocol.SignatureHelpRequestArgs, simplifiedResult: boolean): protocol.SignatureHelpItems | SignatureHelpItems { + const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); if (!project) { throw Errors.NoProject; } const scriptInfo = project.getScriptInfo(file); - const position = scriptInfo.lineOffsetToPosition(line, offset); + const position = this.getPosition(args, scriptInfo); const helpItems = project.languageService.getSignatureHelpItems(file, position); if (!helpItems) { return undefined; } - const span = helpItems.applicableSpan; - const result: protocol.SignatureHelpItems = { - items: helpItems.items, - applicableSpan: { - start: scriptInfo.positionToLineOffset(span.start), - end: scriptInfo.positionToLineOffset(span.start + span.length) - }, - selectedItemIndex: helpItems.selectedItemIndex, - argumentIndex: helpItems.argumentIndex, - argumentCount: helpItems.argumentCount, - }; - - return result; + if (simplifiedResult) { + const span = helpItems.applicableSpan; + return { + items: helpItems.items, + applicableSpan: { + start: scriptInfo.positionToLineOffset(span.start), + end: scriptInfo.positionToLineOffset(span.start + span.length) + }, + selectedItemIndex: helpItems.selectedItemIndex, + argumentIndex: helpItems.argumentIndex, + argumentCount: helpItems.argumentCount, + }; + } + else { + return helpItems; + } } private getDiagnostics(delay: number, fileNames: string[]) { @@ -1120,10 +1123,10 @@ namespace ts.server { return this.notRequired(); }, [CommandNames.Quickinfo]: (request: protocol.QuickInfoRequest) => { - return this.requiredResponse(this.getQuickInfoWorker(request.arguments, /*simplified*/ true)); + return this.requiredResponse(this.getQuickInfoWorker(request.arguments, /*simplifiedResult*/ true)); }, [CommandNames.QuickinfoFull]: (request: protocol.QuickInfoRequest) => { - return this.requiredResponse(this.getQuickInfoWorker(request.arguments, /*simplified*/ false)); + return this.requiredResponse(this.getQuickInfoWorker(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.Format]: (request: protocol.Request) => { const formatArgs = request.arguments; @@ -1142,9 +1145,11 @@ namespace ts.server { [CommandNames.CompletionDetails]: (request: protocol.CompletionDetailsRequest) => { return this.requiredResponse(this.getCompletionEntryDetails(request.arguments)) }, - [CommandNames.SignatureHelp]: (request: protocol.Request) => { - const signatureHelpArgs = request.arguments; - return { response: this.getSignatureHelpItems(signatureHelpArgs.line, signatureHelpArgs.offset, signatureHelpArgs.file), responseRequired: true }; + [CommandNames.SignatureHelp]: (request: protocol.SignatureHelpRequest) => { + return this.requiredResponse(this.getSignatureHelpItems(request.arguments, /*simplifiedResult*/ true)); + }, + [CommandNames.SignatureHelpFull]: (request: protocol.SignatureHelpRequest) => { + return this.requiredResponse(this.getSignatureHelpItems(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.Geterr]: (request: protocol.Request) => { const geterrArgs = request.arguments; From 70aacef81887fb0ad3aa4077d17c6c765dda3106 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 13 Jun 2016 16:18:16 -0700 Subject: [PATCH 026/307] added gotodef --- src/server/session.ts | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 6b0dc2f22e9..8fcea1810a9 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -108,6 +108,7 @@ namespace ts.server { export const CompletionDetails = "completionEntryDetails"; export const Configure = "configure"; export const Definition = "definition"; + export const DefinitionFull = "definition-full"; export const Exit = "exit"; export const Format = "format"; export const Formatonkey = "formatonkey"; @@ -319,29 +320,34 @@ namespace ts.server { } } - private getDefinition(line: number, offset: number, fileName: string): protocol.FileSpan[] { - const file = ts.normalizePath(fileName); + private getDefinition(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.FileSpan[] | DefinitionInfo[] { + const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); if (!project) { throw Errors.NoProject; } const scriptInfo = project.getScriptInfo(file); - const position = scriptInfo.lineOffsetToPosition(line, offset); + const position = this.getPosition(args, scriptInfo);; const definitions = project.languageService.getDefinitionAtPosition(file, position); if (!definitions) { return undefined; } - return definitions.map(def => { - const defScriptInfo = project.getScriptInfo(def.fileName); - return { - file: def.fileName, - start: defScriptInfo.positionToLineOffset(def.textSpan.start), - end: defScriptInfo.positionToLineOffset(ts.textSpanEnd(def.textSpan)) - }; - }); + if (simplifiedResult) { + return definitions.map(def => { + const defScriptInfo = project.getScriptInfo(def.fileName); + return { + file: def.fileName, + start: defScriptInfo.positionToLineOffset(def.textSpan.start), + end: defScriptInfo.positionToLineOffset(ts.textSpanEnd(def.textSpan)) + }; + }); + } + else { + return definitions; + } } private getTypeDefinition(line: number, offset: number, fileName: string): protocol.FileSpan[] { @@ -1086,9 +1092,11 @@ namespace ts.server { this.exit(); return this.notRequired(); }, - [CommandNames.Definition]: (request: protocol.Request) => { - const defArgs = request.arguments; - return this.requiredResponse(this.getDefinition(defArgs.line, defArgs.offset, defArgs.file)); + [CommandNames.Definition]: (request: protocol.DefinitionRequest) => { + return this.requiredResponse(this.getDefinition(request.arguments, /*simplifiedResult*/ true)); + }, + [CommandNames.DefinitionFull]: (request: protocol.DefinitionRequest) => { + return this.requiredResponse(this.getDefinition(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.TypeDefinition]: (request: protocol.Request) => { const defArgs = request.arguments; From bd646d114222ee79e3b878debd76001016f0c0da Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 13 Jun 2016 16:56:26 -0700 Subject: [PATCH 027/307] enable document highlighting" --- src/server/session.ts | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 8fcea1810a9..b5616ae2bf9 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -118,6 +118,7 @@ namespace ts.server { export const Navto = "navto"; export const Occurrences = "occurrences"; export const DocumentHighlights = "documentHighlights"; + export const DocumentHighlightsFull = "documentHighlights-full"; export const Open = "open"; export const Quickinfo = "quickinfo"; export const QuickinfoFull = "quickinfo-full"; @@ -406,8 +407,8 @@ namespace ts.server { }); } - private getDocumentHighlights(line: number, offset: number, fileName: string, filesToSearch: string[]): protocol.DocumentHighlightsItem[] { - fileName = ts.normalizePath(fileName); + private getDocumentHighlights(args: protocol.DocumentHighlightsRequestArgs, simplifiedResult: boolean): protocol.DocumentHighlightsItem[] | DocumentHighlights[] { + const fileName = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(fileName); if (!project) { @@ -415,15 +416,20 @@ namespace ts.server { } const scriptInfo = project.getScriptInfo(fileName); - const position = scriptInfo.lineOffsetToPosition(line, offset); + const position = this.getPosition(args, scriptInfo); - const documentHighlights = project.languageService.getDocumentHighlights(fileName, position, filesToSearch); + const documentHighlights = project.languageService.getDocumentHighlights(fileName, position, args.filesToSearch); if (!documentHighlights) { return undefined; } - return documentHighlights.map(convertToDocumentHighlightsItem); + if (simplifiedResult) { + return documentHighlights.map(convertToDocumentHighlightsItem); + } + else { + return documentHighlights; + } function convertToDocumentHighlightsItem(documentHighlights: ts.DocumentHighlights): ts.server.protocol.DocumentHighlightsItem { const { fileName, highlightSpans } = documentHighlights; @@ -749,7 +755,7 @@ namespace ts.server { }); } - private getCompletionsWorker(args: protocol.CompletionsRequestArgs, simplifiedResult: boolean): protocol.CompletionEntry[] | CompletionInfo { + private getCompletions(args: protocol.CompletionsRequestArgs, simplifiedResult: boolean): protocol.CompletionEntry[] | CompletionInfo { const prefix = args.prefix || ""; const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); @@ -1145,10 +1151,10 @@ namespace ts.server { return { response: this.getFormattingEditsAfterKeystroke(formatOnKeyArgs.line, formatOnKeyArgs.offset, formatOnKeyArgs.key, formatOnKeyArgs.file), responseRequired: true }; }, [CommandNames.Completions]: (request: protocol.CompletionDetailsRequest) => { - return this.requiredResponse(this.getCompletionsWorker(request.arguments, /*simplifiedResult*/ true)); + return this.requiredResponse(this.getCompletions(request.arguments, /*simplifiedResult*/ true)); }, [CommandNames.CompletionsFull]: (request: protocol.CompletionDetailsRequest) => { - return this.requiredResponse(this.getCompletionsWorker(request.arguments, /*simplifiedResult*/ false)); + return this.requiredResponse(this.getCompletions(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.CompletionDetails]: (request: protocol.CompletionDetailsRequest) => { return this.requiredResponse(this.getCompletionEntryDetails(request.arguments)) @@ -1210,9 +1216,11 @@ namespace ts.server { const { line, offset, file: fileName } = request.arguments; return { response: this.getOccurrences(line, offset, fileName), responseRequired: true }; }, - [CommandNames.DocumentHighlights]: (request: protocol.Request) => { - const { line, offset, file: fileName, filesToSearch } = request.arguments; - return { response: this.getDocumentHighlights(line, offset, fileName, filesToSearch), responseRequired: true }; + [CommandNames.DocumentHighlights]: (request: protocol.DocumentHighlightsRequest) => { + return this.requiredResponse(this.getDocumentHighlights(request.arguments, /*simplifiedResult*/ true)); + }, + [CommandNames.DocumentHighlightsFull]: (request: protocol.DocumentHighlightsRequest) => { + return this.requiredResponse(this.getDocumentHighlights(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.ProjectInfo]: (request: protocol.Request) => { const { file, needFileNameList } = request.arguments; From adb726643a9837bc631778083759f2c3b06613b2 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 14 Jun 2016 17:30:55 -0700 Subject: [PATCH 028/307] add cancellation support --- Jakefile.js | 11 ++++++++++- src/server/cancellationToken.ts | 34 +++++++++++++++++++++++++++++++++ src/server/editorServices.ts | 13 ++++++++++--- src/server/server.ts | 20 ++++++++++++++++--- src/server/session.ts | 12 +++++++++--- 5 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 src/server/cancellationToken.ts diff --git a/Jakefile.js b/Jakefile.js index bf20bf29275..2c3f9f3a2fd 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -110,6 +110,12 @@ var serverCoreSources = [ return path.join(serverDirectory, f); }); +var cancellationTokenSources = [ + "cancellationToken.ts" +].map(function (f) { + return path.join(serverDirectory, f); +}); + var serverSources = serverCoreSources.concat(servicesSources); var languageServiceLibrarySources = [ @@ -517,8 +523,11 @@ compileFile(servicesFile, servicesSources,[builtLocalDirectory, copyright].conca }); +var cancellationTokenFile = path.join(builtLocalDirectory, "cancellationToken.js"); +compileFile(cancellationTokenFile, cancellationTokenSources, [builtLocalDirectory].concat(cancellationTokenSources), /*prefixes*/ [copyright], /*useBuiltCompiler*/ true, { outDir: builtLocalDirectory, noOutFile: true }); + var serverFile = path.join(builtLocalDirectory, "tsserver.js"); -compileFile(serverFile, serverSources,[builtLocalDirectory, copyright].concat(serverSources), /*prefixes*/ [copyright], /*useBuiltCompiler*/ true); +compileFile(serverFile, serverSources,[builtLocalDirectory, copyright, cancellationTokenFile].concat(serverSources), /*prefixes*/ [copyright], /*useBuiltCompiler*/ true); var tsserverLibraryFile = path.join(builtLocalDirectory, "tsserverlibrary.js"); var tsserverLibraryDefinitionFile = path.join(builtLocalDirectory, "tsserverlibrary.d.ts"); diff --git a/src/server/cancellationToken.ts b/src/server/cancellationToken.ts new file mode 100644 index 00000000000..ac5adeab6d7 --- /dev/null +++ b/src/server/cancellationToken.ts @@ -0,0 +1,34 @@ +/// + + +// TODO: extract services types +interface HostCancellationToken { + isCancellationRequested(): boolean; +} + +const fs: typeof NodeJS.fs = require("fs"); + +function createCancellationToken(args: string[]): HostCancellationToken { + let cancellationPipeName: string; + for (let i = 0; i < args.length - 1; i++) { + if (args[i] === "--cancellationPipeName") { + cancellationPipeName = args[i + 1]; + break; + } + } + if (!cancellationPipeName) { + return { isCancellationRequested: () => false }; + } + return { + isCancellationRequested() { + try { + fs.statSync(cancellationPipeName); + return true; + } + catch(e) { + return false; + } + } + }; +} +export = createCancellationToken \ No newline at end of file diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f2e5eabb4e3..87ae34ca239 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -129,7 +129,7 @@ namespace ts.server { private resolvedTypeReferenceDirectives: ts.FileMap>; private getCanonicalFileName: (fileName: string) => string; - constructor(private host: ServerHost, private project: Project) { + constructor(private host: ServerHost, private project: Project, private cancellationToken: HostCancellationToken) { this.getCanonicalFileName = ts.createGetCanonicalFileName(this.host.useCaseSensitiveFileNames); this.resolvedModuleNames = createFileMap>(); this.resolvedTypeReferenceDirectives = createFileMap>(); @@ -188,6 +188,10 @@ namespace ts.server { } } + getCancellationToken() { + return this.cancellationToken; + } + resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { return this.resolveNamesWithLocalCache(typeDirectiveNames, containingFile, this.resolvedTypeReferenceDirectives, resolveTypeReferenceDirective, m => m.resolvedTypeReferenceDirective); } @@ -316,7 +320,7 @@ namespace ts.server { compilerOptions.allowNonTsExtensions = true; } - this.lsHost = new LSHost(this.projectService.host, this); + this.lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken); this.lsHost.setCompilationSettings(compilerOptions); this.languageService = ts.createLanguageService(this.lsHost, documentRegistry); } @@ -642,7 +646,10 @@ namespace ts.server { private documentRegistry: ts.DocumentRegistry; - constructor(public host: ServerHost, public psLogger: Logger, public eventHandler?: ProjectServiceEventHandler) { + constructor(public host: ServerHost, + public psLogger: Logger, + public cancellationToken: HostCancellationToken, + public eventHandler?: ProjectServiceEventHandler) { // ts.disableIncrementalParsing = true; this.setDefaultHostConfiguration(); this.documentRegistry = ts.createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); diff --git a/src/server/server.ts b/src/server/server.ts index efd01741f86..da1996d341f 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -92,8 +92,8 @@ namespace ts.server { } class IOSession extends Session { - constructor(host: ServerHost, logger: ts.server.Logger) { - super(host, Buffer.byteLength, process.hrtime, logger); + constructor(host: ServerHost, cancellationToken: HostCancellationToken, logger: ts.server.Logger) { + super(host, cancellationToken, Buffer.byteLength, process.hrtime, logger); } exit() { @@ -294,7 +294,21 @@ namespace ts.server { sys.setTimeout = setTimeout; sys.clearTimeout = clearTimeout; - const ioSession = new IOSession(sys, logger); + let cancellationToken: HostCancellationToken; + try { + let cancellationPipeName: string; + if (cancellationPipeName) { + const factory = require("./cancellationToken"); + cancellationToken = factory(sys.args); + } + } + catch(e) { + cancellationToken = { + isCancellationRequested: () => false + } + } + + const ioSession = new IOSession(sys, cancellationToken, logger); process.on("uncaughtException", function(err: Error) { ioSession.logError(err, "unknown"); }); diff --git a/src/server/session.ts b/src/server/session.ts index b5616ae2bf9..86d8dc0734f 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -156,12 +156,12 @@ namespace ts.server { constructor( private host: ServerHost, + private cancellationToken: HostCancellationToken, private byteLength: (buf: string, encoding?: string) => number, private hrtime: (start?: number[]) => number[], - private logger: Logger - ) { + private logger: Logger) { this.projectService = - new ProjectService(host, logger, (eventName, project, fileName) => { + new ProjectService(host, logger, cancellationToken, (eventName, project, fileName) => { this.handleEvent(eventName, project, fileName); }); } @@ -1067,6 +1067,10 @@ namespace ts.server { return { response, responseRequired: true }; } + private canceledResponse() { + return { canceled: true, responseRequired: true }; + } + private handlers: Map<(request: protocol.Request) => { response?: any, responseRequired?: boolean }> = { [CommandNames.OpenExternalProject]: (request: protocol.OpenExternalProjectRequest) => { this.projectService.openExternalProject(request.arguments); @@ -1282,6 +1286,8 @@ namespace ts.server { catch (err) { if (err instanceof OperationCanceledException) { // Handle cancellation exceptions + this.output({ canceled: true }, request.command, request.seq); + return; } this.logError(err, message); this.output( From c3b16458681038d29fab1ac0b7defbc3129a1d31 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 14 Jun 2016 17:51:19 -0700 Subject: [PATCH 029/307] fix tests issues --- src/harness/harnessLanguageService.ts | 1 + src/server/server.ts | 16 +++++--------- src/server/session.ts | 6 ++--- .../cases/unittests/cachingInServerLSHost.ts | 2 +- tests/cases/unittests/session.ts | 7 +++--- .../cases/unittests/tsserverProjectSystem.ts | 22 +++++++++++-------- 6 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index b3478e40609..05d718340bd 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -669,6 +669,7 @@ namespace Harness.LanguageService { // host to answer server queries about files on disk const serverHost = new SessionServerHost(clientHost); const server = new ts.server.Session(serverHost, + { isCancellationRequested: () => false }, Buffer ? Buffer.byteLength : (string: string, encoding?: string) => string.length, process.hrtime, serverHost); diff --git a/src/server/server.ts b/src/server/server.ts index da1996d341f..98762982d3f 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -83,8 +83,7 @@ namespace ts.server { if (this.fd >= 0) { fs.writeSync(this.fd, buf, 0, buf.length, null); } - if (this.traceToConsole) - { + if (this.traceToConsole) { console.warn(s); } } @@ -296,17 +295,14 @@ namespace ts.server { let cancellationToken: HostCancellationToken; try { - let cancellationPipeName: string; - if (cancellationPipeName) { - const factory = require("./cancellationToken"); - cancellationToken = factory(sys.args); - } + const factory = require("./cancellationToken"); + cancellationToken = factory(sys.args); } - catch(e) { + catch (e) { cancellationToken = { isCancellationRequested: () => false - } - } + }; + }; const ioSession = new IOSession(sys, cancellationToken, logger); process.on("uncaughtException", function(err: Error) { diff --git a/src/server/session.ts b/src/server/session.ts index 86d8dc0734f..9ee676bb06a 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -329,7 +329,7 @@ namespace ts.server { } const scriptInfo = project.getScriptInfo(file); - const position = this.getPosition(args, scriptInfo);; + const position = this.getPosition(args, scriptInfo); const definitions = project.languageService.getDefinitionAtPosition(file, position); if (!definitions) { @@ -764,7 +764,7 @@ namespace ts.server { } const scriptInfo = project.getScriptInfo(file); - const position = this.getPosition(args, scriptInfo) + const position = this.getPosition(args, scriptInfo); const completions = project.languageService.getCompletionsAtPosition(file, position); if (!completions) { @@ -1161,7 +1161,7 @@ namespace ts.server { return this.requiredResponse(this.getCompletions(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.CompletionDetails]: (request: protocol.CompletionDetailsRequest) => { - return this.requiredResponse(this.getCompletionEntryDetails(request.arguments)) + return this.requiredResponse(this.getCompletionEntryDetails(request.arguments)); }, [CommandNames.SignatureHelp]: (request: protocol.SignatureHelpRequest) => { return this.requiredResponse(this.getSignatureHelpItems(request.arguments, /*simplifiedResult*/ true)); diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index 31bd89955c7..b14b9a74350 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -79,7 +79,7 @@ namespace ts { msg: (s: string, type?: string) => { } }; - const projectService = new server.ProjectService(serverHost, logger); + const projectService = new server.ProjectService(serverHost, logger, { isCancellationRequested: () => false }); const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */true); const project = projectService.createAndAddInferredProject(rootScriptInfo); project.setCompilerOptions({ module: ts.ModuleKind.AMD } ); diff --git a/tests/cases/unittests/session.ts b/tests/cases/unittests/session.ts index a2edb05b39b..6a5fa33d29c 100644 --- a/tests/cases/unittests/session.ts +++ b/tests/cases/unittests/session.ts @@ -23,6 +23,7 @@ namespace ts.server { setTimeout(callback, ms, ...args) { return 0; }, clearTimeout(timeoutId) { } }; + const nullCancellationToken: HostCancellationToken = { isCancellationRequested: () => false }; const mockLogger: Logger = { close(): void {}, isVerbose(): boolean { return false; }, @@ -39,7 +40,7 @@ namespace ts.server { let lastSent: protocol.Message; beforeEach(() => { - session = new Session(mockHost, Utils.byteLength, process.hrtime, mockLogger); + session = new Session(mockHost, nullCancellationToken, Utils.byteLength, process.hrtime, mockLogger); session.send = (msg: protocol.Message) => { lastSent = msg; }; @@ -264,7 +265,7 @@ namespace ts.server { lastSent: protocol.Message; customHandler = "testhandler"; constructor() { - super(mockHost, Utils.byteLength, process.hrtime, mockLogger); + super(mockHost, nullCancellationToken, Utils.byteLength, process.hrtime, mockLogger); this.addProtocolHandler(this.customHandler, () => { return {response: undefined, responseRequired: true}; }); @@ -322,7 +323,7 @@ namespace ts.server { class InProcSession extends Session { private queue: protocol.Request[] = []; constructor(private client: InProcClient) { - super(mockHost, Utils.byteLength, process.hrtime, mockLogger); + super(mockHost, nullCancellationToken, Utils.byteLength, process.hrtime, mockLogger); this.addProtocolHandler("echo", (req: protocol.Request) => ({ response: req.arguments, responseRequired: true diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index cf382d09463..9a1c69a0171 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -16,6 +16,10 @@ namespace ts { msg: () => void 0 }; + const nullCancellationToken: HostCancellationToken = { + isCancellationRequested: () => false + }; + const { content: libFileContent } = Harness.getDefaultLibraryFile(Harness.IO); function getExecutingFilePathFromLibFile(libFile: FileOrFolder): string { @@ -310,7 +314,7 @@ namespace ts { content: `export let x: number` }; const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [appFile, moduleFile, libFile]); - const projectService = new server.ProjectService(host, nullLogger); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); const { configFileName } = projectService.openClientFile(appFile.path); assert(!configFileName, `should not find config, got: '${configFileName}`); @@ -348,7 +352,7 @@ namespace ts { }; const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [ configFile, libFile, file1, file2, file3 ]); - const projectService = new server.ProjectService(host, nullLogger); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); assert(configFileName, "should find config file"); @@ -374,7 +378,7 @@ namespace ts { const filesWithoutConfig = [ libFile, commonFile1, commonFile2 ]; const filesWithConfig = [ libFile, commonFile1, commonFile2, configFile ]; const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", filesWithoutConfig); - const projectService = new server.ProjectService(host, nullLogger); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); projectService.openClientFile(commonFile1.path); projectService.openClientFile(commonFile2.path); @@ -405,7 +409,7 @@ namespace ts { content: `{}` }; const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, libFile, configFile]); - const projectService = new server.ProjectService(host, nullLogger); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); projectService.openClientFile(commonFile1.path); checkWatchedDirectories(host, ["/a/b"]); checkNumberOfConfiguredProjects(projectService, 1); @@ -433,7 +437,7 @@ namespace ts { }` }; const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, commonFile2, configFile]); - const projectService = new server.ProjectService(host, nullLogger); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); projectService.openClientFile(commonFile1.path); projectService.openClientFile(commonFile2.path); @@ -449,7 +453,7 @@ namespace ts { content: `{}` }; const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, commonFile2, configFile]); - const projectService = new server.ProjectService(host, nullLogger); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); projectService.openClientFile(commonFile1.path); checkNumberOfConfiguredProjects(projectService, 1); @@ -479,7 +483,7 @@ namespace ts { }; const files = [commonFile1, commonFile2, configFile]; const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", files); - const projectService = new server.ProjectService(host, nullLogger); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); projectService.openClientFile(commonFile1.path); const project = projectService.configuredProjects[0]; @@ -512,7 +516,7 @@ namespace ts { }; const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, commonFile2, excludedFile1, configFile]); - const projectService = new server.ProjectService(host, nullLogger); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); projectService.openClientFile(commonFile1.path); checkNumberOfConfiguredProjects(projectService, 1); @@ -546,7 +550,7 @@ namespace ts { }; const files = [file1, nodeModuleFile, classicModuleFile, configFile]; const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", files); - const projectService = new server.ProjectService(host, nullLogger); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); projectService.openClientFile(file1.path); projectService.openClientFile(nodeModuleFile.path); projectService.openClientFile(classicModuleFile.path); From e8c5a591a48f6c42fbb040bfc0d0fd1dd8126239 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 15 Jun 2016 13:05:55 -0700 Subject: [PATCH 030/307] getSemanticDiagnostics --- src/server/editorServices.ts | 9 +++++++++ src/server/protocol.d.ts | 15 +++++++++++++++ src/server/session.ts | 27 +++++++++++++++++++++++++++ src/services/services.ts | 16 ++++++++++++++++ src/services/shims.ts | 15 --------------- 5 files changed, 67 insertions(+), 15 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 22da5536eff..0e01c4c5baa 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -676,6 +676,15 @@ namespace ts.server { } } + getProject(projectFileName: string): Project { + // TODO: fixme + if (!projectFileName) { + // TODO: fixme + return this.inferredProjects.length ? this.inferredProjects[0] : undefined; + } + return this.findExternalProjectByProjectFileName(projectFileName) || this.findConfiguredProjectByConfigFile(projectFileName); + } + getFormatCodeOptions(file?: string) { if (file) { const info = this.filenameToScriptInfo[file]; diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 6f2adc6d7d4..ef66b0119d4 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -91,6 +91,11 @@ declare namespace ts.server.protocol { * The file for the request (absolute pathname required). */ file: string; + + /* + * Optional name of project that contains file + */ + projectFileName?: string; } /** @@ -125,6 +130,16 @@ declare namespace ts.server.protocol { fileNames?: string[]; } + export interface DiagnosticWithLinePosition { + message: string; + start: number; + length: number; + startLocation: Location; + endLocation: Location; + category: string; + code: number; + } + /** * Response message for "projectInfo" request */ diff --git a/src/server/session.ts b/src/server/session.ts index fa883fbe9de..d7c0fe96e20 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -114,6 +114,7 @@ namespace ts.server { export const Formatonkey = "formatonkey"; export const Geterr = "geterr"; export const GeterrForProject = "geterrForProject"; + export const SemanticDiagnosticsFull = "semanticDiagnostics-full"; export const NavBar = "navbar"; export const Navto = "navto"; export const Occurrences = "occurrences"; @@ -245,6 +246,29 @@ namespace ts.server { this.response(body, commandName, requestSequence, errorMessage); } + private getLocation(position: number, scriptInfo: ScriptInfo): protocol.Location { + const { line, offset } = scriptInfo.positionToLineOffset(position); + return { line, offset: offset + 1 }; + } + + private getSemanticDiagnostics(args: protocol.FileRequestArgs): protocol.DiagnosticWithLinePosition[] { + var project = this.projectService.getProject(args.projectFileName) || this.projectService.getProjectForFile(args.file); + if (!project) { + return []; + } + const scriptInfo = project.getScriptInfo(args.file); + const diagnostics = project.languageService.getSemanticDiagnostics(args.file); + return diagnostics.map(d => { + message: flattenDiagnosticMessageText(d.messageText, this.host.newLine), + start: d.start, + length: d.length, + category: DiagnosticCategory[d.category].toLowerCase(), + code: d.code, + startLocation: this.getLocation(d.start, scriptInfo), + endLocation: this.getLocation(d.start + d.length, scriptInfo) + }); + } + private semanticCheck(file: string, project: Project) { try { const diags = project.languageService.getSemanticDiagnostics(file); @@ -1170,6 +1194,9 @@ namespace ts.server { [CommandNames.SignatureHelpFull]: (request: protocol.SignatureHelpRequest) => { return this.requiredResponse(this.getSignatureHelpItems(request.arguments, /*simplifiedResult*/ false)); }, + [CommandNames.SemanticDiagnosticsFull]: (request: protocol.FileRequest) => { + return this.requiredResponse(this.getSemanticDiagnostics(request.arguments)); + }, [CommandNames.Geterr]: (request: protocol.Request) => { const geterrArgs = request.arguments; return { response: this.getDiagnostics(geterrArgs.delay, geterrArgs.files), responseRequired: false }; diff --git a/src/services/services.ts b/src/services/services.ts index 8931fa72542..9209fc157c2 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -124,6 +124,7 @@ namespace ts { return new StringScriptSnapshot(text); } } + export interface PreProcessedFileInfo { referencedFiles: FileReference[]; typeReferenceDirectives: FileReference[]; @@ -132,6 +133,21 @@ namespace ts { isLibFile: boolean; } + export function realizeDiagnostics(diagnostics: Diagnostic[], newLine: string): { message: string; start: number; length: number; category: string; code: number; }[] { + return diagnostics.map(d => realizeDiagnostic(d, newLine)); + } + + export function realizeDiagnostic(diagnostic: Diagnostic, newLine: string): { message: string; start: number; length: number; category: string; code: number; } { + return { + message: flattenDiagnosticMessageText(diagnostic.messageText, newLine), + start: diagnostic.start, + length: diagnostic.length, + /// TODO: no need for the tolowerCase call + category: DiagnosticCategory[diagnostic.category].toLowerCase(), + code: diagnostic.code + }; + } + const scanner: Scanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ true); const emptyArray: any[] = []; diff --git a/src/services/shims.ts b/src/services/shims.ts index 96392ca42be..4cc1944c94b 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -517,21 +517,6 @@ namespace ts { } } - export function realizeDiagnostics(diagnostics: Diagnostic[], newLine: string): { message: string; start: number; length: number; category: string; code: number; }[] { - return diagnostics.map(d => realizeDiagnostic(d, newLine)); - } - - function realizeDiagnostic(diagnostic: Diagnostic, newLine: string): { message: string; start: number; length: number; category: string; code: number; } { - return { - message: flattenDiagnosticMessageText(diagnostic.messageText, newLine), - start: diagnostic.start, - length: diagnostic.length, - /// TODO: no need for the tolowerCase call - category: DiagnosticCategory[diagnostic.category].toLowerCase(), - code: diagnostic.code - }; - } - class LanguageServiceShimObject extends ShimBase implements LanguageServiceShim { private logger: Logger; private logPerformance = false; From f103f20b6917c9665ae282357a92e8d040c1d95a Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 15 Jun 2016 13:52:09 -0700 Subject: [PATCH 031/307] do getDirectoryPath after normalization --- src/server/editorServices.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 0e01c4c5baa..d33461efc20 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1000,7 +1000,7 @@ namespace ts.server { * the tsconfig file content and update the project; otherwise we create a new one. */ private openOrUpdateConfiguredProjectForFile(fileName: string): { configFileName?: string, configFileErrors?: Diagnostic[] } { - const searchPath = ts.normalizePath(getDirectoryPath(fileName)); + const searchPath = getDirectoryPath(normalizePath(fileName)); this.log("Search path: " + searchPath, "Info"); // check if this file is already included in one of external projects const configFileName = this.findConfigFile(searchPath); From 64423257caef5c063668a36dd8c6570111b90981 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 15 Jun 2016 16:45:04 -0700 Subject: [PATCH 032/307] normalize file name before lookup --- src/server/session.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index d7c0fe96e20..f797aa2b08d 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -252,12 +252,13 @@ namespace ts.server { } private getSemanticDiagnostics(args: protocol.FileRequestArgs): protocol.DiagnosticWithLinePosition[] { - var project = this.projectService.getProject(args.projectFileName) || this.projectService.getProjectForFile(args.file); + const file = normalizePath(args.file); + var project = (args.projectFileName && this.projectService.getProject(normalizePath(args.projectFileName))) || this.projectService.getProjectForFile(file); if (!project) { return []; } - const scriptInfo = project.getScriptInfo(args.file); - const diagnostics = project.languageService.getSemanticDiagnostics(args.file); + const scriptInfo = project.getScriptInfo(file); + const diagnostics = project.languageService.getSemanticDiagnostics(file); return diagnostics.map(d => { message: flattenDiagnosticMessageText(d.messageText, this.host.newLine), start: d.start, From 7b93e732d5b4cb42310ca216974fc4dc666793ef Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 15 Jun 2016 18:30:08 -0700 Subject: [PATCH 033/307] enable semantic classification --- src/server/protocol.d.ts | 9 +++++++ src/server/session.ts | 51 +++++++++++++++++++++++++--------------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index ef66b0119d4..db093f09ef7 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -182,6 +182,15 @@ declare namespace ts.server.protocol { arguments: FileLocationRequestArgs; } + export interface FileSpanRequestArgs extends FileRequestArgs { + start: number; + length: number; + } + + export interface FileSpanRequest extends FileRequest { + arguments: FileSpanRequestArgs; + } + /** * Arguments in document highlight request; include: filesToSearch, file, * line, offset. diff --git a/src/server/session.ts b/src/server/session.ts index f797aa2b08d..f9820bc1f4b 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -138,6 +138,7 @@ namespace ts.server { export const CloseExternalProject = "closeExternalProject"; export const SynchronizeProjectList = "synchronizeProjectList"; export const ApplyChangedToOpenFiles = "applyChangedToOpenFiles"; + export const EncodedSemanticClassificationsFull = "encodedSemanticClassifications-full"; } namespace Errors { @@ -251,25 +252,6 @@ namespace ts.server { return { line, offset: offset + 1 }; } - private getSemanticDiagnostics(args: protocol.FileRequestArgs): protocol.DiagnosticWithLinePosition[] { - const file = normalizePath(args.file); - var project = (args.projectFileName && this.projectService.getProject(normalizePath(args.projectFileName))) || this.projectService.getProjectForFile(file); - if (!project) { - return []; - } - const scriptInfo = project.getScriptInfo(file); - const diagnostics = project.languageService.getSemanticDiagnostics(file); - return diagnostics.map(d => { - message: flattenDiagnosticMessageText(d.messageText, this.host.newLine), - start: d.start, - length: d.length, - category: DiagnosticCategory[d.category].toLowerCase(), - code: d.code, - startLocation: this.getLocation(d.start, scriptInfo), - endLocation: this.getLocation(d.start + d.length, scriptInfo) - }); - } - private semanticCheck(file: string, project: Project) { try { const diags = project.languageService.getSemanticDiagnostics(file); @@ -346,6 +328,34 @@ namespace ts.server { } } + private getEncodedSemanticClassifications(args: protocol.FileSpanRequestArgs) { + const file = normalizePath(args.file); + const project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + return project.languageService.getEncodedSemanticClassifications(file, args); + } + + private getSemanticDiagnostics(args: protocol.FileRequestArgs): protocol.DiagnosticWithLinePosition[] { + const file = normalizePath(args.file); + var project = (args.projectFileName && this.projectService.getProject(normalizePath(args.projectFileName))) || this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + const scriptInfo = project.getScriptInfo(file); + const diagnostics = project.languageService.getSemanticDiagnostics(file); + return diagnostics.map(d => { + message: flattenDiagnosticMessageText(d.messageText, this.host.newLine), + start: d.start, + length: d.length, + category: DiagnosticCategory[d.category].toLowerCase(), + code: d.code, + startLocation: this.getLocation(d.start, scriptInfo), + endLocation: this.getLocation(d.start + d.length, scriptInfo) + }); + } + private getDefinition(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.FileSpan[] | DefinitionInfo[] { const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); @@ -1198,6 +1208,9 @@ namespace ts.server { [CommandNames.SemanticDiagnosticsFull]: (request: protocol.FileRequest) => { return this.requiredResponse(this.getSemanticDiagnostics(request.arguments)); }, + [CommandNames.EncodedSemanticClassificationsFull]: (request: protocol.FileSpanRequest) => { + return this.requiredResponse(this.getEncodedSemanticClassifications(request.arguments)); + }, [CommandNames.Geterr]: (request: protocol.Request) => { const geterrArgs = request.arguments; return { response: this.getDiagnostics(geterrArgs.delay, geterrArgs.files), responseRequired: false }; From c1d6a14a6b2ac775c53a5443a59487fa01fb3b42 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 15 Jun 2016 21:41:54 -0700 Subject: [PATCH 034/307] set default project on script info when it is queried from project --- src/server/editorServices.ts | 6 +++++- src/server/session.ts | 6 +----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index d33461efc20..cd2a720fbc4 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -397,7 +397,11 @@ namespace ts.server { } getScriptInfo(fileName: string) { - return this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); + const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); + if (!scriptInfo.defaultProject) { + scriptInfo.defaultProject = this; + } + return scriptInfo; } filesToString() { diff --git a/src/server/session.ts b/src/server/session.ts index f9820bc1f4b..d46ed9f6732 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -339,7 +339,7 @@ namespace ts.server { private getSemanticDiagnostics(args: protocol.FileRequestArgs): protocol.DiagnosticWithLinePosition[] { const file = normalizePath(args.file); - var project = (args.projectFileName && this.projectService.getProject(normalizePath(args.projectFileName))) || this.projectService.getProjectForFile(file); + const project = (args.projectFileName && this.projectService.getProject(normalizePath(args.projectFileName))) || this.projectService.getProjectForFile(file); if (!project) { throw Errors.NoProject; } @@ -1103,10 +1103,6 @@ namespace ts.server { return { response, responseRequired: true }; } - private canceledResponse() { - return { canceled: true, responseRequired: true }; - } - private handlers: Map<(request: protocol.Request) => { response?: any, responseRequired?: boolean }> = { [CommandNames.OpenExternalProject]: (request: protocol.OpenExternalProjectRequest) => { this.projectService.openExternalProject(request.arguments); From a8dc65de192252cdb231fa241712058089e1b483 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 15 Jun 2016 21:52:39 -0700 Subject: [PATCH 035/307] added missing check --- src/server/editorServices.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index cd2a720fbc4..965c576281a 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -398,7 +398,7 @@ namespace ts.server { getScriptInfo(fileName: string) { const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); - if (!scriptInfo.defaultProject) { + if (scriptInfo && !scriptInfo.defaultProject) { scriptInfo.defaultProject = this; } return scriptInfo; From 7c0927d7520e1ac62520568538f7c1b136896af8 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 16 Jun 2016 15:23:52 -0700 Subject: [PATCH 036/307] formatting --- src/server/editorServices.ts | 3 ++- src/server/protocol.d.ts | 5 ++++ src/server/session.ts | 46 ++++++++++++++++++++++++++++++++++-- src/services/services.ts | 3 ++- 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 965c576281a..f77abe5e25f 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1519,7 +1519,8 @@ namespace ts.server { for (const file of changedFiles) { const scriptInfo = this.getScriptInfo(file.fileName); Debug.assert(!!scriptInfo); - for (const change of file.changes) { + for (let i = file.changes.length - 1; i >= 0; i--) { + const change = file.changes[i]; scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); } } diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index db093f09ef7..5fddf266ffd 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -735,6 +735,9 @@ declare namespace ts.server.protocol { * Character offset on last line of range for which to format text in file. */ endOffset: number; + + endPosition?: number; + options?: ts.FormatCodeOptions; } /** @@ -788,6 +791,8 @@ declare namespace ts.server.protocol { * Key pressed (';', '\n', or '}'). */ key: string; + + options?: ts.FormatCodeOptions; } /** diff --git a/src/server/session.ts b/src/server/session.ts index d46ed9f6732..39079b24c8d 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -112,6 +112,9 @@ namespace ts.server { export const Exit = "exit"; export const Format = "format"; export const Formatonkey = "formatonkey"; + export const FormatFull = "format-full"; + export const FormatonkeyFull = "formatonkey-full"; + export const FormatRangeFull = "formatRange-full"; export const Geterr = "geterr"; export const GeterrForProject = "geterrForProject"; export const SemanticDiagnosticsFull = "semanticDiagnostics-full"; @@ -720,6 +723,36 @@ namespace ts.server { }); } + private getFormattingEditsForRangeFull(args: protocol.FormatRequestArgs) { + const file = ts.normalizePath(args.file); + const project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + + return project.languageService.getFormattingEditsForRange(file, args.position, args.endPosition, args.options); + } + + private getFormattingEditsForDocumentFull(args: protocol.FormatRequestArgs) { + const file = ts.normalizePath(args.file); + const project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + + return project.languageService.getFormattingEditsForDocument(file, args.options); + } + + private getFormattingEditsAfterKeystrokeFull(args: protocol.FormatOnKeyRequestArgs) { + const file = ts.normalizePath(args.file); + const project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + + return project.languageService.getFormattingEditsAfterKeystroke(file, args.position, args.key, args.options); + } + private getFormattingEditsAfterKeystroke(line: number, offset: number, key: string, fileName: string): protocol.CodeEdit[] { const file = ts.normalizePath(fileName); @@ -1180,11 +1213,20 @@ namespace ts.server { }, [CommandNames.Format]: (request: protocol.Request) => { const formatArgs = request.arguments; - return { response: this.getFormattingEditsForRange(formatArgs.line, formatArgs.offset, formatArgs.endLine, formatArgs.endOffset, formatArgs.file), responseRequired: true }; + return this.requiredResponse(this.getFormattingEditsForRange(formatArgs.line, formatArgs.offset, formatArgs.endLine, formatArgs.endOffset, formatArgs.file)); }, [CommandNames.Formatonkey]: (request: protocol.Request) => { const formatOnKeyArgs = request.arguments; - return { response: this.getFormattingEditsAfterKeystroke(formatOnKeyArgs.line, formatOnKeyArgs.offset, formatOnKeyArgs.key, formatOnKeyArgs.file), responseRequired: true }; + return this.requiredResponse(this.getFormattingEditsAfterKeystroke(formatOnKeyArgs.line, formatOnKeyArgs.offset, formatOnKeyArgs.key, formatOnKeyArgs.file)); + }, + [CommandNames.FormatFull]: (request: protocol.FormatRequest) => { + return this.requiredResponse(this.getFormattingEditsForDocumentFull(request.arguments)); + }, + [CommandNames.FormatonkeyFull]: (request: protocol.FormatOnKeyRequest) => { + return this.requiredResponse(this.getFormattingEditsAfterKeystrokeFull(request.arguments)); + }, + [CommandNames.FormatRangeFull]: (request: protocol.FormatRequest) => { + return this.requiredResponse(this.getFormattingEditsForRangeFull(request.arguments)); }, [CommandNames.Completions]: (request: protocol.CompletionDetailsRequest) => { return this.requiredResponse(this.getCompletions(request.arguments, /*simplifiedResult*/ true)); diff --git a/src/services/services.ts b/src/services/services.ts index 9209fc157c2..43232f1968d 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1894,6 +1894,7 @@ namespace ts { } } + let x = 1; class SyntaxTreeCache { // For our syntactic only features, we also keep a cache of the syntax tree for the // currently edited file. @@ -1926,7 +1927,7 @@ namespace ts { sourceFile = updateLanguageServiceSourceFile(this.currentSourceFile, scriptSnapshot, version, editRange); } - if (sourceFile) { + if (sourceFile) { // All done, ensure state is up to date this.currentFileVersion = version; this.currentFileName = fileName; From 04d617de8d58f94af6b19330e4134e14675b7b99 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 16 Jun 2016 15:47:01 -0700 Subject: [PATCH 037/307] fix linter --- src/server/session.ts | 5 ++--- src/services/services.ts | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 39079b24c8d..11c413a7a18 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -729,7 +729,7 @@ namespace ts.server { if (!project) { throw Errors.NoProject; } - + return project.languageService.getFormattingEditsForRange(file, args.position, args.endPosition, args.options); } @@ -739,7 +739,7 @@ namespace ts.server { if (!project) { throw Errors.NoProject; } - + return project.languageService.getFormattingEditsForDocument(file, args.options); } @@ -749,7 +749,6 @@ namespace ts.server { if (!project) { throw Errors.NoProject; } - return project.languageService.getFormattingEditsAfterKeystroke(file, args.position, args.key, args.options); } diff --git a/src/services/services.ts b/src/services/services.ts index 43232f1968d..9209fc157c2 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1894,7 +1894,6 @@ namespace ts { } } - let x = 1; class SyntaxTreeCache { // For our syntactic only features, we also keep a cache of the syntax tree for the // currently edited file. @@ -1927,7 +1926,7 @@ namespace ts { sourceFile = updateLanguageServiceSourceFile(this.currentSourceFile, scriptSnapshot, version, editRange); } - if (sourceFile) { + if (sourceFile) { // All done, ensure state is up to date this.currentFileVersion = version; this.currentFileName = fileName; From 6ebc8abea2a7e7a00527f6615e395ec3d833123e Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 17 Jun 2016 14:06:33 -0700 Subject: [PATCH 038/307] introduce EditorSettings/FormatCodeSettings interfaces --- src/harness/fourslash.ts | 66 +++++++++--------- src/server/editorServices.ts | 66 +++++++++--------- src/server/protocol.d.ts | 3 - src/server/session.ts | 8 +-- src/services/formatting/formatting.ts | 48 ++++++------- src/services/formatting/rulesProvider.ts | 26 +++---- src/services/formatting/smartIndenter.ts | 34 ++++----- src/services/services.ts | 87 +++++++++++++++++++----- 8 files changed, 194 insertions(+), 144 deletions(-) diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 9f675e3ffd0..d14dfc8f80f 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -202,7 +202,7 @@ namespace FourSlash { // Whether or not we should format on keystrokes public enableFormatting = true; - public formatCodeOptions: ts.FormatCodeOptions; + public formatCodeSettings: ts.FormatCodeSettings; private inputFiles: ts.Map = {}; // Map between inputFile's fileName and its content for easily looking up when resolving references @@ -309,22 +309,22 @@ namespace FourSlash { Harness.Compiler.getDefaultLibrarySourceFile().text, /*isRootFile*/ false); } - this.formatCodeOptions = { - IndentSize: 4, - TabSize: 4, - NewLineCharacter: Harness.IO.newLine(), - ConvertTabsToSpaces: true, - IndentStyle: ts.IndentStyle.Smart, - InsertSpaceAfterCommaDelimiter: true, - InsertSpaceAfterSemicolonInForStatements: true, - InsertSpaceBeforeAndAfterBinaryOperators: true, - InsertSpaceAfterKeywordsInControlFlowStatements: true, - InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false, - InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, - InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, - InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, - PlaceOpenBraceOnNewLineForFunctions: false, - PlaceOpenBraceOnNewLineForControlBlocks: false, + this.formatCodeSettings = { + indentSize: 4, + tabSize: 4, + newLineCharacter: Harness.IO.newLine(), + convertTabsToSpaces: true, + indentStyle: ts.IndentStyle.Smart, + insertSpaceAfterCommaDelimiter: true, + insertSpaceAfterSemicolonInForStatements: true, + insertSpaceBeforeAndAfterBinaryOperators: true, + insertSpaceAfterKeywordsInControlFlowStatements: true, + insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, + insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, + placeOpenBraceOnNewLineForFunctions: false, + placeOpenBraceOnNewLineForControlBlocks: false, }; // Open the first file by default @@ -1278,7 +1278,7 @@ namespace FourSlash { // Handle post-keystroke formatting if (this.enableFormatting) { - const edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, offset, ch, this.formatCodeOptions); + const edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, offset, ch, this.formatCodeSettings); if (edits.length) { offset += this.applyEdits(this.activeFile.fileName, edits, /*isFormattingEdit*/ true); // this.checkPostEditInvariants(); @@ -1316,7 +1316,7 @@ namespace FourSlash { // Handle post-keystroke formatting if (this.enableFormatting) { - const edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, offset, ch, this.formatCodeOptions); + const edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, offset, ch, this.formatCodeSettings); if (edits.length) { offset += this.applyEdits(this.activeFile.fileName, edits, /*isFormattingEdit*/ true); } @@ -1369,7 +1369,7 @@ namespace FourSlash { // Handle post-keystroke formatting if (this.enableFormatting) { - const edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, offset, ch, this.formatCodeOptions); + const edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, offset, ch, this.formatCodeSettings); if (edits.length) { offset += this.applyEdits(this.activeFile.fileName, edits, /*isFormattingEdit*/ true); // this.checkPostEditInvariants(); @@ -1395,7 +1395,7 @@ namespace FourSlash { // Handle formatting if (this.enableFormatting) { - const edits = this.languageService.getFormattingEditsForRange(this.activeFile.fileName, start, offset, this.formatCodeOptions); + const edits = this.languageService.getFormattingEditsForRange(this.activeFile.fileName, start, offset, this.formatCodeSettings); if (edits.length) { offset += this.applyEdits(this.activeFile.fileName, edits, /*isFormattingEdit*/ true); this.checkPostEditInvariants(); @@ -1469,30 +1469,30 @@ namespace FourSlash { return runningOffset; } - public copyFormatOptions(): ts.FormatCodeOptions { - return ts.clone(this.formatCodeOptions); + public copyFormatOptions(): ts.FormatCodeSettings { + return ts.clone(this.formatCodeSettings); } - public setFormatOptions(formatCodeOptions: ts.FormatCodeOptions): ts.FormatCodeOptions { - const oldFormatCodeOptions = this.formatCodeOptions; - this.formatCodeOptions = formatCodeOptions; + public setFormatOptions(formatCodeOptions: ts.FormatCodeOptions | ts.FormatCodeSettings): ts.FormatCodeSettings { + const oldFormatCodeOptions = this.formatCodeSettings; + this.formatCodeSettings = ts.toEditorSettings(formatCodeOptions); return oldFormatCodeOptions; } public formatDocument() { - const edits = this.languageService.getFormattingEditsForDocument(this.activeFile.fileName, this.formatCodeOptions); + const edits = this.languageService.getFormattingEditsForDocument(this.activeFile.fileName, this.formatCodeSettings); this.currentCaretPosition += this.applyEdits(this.activeFile.fileName, edits, /*isFormattingEdit*/ true); this.fixCaretPosition(); } public formatSelection(start: number, end: number) { - const edits = this.languageService.getFormattingEditsForRange(this.activeFile.fileName, start, end, this.formatCodeOptions); + const edits = this.languageService.getFormattingEditsForRange(this.activeFile.fileName, start, end, this.formatCodeSettings); this.currentCaretPosition += this.applyEdits(this.activeFile.fileName, edits, /*isFormattingEdit*/ true); this.fixCaretPosition(); } public formatOnType(pos: number, key: string) { - const edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, pos, key, this.formatCodeOptions); + const edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, pos, key, this.formatCodeSettings); this.currentCaretPosition += this.applyEdits(this.activeFile.fileName, edits, /*isFormattingEdit*/ true); this.fixCaretPosition(); } @@ -1621,8 +1621,8 @@ namespace FourSlash { private getIndentation(fileName: string, position: number, indentStyle: ts.IndentStyle): number { - const formatOptions = ts.clone(this.formatCodeOptions); - formatOptions.IndentStyle = indentStyle; + const formatOptions = ts.clone(this.formatCodeSettings); + formatOptions.indentStyle = indentStyle; return this.languageService.getIndentationAtPosition(fileName, position, formatOptions); } @@ -3226,7 +3226,7 @@ namespace FourSlashInterface { this.state.formatDocument(); } - public copyFormatOptions(): ts.FormatCodeOptions { + public copyFormatOptions(): ts.FormatCodeSettings { return this.state.copyFormatOptions(); } @@ -3246,7 +3246,7 @@ namespace FourSlashInterface { public setOption(name: string, value: string): void; public setOption(name: string, value: boolean): void; public setOption(name: string, value: any): void { - this.state.formatCodeOptions[name] = value; + (this.state.formatCodeSettings)[name] = value; } } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f77abe5e25f..bfbee344b7a 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -16,53 +16,51 @@ namespace ts.server { msg(s: string, type?: string): void; } - function getDefaultFormatCodeOptions(host: ServerHost): ts.FormatCodeOptions { - return ts.clone({ - IndentSize: 4, - TabSize: 4, - NewLineCharacter: host.newLine || "\n", - ConvertTabsToSpaces: true, - IndentStyle: ts.IndentStyle.Smart, - InsertSpaceAfterCommaDelimiter: true, - InsertSpaceAfterSemicolonInForStatements: true, - InsertSpaceBeforeAndAfterBinaryOperators: true, - InsertSpaceAfterKeywordsInControlFlowStatements: true, - InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false, - InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, - InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, - InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, - PlaceOpenBraceOnNewLineForFunctions: false, - PlaceOpenBraceOnNewLineForControlBlocks: false, + function getDefaultFormatCodeSettings(host: ServerHost): ts.FormatCodeSettings { + return ts.clone({ + indentSize: 4, + tabSize: 4, + newLineCharacter: host.newLine || "\n", + convertTabsToSpaces: true, + indentStyle: ts.IndentStyle.Smart, + insertSpaceAfterCommaDelimiter: true, + insertSpaceAfterSemicolonInForStatements: true, + insertSpaceBeforeAndAfterBinaryOperators: true, + insertSpaceAfterKeywordsInControlFlowStatements: true, + insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, + insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, + placeOpenBraceOnNewLineForFunctions: false, + placeOpenBraceOnNewLineForControlBlocks: false, }); } - function mergeFormatOptions(formatCodeOptions: FormatCodeOptions, formatOptions: protocol.FormatOptions): void { - const hasOwnProperty = Object.prototype.hasOwnProperty; - Object.keys(formatOptions).forEach((key) => { - const codeKey = key.charAt(0).toUpperCase() + key.substring(1); - if (hasOwnProperty.call(formatCodeOptions, codeKey)) { - formatCodeOptions[codeKey] = formatOptions[key]; + function mergeMaps(target: Map, source: Map): void { + for (const key in source) { + if (hasProperty(source, key)) { + target[key] = source[key]; } - }); + } } export class ScriptInfo { svc: ScriptVersionCache; defaultProject: Project; // project to use by default for file fileWatcher: FileWatcher; - formatCodeOptions: ts.FormatCodeOptions; + formatCodeSettings: ts.FormatCodeSettings; path: Path; scriptKind: ScriptKind; constructor(private host: ServerHost, public fileName: string, public content: string, public isOpen = false) { this.path = toPath(fileName, host.getCurrentDirectory(), createGetCanonicalFileName(host.useCaseSensitiveFileNames)); this.svc = ScriptVersionCache.fromString(host, content); - this.formatCodeOptions = getDefaultFormatCodeOptions(this.host); + this.formatCodeSettings = getDefaultFormatCodeSettings(this.host); } - setFormatOptions(formatOptions: protocol.FormatOptions): void { - if (formatOptions) { - mergeFormatOptions(this.formatCodeOptions, formatOptions); + setFormatOptions(formatSettings: protocol.FormatOptions): void { + if (formatSettings) { + mergeMaps(this.formatCodeSettings, formatSettings); } } @@ -603,7 +601,7 @@ namespace ts.server { } export interface HostConfiguration { - formatCodeOptions: ts.FormatCodeOptions; + formatCodeOptions: ts.FormatCodeSettings; hostInfo: string; } @@ -665,7 +663,7 @@ namespace ts.server { private setDefaultHostConfiguration() { this.hostConfiguration = { - formatCodeOptions: getDefaultFormatCodeOptions(this.host), + formatCodeOptions: getDefaultFormatCodeSettings(this.host), hostInfo: "Unknown host" }; } @@ -693,7 +691,7 @@ namespace ts.server { if (file) { const info = this.filenameToScriptInfo[file]; if (info) { - return info.formatCodeOptions; + return info.formatCodeSettings; } } return this.hostConfiguration.formatCodeOptions; @@ -1286,7 +1284,7 @@ namespace ts.server { if (content !== undefined) { info = new ScriptInfo(this.host, fileName, content, openedByClient); info.scriptKind = scriptKind; - info.setFormatOptions(this.getFormatCodeOptions()); + info.setFormatOptions(toEditorSettings(this.getFormatCodeOptions())); this.filenameToScriptInfo[fileName] = info; if (!info.isOpen) { info.fileWatcher = this.host.watchFile(fileName, _ => { this.onSourceFileChanged(fileName); }); @@ -1322,7 +1320,7 @@ namespace ts.server { this.log("Host information " + args.hostInfo, "Info"); } if (args.formatOptions) { - mergeFormatOptions(this.hostConfiguration.formatCodeOptions, args.formatOptions); + mergeMaps(this.hostConfiguration.formatCodeOptions, args.formatOptions); this.log("Format host information updated", "Info"); } } diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 5fddf266ffd..1d77aeba2ac 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -544,9 +544,6 @@ declare namespace ts.server.protocol { /** Defines whether an open brace is put onto a new line for control blocks or not. Default value is false. */ placeOpenBraceOnNewLineForControlBlocks?: boolean; - - /** Index operator */ - [key: string]: string | number | boolean; } /** diff --git a/src/server/session.ts b/src/server/session.ts index 11c413a7a18..03fee122d39 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -778,10 +778,10 @@ namespace ts.server { if (lineText.search("\\S") < 0) { // TODO: get these options from host const editorOptions: ts.EditorOptions = { - IndentSize: formatOptions.IndentSize, - TabSize: formatOptions.TabSize, - NewLineCharacter: formatOptions.NewLineCharacter, - ConvertTabsToSpaces: formatOptions.ConvertTabsToSpaces, + IndentSize: formatOptions.indentSize, + TabSize: formatOptions.tabSize, + NewLineCharacter: formatOptions.newLineCharacter, + ConvertTabsToSpaces: formatOptions.convertTabsToSpaces, IndentStyle: ts.IndentStyle.Smart, }; const preferredIndent = project.languageService.getIndentationAtPosition(file, position, editorOptions); diff --git a/src/services/formatting/formatting.ts b/src/services/formatting/formatting.ts index ef8fddcfb3a..fccb9fb0b1c 100644 --- a/src/services/formatting/formatting.ts +++ b/src/services/formatting/formatting.ts @@ -67,7 +67,7 @@ namespace ts.formatting { delta: number; } - export function formatOnEnter(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeOptions): TextChange[] { + export function formatOnEnter(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] { const line = sourceFile.getLineAndCharacterOfPosition(position).line; if (line === 0) { return []; @@ -96,15 +96,15 @@ namespace ts.formatting { return formatSpan(span, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnEnter); } - export function formatOnSemicolon(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeOptions): TextChange[] { + export function formatOnSemicolon(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] { return formatOutermostParent(position, SyntaxKind.SemicolonToken, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnSemicolon); } - export function formatOnClosingCurly(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeOptions): TextChange[] { + export function formatOnClosingCurly(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] { return formatOutermostParent(position, SyntaxKind.CloseBraceToken, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnClosingCurlyBrace); } - export function formatDocument(sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeOptions): TextChange[] { + export function formatDocument(sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] { const span = { pos: 0, end: sourceFile.text.length @@ -112,7 +112,7 @@ namespace ts.formatting { return formatSpan(span, sourceFile, options, rulesProvider, FormattingRequestKind.FormatDocument); } - export function formatSelection(start: number, end: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeOptions): TextChange[] { + export function formatSelection(start: number, end: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] { // format from the beginning of the line const span = { pos: getLineStartPositionForPosition(start, sourceFile), @@ -121,7 +121,7 @@ namespace ts.formatting { return formatSpan(span, sourceFile, options, rulesProvider, FormattingRequestKind.FormatSelection); } - function formatOutermostParent(position: number, expectedLastToken: SyntaxKind, sourceFile: SourceFile, options: FormatCodeOptions, rulesProvider: RulesProvider, requestKind: FormattingRequestKind): TextChange[] { + function formatOutermostParent(position: number, expectedLastToken: SyntaxKind, sourceFile: SourceFile, options: FormatCodeSettings, rulesProvider: RulesProvider, requestKind: FormattingRequestKind): TextChange[] { const parent = findOutermostParent(position, expectedLastToken, sourceFile); if (!parent) { return []; @@ -294,7 +294,7 @@ namespace ts.formatting { * if parent is on the different line - its delta was already contributed * to the initial indentation. */ - function getOwnOrInheritedDelta(n: Node, options: FormatCodeOptions, sourceFile: SourceFile): number { + function getOwnOrInheritedDelta(n: Node, options: FormatCodeSettings, sourceFile: SourceFile): number { let previousLine = Constants.Unknown; let child: Node; while (n) { @@ -304,7 +304,7 @@ namespace ts.formatting { } if (SmartIndenter.shouldIndentChildNode(n, child)) { - return options.IndentSize; + return options.indentSize; } previousLine = line; @@ -316,7 +316,7 @@ namespace ts.formatting { function formatSpan(originalRange: TextRange, sourceFile: SourceFile, - options: FormatCodeOptions, + options: FormatCodeSettings, rulesProvider: RulesProvider, requestKind: FormattingRequestKind): TextChange[] { @@ -410,7 +410,7 @@ namespace ts.formatting { effectiveParentStartLine: number): Indentation { let indentation = inheritedIndentation; - let delta = SmartIndenter.shouldIndentChildNode(node) ? options.IndentSize : 0; + let delta = SmartIndenter.shouldIndentChildNode(node) ? options.indentSize : 0; if (effectiveParentStartLine === startLine) { // if node is located on the same line with the parent @@ -419,7 +419,7 @@ namespace ts.formatting { indentation = startLine === lastIndentedLine ? indentationOnLastIndentedLine : parentDynamicIndentation.getIndentation(); - delta = Math.min(options.IndentSize, parentDynamicIndentation.getDelta(node) + delta); + delta = Math.min(options.indentSize, parentDynamicIndentation.getDelta(node) + delta); } else if (indentation === Constants.Unknown) { if (SmartIndenter.childStartsOnTheSameLineWithElseInIfStatement(parent, node, startLine, sourceFile)) { @@ -503,14 +503,14 @@ namespace ts.formatting { recomputeIndentation: lineAdded => { if (node.parent && SmartIndenter.shouldIndentChildNode(node.parent, node)) { if (lineAdded) { - indentation += options.IndentSize; + indentation += options.indentSize; } else { - indentation -= options.IndentSize; + indentation -= options.indentSize; } if (SmartIndenter.shouldIndentChildNode(node)) { - delta = options.IndentSize; + delta = options.indentSize; } else { delta = 0; @@ -1036,7 +1036,7 @@ namespace ts.formatting { // edit should not be applied only if we have one line feed between elements const lineDelta = currentStartLine - previousStartLine; if (lineDelta !== 1) { - recordReplace(previousRange.end, currentRange.pos - previousRange.end, options.NewLineCharacter); + recordReplace(previousRange.end, currentRange.pos - previousRange.end, options.newLineCharacter); } break; case RuleAction.Space: @@ -1102,19 +1102,19 @@ namespace ts.formatting { let internedTabsIndentation: string[]; let internedSpacesIndentation: string[]; - export function getIndentationString(indentation: number, options: FormatCodeOptions): string { + export function getIndentationString(indentation: number, options: EditorSettings): string { // reset interned strings if FormatCodeOptions were changed const resetInternedStrings = - !internedSizes || (internedSizes.tabSize !== options.TabSize || internedSizes.indentSize !== options.IndentSize); + !internedSizes || (internedSizes.tabSize !== options.tabSize || internedSizes.indentSize !== options.indentSize); if (resetInternedStrings) { - internedSizes = { tabSize: options.TabSize, indentSize: options.IndentSize }; + internedSizes = { tabSize: options.tabSize, indentSize: options.indentSize }; internedTabsIndentation = internedSpacesIndentation = undefined; } - if (!options.ConvertTabsToSpaces) { - const tabs = Math.floor(indentation / options.TabSize); - const spaces = indentation - tabs * options.TabSize; + if (!options.convertTabsToSpaces) { + const tabs = Math.floor(indentation / options.tabSize); + const spaces = indentation - tabs * options.tabSize; let tabString: string; if (!internedTabsIndentation) { @@ -1132,14 +1132,14 @@ namespace ts.formatting { } else { let spacesString: string; - const quotient = Math.floor(indentation / options.IndentSize); - const remainder = indentation % options.IndentSize; + const quotient = Math.floor(indentation / options.indentSize); + const remainder = indentation % options.indentSize; if (!internedSpacesIndentation) { internedSpacesIndentation = []; } if (internedSpacesIndentation[quotient] === undefined) { - spacesString = repeat(" ", options.IndentSize * quotient); + spacesString = repeat(" ", options.indentSize * quotient); internedSpacesIndentation[quotient] = spacesString; } else { diff --git a/src/services/formatting/rulesProvider.ts b/src/services/formatting/rulesProvider.ts index d672a401d89..ba3c3e0d356 100644 --- a/src/services/formatting/rulesProvider.ts +++ b/src/services/formatting/rulesProvider.ts @@ -4,7 +4,7 @@ namespace ts.formatting { export class RulesProvider { private globalRules: Rules; - private options: ts.FormatCodeOptions; + private options: ts.FormatCodeSettings; private activeRules: Rule[]; private rulesMap: RulesMap; @@ -24,7 +24,7 @@ namespace ts.formatting { return this.rulesMap; } - public ensureUpToDate(options: ts.FormatCodeOptions) { + public ensureUpToDate(options: ts.FormatCodeSettings) { if (!this.options || !ts.compareDataObjects(this.options, options)) { const activeRules = this.createActiveRules(options); const rulesMap = RulesMap.create(activeRules); @@ -35,31 +35,31 @@ namespace ts.formatting { } } - private createActiveRules(options: ts.FormatCodeOptions): Rule[] { + private createActiveRules(options: ts.FormatCodeSettings): Rule[] { let rules = this.globalRules.HighPriorityCommonRules.slice(0); - if (options.InsertSpaceAfterCommaDelimiter) { + if (options.insertSpaceAfterCommaDelimiter) { rules.push(this.globalRules.SpaceAfterComma); } else { rules.push(this.globalRules.NoSpaceAfterComma); } - if (options.InsertSpaceAfterFunctionKeywordForAnonymousFunctions) { + if (options.insertSpaceAfterFunctionKeywordForAnonymousFunctions) { rules.push(this.globalRules.SpaceAfterAnonymousFunctionKeyword); } else { rules.push(this.globalRules.NoSpaceAfterAnonymousFunctionKeyword); } - if (options.InsertSpaceAfterKeywordsInControlFlowStatements) { + if (options.insertSpaceAfterKeywordsInControlFlowStatements) { rules.push(this.globalRules.SpaceAfterKeywordInControl); } else { rules.push(this.globalRules.NoSpaceAfterKeywordInControl); } - if (options.InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis) { + if (options.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis) { rules.push(this.globalRules.SpaceAfterOpenParen); rules.push(this.globalRules.SpaceBeforeCloseParen); rules.push(this.globalRules.NoSpaceBetweenParens); @@ -70,7 +70,7 @@ namespace ts.formatting { rules.push(this.globalRules.NoSpaceBetweenParens); } - if (options.InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets) { + if (options.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets) { rules.push(this.globalRules.SpaceAfterOpenBracket); rules.push(this.globalRules.SpaceBeforeCloseBracket); rules.push(this.globalRules.NoSpaceBetweenBrackets); @@ -81,7 +81,7 @@ namespace ts.formatting { rules.push(this.globalRules.NoSpaceBetweenBrackets); } - if (options.InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces) { + if (options.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces) { rules.push(this.globalRules.SpaceAfterTemplateHeadAndMiddle); rules.push(this.globalRules.SpaceBeforeTemplateMiddleAndTail); } @@ -90,14 +90,14 @@ namespace ts.formatting { rules.push(this.globalRules.NoSpaceBeforeTemplateMiddleAndTail); } - if (options.InsertSpaceAfterSemicolonInForStatements) { + if (options.insertSpaceAfterSemicolonInForStatements) { rules.push(this.globalRules.SpaceAfterSemicolonInFor); } else { rules.push(this.globalRules.NoSpaceAfterSemicolonInFor); } - if (options.InsertSpaceBeforeAndAfterBinaryOperators) { + if (options.insertSpaceBeforeAndAfterBinaryOperators) { rules.push(this.globalRules.SpaceBeforeBinaryOperator); rules.push(this.globalRules.SpaceAfterBinaryOperator); } @@ -106,11 +106,11 @@ namespace ts.formatting { rules.push(this.globalRules.NoSpaceAfterBinaryOperator); } - if (options.PlaceOpenBraceOnNewLineForControlBlocks) { + if (options.placeOpenBraceOnNewLineForControlBlocks) { rules.push(this.globalRules.NewLineBeforeOpenBraceInControl); } - if (options.PlaceOpenBraceOnNewLineForFunctions) { + if (options.placeOpenBraceOnNewLineForFunctions) { rules.push(this.globalRules.NewLineBeforeOpenBraceInFunction); rules.push(this.globalRules.NewLineBeforeOpenBraceInTypeScriptDeclWithBlock); } diff --git a/src/services/formatting/smartIndenter.ts b/src/services/formatting/smartIndenter.ts index 23a1d937869..9c9d9eb9149 100644 --- a/src/services/formatting/smartIndenter.ts +++ b/src/services/formatting/smartIndenter.ts @@ -8,14 +8,14 @@ namespace ts.formatting { Unknown = -1 } - export function getIndentation(position: number, sourceFile: SourceFile, options: EditorOptions): number { + export function getIndentation(position: number, sourceFile: SourceFile, options: EditorSettings): number { if (position > sourceFile.text.length) { return 0; // past EOF } // no indentation when the indent style is set to none, // so we can return fast - if (options.IndentStyle === IndentStyle.None) { + if (options.indentStyle === IndentStyle.None) { return 0; } @@ -35,7 +35,7 @@ namespace ts.formatting { // indentation is first non-whitespace character in a previous line // for block indentation, we should look for a line which contains something that's not // whitespace. - if (options.IndentStyle === IndentStyle.Block) { + if (options.indentStyle === IndentStyle.Block) { // move backwards until we find a line with a non-whitespace character, // then find the first non-whitespace character for that line. @@ -75,7 +75,7 @@ namespace ts.formatting { indentationDelta = 0; } else { - indentationDelta = lineAtPosition !== currentStart.line ? options.IndentSize : 0; + indentationDelta = lineAtPosition !== currentStart.line ? options.indentSize : 0; } break; @@ -88,7 +88,7 @@ namespace ts.formatting { } actualIndentation = getLineIndentationWhenExpressionIsInMultiLine(current, sourceFile, options); if (actualIndentation !== Value.Unknown) { - return actualIndentation + options.IndentSize; + return actualIndentation + options.indentSize; } previous = current; @@ -103,7 +103,7 @@ namespace ts.formatting { return getIndentationForNodeWorker(current, currentStart, /*ignoreActualIndentationRange*/ undefined, indentationDelta, sourceFile, options); } - export function getIndentationForNode(n: Node, ignoreActualIndentationRange: TextRange, sourceFile: SourceFile, options: FormatCodeOptions): number { + export function getIndentationForNode(n: Node, ignoreActualIndentationRange: TextRange, sourceFile: SourceFile, options: EditorSettings): number { const start = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)); return getIndentationForNodeWorker(n, start, ignoreActualIndentationRange, /*indentationDelta*/ 0, sourceFile, options); } @@ -114,7 +114,7 @@ namespace ts.formatting { ignoreActualIndentationRange: TextRange, indentationDelta: number, sourceFile: SourceFile, - options: EditorOptions): number { + options: EditorSettings): number { let parent: Node = current.parent; let parentStart: LineAndCharacter; @@ -154,7 +154,7 @@ namespace ts.formatting { // increase indentation if parent node wants its content to be indented and parent and child nodes don't start on the same line if (shouldIndentChildNode(parent, current) && !parentAndChildShareLine) { - indentationDelta += options.IndentSize; + indentationDelta += options.indentSize; } current = parent; @@ -178,7 +178,7 @@ namespace ts.formatting { /* * Function returns Value.Unknown if indentation cannot be determined */ - function getActualIndentationForListItemBeforeComma(commaToken: Node, sourceFile: SourceFile, options: EditorOptions): number { + function getActualIndentationForListItemBeforeComma(commaToken: Node, sourceFile: SourceFile, options: EditorSettings): number { // previous token is comma that separates items in list - find the previous item and try to derive indentation from it const commaItemInfo = findListItemInfo(commaToken); if (commaItemInfo && commaItemInfo.listItemIndex > 0) { @@ -198,7 +198,7 @@ namespace ts.formatting { currentLineAndChar: LineAndCharacter, parentAndChildShareLine: boolean, sourceFile: SourceFile, - options: EditorOptions): number { + options: EditorSettings): number { // actual indentation is used for statements\declarations if one of cases below is true: // - parent is SourceFile - by default immediate children of SourceFile are not indented except when user indents them manually @@ -305,7 +305,7 @@ namespace ts.formatting { return undefined; } - function getActualIndentationForListItem(node: Node, sourceFile: SourceFile, options: EditorOptions): number { + function getActualIndentationForListItem(node: Node, sourceFile: SourceFile, options: EditorSettings): number { const containingList = getContainingList(node, sourceFile); return containingList ? getActualIndentationFromList(containingList) : Value.Unknown; @@ -315,7 +315,7 @@ namespace ts.formatting { } } - function getLineIndentationWhenExpressionIsInMultiLine(node: Node, sourceFile: SourceFile, options: EditorOptions): number { + function getLineIndentationWhenExpressionIsInMultiLine(node: Node, sourceFile: SourceFile, options: EditorSettings): number { // actual indentation should not be used when: // - node is close parenthesis - this is the end of the expression if (node.kind === SyntaxKind.CloseParenToken) { @@ -363,7 +363,7 @@ namespace ts.formatting { } } - function deriveActualIndentationFromList(list: Node[], index: number, sourceFile: SourceFile, options: EditorOptions): number { + function deriveActualIndentationFromList(list: Node[], index: number, sourceFile: SourceFile, options: EditorSettings): number { Debug.assert(index >= 0 && index < list.length); const node = list[index]; @@ -385,7 +385,7 @@ namespace ts.formatting { return Value.Unknown; } - function findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter: LineAndCharacter, sourceFile: SourceFile, options: EditorOptions): number { + function findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter: LineAndCharacter, sourceFile: SourceFile, options: EditorSettings): number { const lineStart = sourceFile.getPositionOfLineAndCharacter(lineAndCharacter.line, 0); return findFirstNonWhitespaceColumn(lineStart, lineStart + lineAndCharacter.character, sourceFile, options); } @@ -397,7 +397,7 @@ namespace ts.formatting { value of 'character' for '$' is 3 value of 'column' for '$' is 6 (assuming that tab size is 4) */ - export function findFirstNonWhitespaceCharacterAndColumn(startPos: number, endPos: number, sourceFile: SourceFile, options: EditorOptions) { + export function findFirstNonWhitespaceCharacterAndColumn(startPos: number, endPos: number, sourceFile: SourceFile, options: EditorSettings) { let character = 0; let column = 0; for (let pos = startPos; pos < endPos; pos++) { @@ -407,7 +407,7 @@ namespace ts.formatting { } if (ch === CharacterCodes.tab) { - column += options.TabSize + (column % options.TabSize); + column += options.tabSize + (column % options.tabSize); } else { column++; @@ -418,7 +418,7 @@ namespace ts.formatting { return { column, character }; } - export function findFirstNonWhitespaceColumn(startPos: number, endPos: number, sourceFile: SourceFile, options: EditorOptions): number { + export function findFirstNonWhitespaceColumn(startPos: number, endPos: number, sourceFile: SourceFile, options: EditorSettings): number { return findFirstNonWhitespaceCharacterAndColumn(startPos, endPos, sourceFile, options).column; } diff --git a/src/services/services.ts b/src/services/services.ts index 9209fc157c2..5e6abd886bb 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1154,11 +1154,11 @@ namespace ts { getOutliningSpans(fileName: string): OutliningSpan[]; getTodoComments(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[]; getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[]; - getIndentationAtPosition(fileName: string, position: number, options: EditorOptions): number; + getIndentationAtPosition(fileName: string, position: number, options: EditorOptions | EditorSettings): number; - getFormattingEditsForRange(fileName: string, start: number, end: number, options: FormatCodeOptions): TextChange[]; - getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions): TextChange[]; - getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions): TextChange[]; + getFormattingEditsForRange(fileName: string, start: number, end: number, options: FormatCodeOptions | FormatCodeSettings): TextChange[]; + getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[]; + getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[]; getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion; @@ -1258,6 +1258,7 @@ namespace ts { containerKind: string; } + /* @deprecated - consider using EditorSettings instead */ export interface EditorOptions { IndentSize: number; TabSize: number; @@ -1266,12 +1267,21 @@ namespace ts { IndentStyle: IndentStyle; } + export interface EditorSettings { + indentSize: number; + tabSize: number; + newLineCharacter: string; + convertTabsToSpaces: boolean; + indentStyle: IndentStyle; + } + export enum IndentStyle { None = 0, Block = 1, Smart = 2, } + /* @deprecated - consider using FormatCodeSettings instead */ export interface FormatCodeOptions extends EditorOptions { InsertSpaceAfterCommaDelimiter: boolean; InsertSpaceAfterSemicolonInForStatements: boolean; @@ -1283,9 +1293,50 @@ namespace ts { InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: boolean; PlaceOpenBraceOnNewLineForFunctions: boolean; PlaceOpenBraceOnNewLineForControlBlocks: boolean; - [s: string]: boolean | number | string; } + export interface FormatCodeSettings extends EditorSettings { + insertSpaceAfterCommaDelimiter: boolean; + insertSpaceAfterSemicolonInForStatements: boolean; + insertSpaceBeforeAndAfterBinaryOperators: boolean; + insertSpaceAfterKeywordsInControlFlowStatements: boolean; + insertSpaceAfterFunctionKeywordForAnonymousFunctions: boolean; + insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: boolean; + insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: boolean; + insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: boolean; + placeOpenBraceOnNewLineForFunctions: boolean; + placeOpenBraceOnNewLineForControlBlocks: boolean; + } + + /* @internal */ + export function toEditorSettings(options: FormatCodeOptions | FormatCodeSettings): FormatCodeSettings; + export function toEditorSettings(options: EditorOptions | EditorSettings): EditorSettings; + export function toEditorSettings(optionsAsMap: Map): Map { + let allPropertiesAreCamelCased = true; + for (const key in optionsAsMap) { + if (hasProperty(optionsAsMap, key) && !isCamelCase(key)) { + allPropertiesAreCamelCased = false; + break; + } + } + if (allPropertiesAreCamelCased) { + return optionsAsMap; + } + const settings: Map = {}; + for (const key in optionsAsMap) { + if (hasProperty(optionsAsMap, key)) { + const newKey = isCamelCase(key) ? key : key.charAt(0).toLowerCase() + key.substr(1); + settings[newKey] = optionsAsMap[key]; + } + } + return settings; + } + + function isCamelCase(s: string) { + return !s.length || s.charAt(0) === s.charAt(0).toLowerCase(); + } + + export interface DefinitionInfo { fileName: string; textSpan: TextSpan; @@ -2949,7 +3000,7 @@ namespace ts { return sourceFile; } - function getRuleProvider(options: FormatCodeOptions) { + function getRuleProvider(options: FormatCodeSettings) { // Ensure rules are initialized and up to date wrt to formatting options if (!ruleProvider) { ruleProvider = new formatting.RulesProvider(); @@ -7632,40 +7683,44 @@ namespace ts { } } - function getIndentationAtPosition(fileName: string, position: number, editorOptions: EditorOptions) { + function getIndentationAtPosition(fileName: string, position: number, optionsOrSettings: EditorOptions | EditorSettings) { let start = new Date().getTime(); + const settings = toEditorSettings(optionsOrSettings); const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); log("getIndentationAtPosition: getCurrentSourceFile: " + (new Date().getTime() - start)); start = new Date().getTime(); - const result = formatting.SmartIndenter.getIndentation(position, sourceFile, editorOptions); + const result = formatting.SmartIndenter.getIndentation(position, sourceFile, settings); log("getIndentationAtPosition: computeIndentation : " + (new Date().getTime() - start)); return result; } - function getFormattingEditsForRange(fileName: string, start: number, end: number, options: FormatCodeOptions): TextChange[] { + function getFormattingEditsForRange(fileName: string, start: number, end: number, optionsOrSettings: FormatCodeOptions | FormatCodeSettings): TextChange[] { + const settings = toEditorSettings(optionsOrSettings); const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - return formatting.formatSelection(start, end, sourceFile, getRuleProvider(options), options); + return formatting.formatSelection(start, end, sourceFile, getRuleProvider(settings), settings); } - function getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions): TextChange[] { + function getFormattingEditsForDocument(fileName: string, optionsOrSettings: FormatCodeOptions | FormatCodeSettings): TextChange[] { + const settings = toEditorSettings(optionsOrSettings); const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - return formatting.formatDocument(sourceFile, getRuleProvider(options), options); + return formatting.formatDocument(sourceFile, getRuleProvider(settings), settings); } - function getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions): TextChange[] { + function getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, optionsOrSettings: FormatCodeOptions | FormatCodeSettings): TextChange[] { + const settings = toEditorSettings(optionsOrSettings); const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); if (key === "}") { - return formatting.formatOnClosingCurly(position, sourceFile, getRuleProvider(options), options); + return formatting.formatOnClosingCurly(position, sourceFile, getRuleProvider(settings), settings); } else if (key === ";") { - return formatting.formatOnSemicolon(position, sourceFile, getRuleProvider(options), options); + return formatting.formatOnSemicolon(position, sourceFile, getRuleProvider(settings), settings); } else if (key === "\n") { - return formatting.formatOnEnter(position, sourceFile, getRuleProvider(options), options); + return formatting.formatOnEnter(position, sourceFile, getRuleProvider(settings), settings); } return []; From f34ba2df0bdd08f4994eddd593500124768be8a6 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 17 Jun 2016 16:22:03 -0700 Subject: [PATCH 039/307] added cleanup message --- src/compiler/program.ts | 7 ++++++- src/compiler/types.ts | 1 + src/server/session.ts | 27 +++++++++++++++++++++++++++ src/services/services.ts | 1 + 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 15afbe18265..47993d14c24 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1151,7 +1151,8 @@ namespace ts { getSymbolCount: () => getDiagnosticsProducingTypeChecker().getSymbolCount(), getTypeCount: () => getDiagnosticsProducingTypeChecker().getTypeCount(), getFileProcessingDiagnostics: () => fileProcessingDiagnostics, - getResolvedTypeReferenceDirectives: () => resolvedTypeReferenceDirectives + getResolvedTypeReferenceDirectives: () => resolvedTypeReferenceDirectives, + dropDiagnosticsProducingTypeChecker }; verifyCompilerOptions(); @@ -1345,6 +1346,10 @@ namespace ts { return diagnosticsProducingTypeChecker || (diagnosticsProducingTypeChecker = createTypeChecker(program, /*produceDiagnostics:*/ true)); } + function dropDiagnosticsProducingTypeChecker() { + diagnosticsProducingTypeChecker = undefined; + } + function getTypeChecker() { return noDiagnosticsTypeChecker || (noDiagnosticsTypeChecker = createTypeChecker(program, /*produceDiagnostics:*/ false)); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index db34bb7c6e8..d55541dc44f 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1725,6 +1725,7 @@ namespace ts { // For testing purposes only. Should not be used by any other consumers (including the // language service). /* @internal */ getDiagnosticsProducingTypeChecker(): TypeChecker; + /* @internal */ dropDiagnosticsProducingTypeChecker(): void; /* @internal */ getClassifiableNames(): Map; diff --git a/src/server/session.ts b/src/server/session.ts index 03fee122d39..d8a4ac86516 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -142,6 +142,7 @@ namespace ts.server { export const SynchronizeProjectList = "synchronizeProjectList"; export const ApplyChangedToOpenFiles = "applyChangedToOpenFiles"; export const EncodedSemanticClassificationsFull = "encodedSemanticClassifications-full"; + export const Cleanup = "cleanup"; } namespace Errors { @@ -331,6 +332,28 @@ namespace ts.server { } } + private cleanProjects(caption: string, projects: Project[]) { + if (!projects) { + return; + } + this.projectService.log(`cleaning ${caption}`); + for (const p of projects) { + p.languageService.cleanupSemanticCache(); + } + } + + private cleanup() { + this.cleanProjects("inferred projects", this.projectService.inferredProjects); + this.cleanProjects("configured projects", this.projectService.configuredProjects); + this.cleanProjects("external projects", this.projectService.externalProjects); + if (typeof global !== "undefined" && global.gc) { + this.projectService.log(`global.gc()`); + global.gc(); + global.gc(); + global.gc(); + } + } + private getEncodedSemanticClassifications(args: protocol.FileSpanRequestArgs) { const file = normalizePath(args.file); const project = this.projectService.getProjectForFile(file); @@ -1248,6 +1271,10 @@ namespace ts.server { [CommandNames.EncodedSemanticClassificationsFull]: (request: protocol.FileSpanRequest) => { return this.requiredResponse(this.getEncodedSemanticClassifications(request.arguments)); }, + [CommandNames.Cleanup]: (request: protocol.Request) => { + this.cleanup(); + return this.requiredResponse(true); + }, [CommandNames.Geterr]: (request: protocol.Request) => { const geterrArgs = request.arguments; return { response: this.getDiagnostics(geterrArgs.delay, geterrArgs.files), responseRequired: false }; diff --git a/src/services/services.ts b/src/services/services.ts index 5e6abd886bb..ce54221fafb 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -3212,6 +3212,7 @@ namespace ts { function cleanupSemanticCache(): void { // TODO: Should we jettison the program (or it's type checker) here? + program.dropDiagnosticsProducingTypeChecker(); } function dispose(): void { From bd28b9a46ddbb33eedff6c8113a21a42564869bd Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 17 Jun 2016 23:31:15 -0700 Subject: [PATCH 040/307] added outlining spans --- src/server/session.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/server/session.ts b/src/server/session.ts index d8a4ac86516..965844f9b8b 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -143,6 +143,7 @@ namespace ts.server { export const ApplyChangedToOpenFiles = "applyChangedToOpenFiles"; export const EncodedSemanticClassificationsFull = "encodedSemanticClassifications-full"; export const Cleanup = "cleanup"; + export const OutliningSpans = "outliningSpans"; } namespace Errors { @@ -689,6 +690,15 @@ namespace ts.server { return args.position !== undefined ? args.position : scriptInfo.lineOffsetToPosition(args.line, args.offset); } + private getOutliningSpans(args: protocol.FileRequestArgs) { + const file = ts.normalizePath(args.file); + const project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + return project.languageService.getOutliningSpans(file); + } + private getQuickInfoWorker(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.QuickInfoResponseBody | QuickInfo { const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); @@ -1233,6 +1243,9 @@ namespace ts.server { [CommandNames.QuickinfoFull]: (request: protocol.QuickInfoRequest) => { return this.requiredResponse(this.getQuickInfoWorker(request.arguments, /*simplifiedResult*/ false)); }, + [CommandNames.OutliningSpans]: (request: protocol.FileRequest) => { + return this.requiredResponse(this.getOutliningSpans(request.arguments)); + }, [CommandNames.Format]: (request: protocol.Request) => { const formatArgs = request.arguments; return this.requiredResponse(this.getFormattingEditsForRange(formatArgs.line, formatArgs.offset, formatArgs.endLine, formatArgs.endOffset, formatArgs.file)); From 4d9213a3377d53fabdcdd78af3267e57d2c80014 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 17 Jun 2016 23:54:59 -0700 Subject: [PATCH 041/307] added todo comments --- src/server/protocol.d.ts | 8 ++++++++ src/server/session.ts | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 1d77aeba2ac..2fdf7723265 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -98,6 +98,14 @@ declare namespace ts.server.protocol { projectFileName?: string; } + export interface TodoCommentRequest extends FileRequest { + arguments: TodoCommentRequestArgs; + } + + export interface TodoCommentRequestArgs extends FileRequestArgs { + descriptors: TodoCommentDescriptor[]; + } + /** * Arguments for ProjectInfoRequest request. */ diff --git a/src/server/session.ts b/src/server/session.ts index 965844f9b8b..1a0a46c6b9e 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -144,6 +144,7 @@ namespace ts.server { export const EncodedSemanticClassificationsFull = "encodedSemanticClassifications-full"; export const Cleanup = "cleanup"; export const OutliningSpans = "outliningSpans"; + export const TodoComments = "todoComments"; } namespace Errors { @@ -699,6 +700,15 @@ namespace ts.server { return project.languageService.getOutliningSpans(file); } + private getTodoComments(args: protocol.TodoCommentRequestArgs) { + const file = ts.normalizePath(args.file); + const project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + return project.languageService.getTodoComments(file, args.descriptors); + } + private getQuickInfoWorker(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.QuickInfoResponseBody | QuickInfo { const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); @@ -1246,6 +1256,9 @@ namespace ts.server { [CommandNames.OutliningSpans]: (request: protocol.FileRequest) => { return this.requiredResponse(this.getOutliningSpans(request.arguments)); }, + [CommandNames.TodoComments]: (request: protocol.TodoCommentRequest) => { + return this.requiredResponse(this.getTodoComments(request.arguments)); + }, [CommandNames.Format]: (request: protocol.Request) => { const formatArgs = request.arguments; return this.requiredResponse(this.getFormattingEditsForRange(formatArgs.line, formatArgs.offset, formatArgs.endLine, formatArgs.endOffset, formatArgs.file)); From f2d55874d997d92abacca309e39ec64270152759 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Sat, 18 Jun 2016 20:43:53 -0700 Subject: [PATCH 042/307] added indentation --- src/server/protocol.d.ts | 8 ++++++++ src/server/session.ts | 26 +++++++++++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 2fdf7723265..083fde39824 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -106,6 +106,14 @@ declare namespace ts.server.protocol { descriptors: TodoCommentDescriptor[]; } + export interface IndentationRequest extends FileLocationRequest { + arguments: IndentationRequestArgs; + } + + export interface IndentationRequestArgs extends FileLocationRequestArgs { + options: EditorSettings; + } + /** * Arguments for ProjectInfoRequest request. */ diff --git a/src/server/session.ts b/src/server/session.ts index 1a0a46c6b9e..992ee9f506e 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -145,6 +145,7 @@ namespace ts.server { export const Cleanup = "cleanup"; export const OutliningSpans = "outliningSpans"; export const TodoComments = "todoComments"; + export const Indentation = "indentation"; } namespace Errors { @@ -691,24 +692,32 @@ namespace ts.server { return args.position !== undefined ? args.position : scriptInfo.lineOffsetToPosition(args.line, args.offset); } - private getOutliningSpans(args: protocol.FileRequestArgs) { - const file = ts.normalizePath(args.file); + private getFileAndProject(fileName: string) { + const file = ts.normalizePath(fileName); const project = this.projectService.getProjectForFile(file); if (!project) { throw Errors.NoProject; } + return { file, project }; + } + + private getOutliningSpans(args: protocol.FileRequestArgs) { + const { file, project } = this.getFileAndProject(args.file); return project.languageService.getOutliningSpans(file); } private getTodoComments(args: protocol.TodoCommentRequestArgs) { - const file = ts.normalizePath(args.file); - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } + const { file, project } = this.getFileAndProject(args.file); return project.languageService.getTodoComments(file, args.descriptors); } + private getIndentation(args: protocol.IndentationRequestArgs) { + const { file, project } = this.getFileAndProject(args.file); + const position = this.getPosition(args, project.getScriptInfo(file)); + const indentation = project.languageService.getIndentationAtPosition(file, position, args.options); + return { position, indentation }; + } + private getQuickInfoWorker(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.QuickInfoResponseBody | QuickInfo { const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); @@ -1259,6 +1268,9 @@ namespace ts.server { [CommandNames.TodoComments]: (request: protocol.TodoCommentRequest) => { return this.requiredResponse(this.getTodoComments(request.arguments)); }, + [CommandNames.Indentation]: (request: protocol.IndentationRequest) => { + return this.requiredResponse(this.getIndentation(request.arguments)); + }, [CommandNames.Format]: (request: protocol.Request) => { const formatArgs = request.arguments; return this.requiredResponse(this.getFormattingEditsForRange(formatArgs.line, formatArgs.offset, formatArgs.endLine, formatArgs.endOffset, formatArgs.file)); From 12e593825f75b60ccc6418a793310f4263125fa0 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Sat, 18 Jun 2016 22:34:00 -0700 Subject: [PATCH 043/307] brace matching --- src/server/session.ts | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 992ee9f506e..dfadcf66d65 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -101,6 +101,7 @@ namespace ts.server { export namespace CommandNames { export const Brace = "brace"; + export const BraceFull = "brace-full"; export const Change = "change"; export const Close = "close"; export const Completions = "completions"; @@ -1108,26 +1109,26 @@ namespace ts.server { } } - private getBraceMatching(line: number, offset: number, fileName: string): protocol.TextSpan[] { - const file = ts.normalizePath(fileName); + private getBraceMatching(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.TextSpan[] | TextSpan[] { + const { file, project } = this.getFileAndProject(args.file); - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } - - const scriptInfo = project.getScriptInfo(fileName); - const position = scriptInfo.lineOffsetToPosition(line, offset); + const scriptInfo = project.getScriptInfo(file); + const position = this.getPosition(args, scriptInfo); const spans = project.languageService.getBraceMatchingAtPosition(file, position); if (!spans) { return undefined; } + if (simplifiedResult) { - return spans.map(span => ({ - start: scriptInfo.positionToLineOffset(span.start), - end: scriptInfo.positionToLineOffset(span.start + span.length) - })); + return spans.map(span => ({ + start: scriptInfo.positionToLineOffset(span.start), + end: scriptInfo.positionToLineOffset(span.start + span.length) + })); + } + else { + return spans; + } } getDiagnosticsForProject(delay: number, fileName: string) { @@ -1352,9 +1353,11 @@ namespace ts.server { const navtoArgs = request.arguments; return { response: this.getNavigateToItems(navtoArgs.searchValue, navtoArgs.file, navtoArgs.maxResultCount), responseRequired: true }; }, - [CommandNames.Brace]: (request: protocol.Request) => { - const braceArguments = request.arguments; - return { response: this.getBraceMatching(braceArguments.line, braceArguments.offset, braceArguments.file), responseRequired: true }; + [CommandNames.Brace]: (request: protocol.FileLocationRequest) => { + return this.requiredResponse(this.getBraceMatching(request.arguments, /*simplifiedResult*/ true)); + }, + [CommandNames.BraceFull]: (request: protocol.FileLocationRequest) => { + return this.requiredResponse(this.getBraceMatching(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.NavBar]: (request: protocol.Request) => { const navBarArgs = request.arguments; From 0ab52bcb824ef9b9d1c1bf3d6c56ac5702c5ea40 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Sat, 18 Jun 2016 22:35:29 -0700 Subject: [PATCH 044/307] linter --- src/server/session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/session.ts b/src/server/session.ts index dfadcf66d65..da4373a82eb 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1313,7 +1313,7 @@ namespace ts.server { [CommandNames.Cleanup]: (request: protocol.Request) => { this.cleanup(); return this.requiredResponse(true); - }, + }, [CommandNames.Geterr]: (request: protocol.Request) => { const geterrArgs = request.arguments; return { response: this.getDiagnostics(geterrArgs.delay, geterrArgs.files), responseRequired: false }; From 061bb0f9e510c143f5722261a41ac46f9e1ab7a8 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Sat, 18 Jun 2016 22:52:31 -0700 Subject: [PATCH 045/307] navbar --- src/server/session.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index da4373a82eb..bdbd82dabbf 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -120,6 +120,7 @@ namespace ts.server { export const GeterrForProject = "geterrForProject"; export const SemanticDiagnosticsFull = "semanticDiagnostics-full"; export const NavBar = "navbar"; + export const NavBarFull = "navbar-full"; export const Navto = "navto"; export const Occurrences = "occurrences"; export const DocumentHighlights = "documentHighlights"; @@ -1036,19 +1037,17 @@ namespace ts.server { })); } - private getNavigationBarItems(fileName: string): protocol.NavigationBarItem[] { - const file = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } + private getNavigationBarItems(fileName: string, simplifiedResult: boolean): protocol.NavigationBarItem[] | NavigationBarItem[] { + const { file, project } = this.getFileAndProject(fileName); const items = project.languageService.getNavigationBarItems(file); if (!items) { return undefined; } - return this.decorateNavigationBarItem(project, fileName, items); + return simplifiedResult + ? this.decorateNavigationBarItem(project, fileName, items) + : items; } private getNavigateToItems(searchValue: string, fileName: string, maxResultCount?: number): protocol.NavtoItem[] { @@ -1359,9 +1358,11 @@ namespace ts.server { [CommandNames.BraceFull]: (request: protocol.FileLocationRequest) => { return this.requiredResponse(this.getBraceMatching(request.arguments, /*simplifiedResult*/ false)); }, - [CommandNames.NavBar]: (request: protocol.Request) => { - const navBarArgs = request.arguments; - return { response: this.getNavigationBarItems(navBarArgs.file), responseRequired: true }; + [CommandNames.NavBar]: (request: protocol.FileRequest) => { + return this.requiredResponse(this.getNavigationBarItems(request.arguments.file, /*simplifiedResult*/ true)); + }, + [CommandNames.NavBarFull]: (request: protocol.FileRequest) => { + return this.requiredResponse(this.getNavigationBarItems(request.arguments.file, /*simplifiedResult*/ false)); }, [CommandNames.Occurrences]: (request: protocol.Request) => { const { line, offset, file: fileName } = request.arguments; From cd1efd5109fb37b6f345fa6a675941182d40c410 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Sun, 19 Jun 2016 15:16:21 -0700 Subject: [PATCH 046/307] docCommentTemplate --- src/server/session.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/server/session.ts b/src/server/session.ts index bdbd82dabbf..42ff097f31c 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -148,6 +148,7 @@ namespace ts.server { export const OutliningSpans = "outliningSpans"; export const TodoComments = "todoComments"; export const Indentation = "indentation"; + export const DocCommentTemplate = "docCommentTemplate"; } namespace Errors { @@ -713,6 +714,13 @@ namespace ts.server { return project.languageService.getTodoComments(file, args.descriptors); } + private getDocCommentTemplate(args: protocol.FileLocationRequestArgs) { + const { file, project } = this.getFileAndProject(args.file); + const scriptInfo = project.getScriptInfo(file); + const position = this.getPosition(args, scriptInfo); + return project.languageService.getDocCommentTemplateAtPosition(file, position); + } + private getIndentation(args: protocol.IndentationRequestArgs) { const { file, project } = this.getFileAndProject(args.file); const position = this.getPosition(args, project.getScriptInfo(file)); @@ -1271,6 +1279,9 @@ namespace ts.server { [CommandNames.Indentation]: (request: protocol.IndentationRequest) => { return this.requiredResponse(this.getIndentation(request.arguments)); }, + [CommandNames.DocCommentTemplate]: (request: protocol.FileLocationRequest) => { + return this.requiredResponse(this.getDocCommentTemplate(request.arguments)); + }, [CommandNames.Format]: (request: protocol.Request) => { const formatArgs = request.arguments; return this.requiredResponse(this.getFormattingEditsForRange(formatArgs.line, formatArgs.offset, formatArgs.endLine, formatArgs.endOffset, formatArgs.file)); From 24ace87f225b5b62e62c8ef09a7fe361cba92611 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Sun, 19 Jun 2016 15:38:16 -0700 Subject: [PATCH 047/307] syntactic/compiler options diagnostics --- src/server/protocol.d.ts | 8 +++++++ src/server/session.ts | 49 ++++++++++++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 083fde39824..6e3cba3044f 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -131,6 +131,14 @@ declare namespace ts.server.protocol { arguments: ProjectInfoRequestArgs; } + export interface ProjectRequest extends Request { + arguments: ProjectRequestArgs; + } + + export interface ProjectRequestArgs { + projectFileName: string; + } + /** * Response message body for "projectInfo" request */ diff --git a/src/server/session.ts b/src/server/session.ts index 42ff097f31c..f245b0da1cb 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -149,6 +149,8 @@ namespace ts.server { export const TodoComments = "todoComments"; export const Indentation = "indentation"; export const DocCommentTemplate = "docCommentTemplate"; + export const SyntacticDiagnosticsFull = "syntacticDiagnostics-full"; + export const CompilerOptionsDiagnosticsFull = "compilerOptionsDiagnostics-full"; } namespace Errors { @@ -369,25 +371,46 @@ namespace ts.server { return project.languageService.getEncodedSemanticClassifications(file, args); } - private getSemanticDiagnostics(args: protocol.FileRequestArgs): protocol.DiagnosticWithLinePosition[] { - const file = normalizePath(args.file); - const project = (args.projectFileName && this.projectService.getProject(normalizePath(args.projectFileName))) || this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } - const scriptInfo = project.getScriptInfo(file); - const diagnostics = project.languageService.getSemanticDiagnostics(file); + private getProject(projectFileName: string) { + return projectFileName && this.projectService.getProject(normalizePath(projectFileName)); + } + + private getCompilerOptionsDiagnostics(args: protocol.ProjectRequestArgs) { + const project = this.getProject(args.projectFileName); + return this.convertDiagnostics(project.languageService.getCompilerOptionsDiagnostics(), /*scriptInfo*/ undefined); + } + + private convertDiagnostics(diagnostics: Diagnostic[], scriptInfo: ScriptInfo) { return diagnostics.map(d => { message: flattenDiagnosticMessageText(d.messageText, this.host.newLine), start: d.start, length: d.length, category: DiagnosticCategory[d.category].toLowerCase(), code: d.code, - startLocation: this.getLocation(d.start, scriptInfo), - endLocation: this.getLocation(d.start + d.length, scriptInfo) + startLocation: scriptInfo && this.getLocation(d.start, scriptInfo), + endLocation: scriptInfo && this.getLocation(d.start + d.length, scriptInfo) }); } + private getDiagnosticsWorker(args: protocol.FileRequestArgs, selector: (project: Project, file: string) => Diagnostic[]) { + const file = normalizePath(args.file); + const project = this.getProject(args.projectFileName) || this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + const scriptInfo = project.getScriptInfo(file); + const diagnostics = selector(project, file); + return this.convertDiagnostics(diagnostics, scriptInfo); + } + + private getSyntacticDiagnostics(args: protocol.FileRequestArgs): protocol.DiagnosticWithLinePosition[] { + return this.getDiagnosticsWorker(args, (project, file) => project.languageService.getSyntacticDiagnostics(file)); + } + + private getSemanticDiagnostics(args: protocol.FileRequestArgs): protocol.DiagnosticWithLinePosition[] { + return this.getDiagnosticsWorker(args, (project, file) => project.languageService.getSemanticDiagnostics(file)); + } + private getDefinition(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.FileSpan[] | DefinitionInfo[] { const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); @@ -1317,6 +1340,12 @@ namespace ts.server { [CommandNames.SemanticDiagnosticsFull]: (request: protocol.FileRequest) => { return this.requiredResponse(this.getSemanticDiagnostics(request.arguments)); }, + [CommandNames.SyntacticDiagnosticsFull]: (request: protocol.FileRequest) => { + return this.requiredResponse(this.getSyntacticDiagnostics(request.arguments)); + }, + [CommandNames.CompilerOptionsDiagnosticsFull]: (request: protocol.ProjectRequest) => { + return this.requiredResponse(this.getCompilerOptionsDiagnostics(request.arguments)); + }, [CommandNames.EncodedSemanticClassificationsFull]: (request: protocol.FileSpanRequest) => { return this.requiredResponse(this.getEncodedSemanticClassifications(request.arguments)); }, From 863c65b901158ed47638447d5ff124a7f54557ec Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Sun, 19 Jun 2016 15:48:10 -0700 Subject: [PATCH 048/307] brace completion --- src/server/protocol.d.ts | 8 ++++++++ src/server/session.ts | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 6e3cba3044f..5aa0ac856b9 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -289,6 +289,14 @@ declare namespace ts.server.protocol { body?: FileSpan[]; } + export interface BraceCompletionRequest extends FileLocationRequest { + arguments: BraceCompletionRequestArgs; + } + + export interface BraceCompletionRequestArgs extends FileLocationRequestArgs { + openingBrace: string; + } + /** * Get occurrences request; value of command field is * "occurrences". Return response giving spans that are relevant diff --git a/src/server/session.ts b/src/server/session.ts index f245b0da1cb..e20811a0c1b 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -102,6 +102,7 @@ namespace ts.server { export namespace CommandNames { export const Brace = "brace"; export const BraceFull = "brace-full"; + export const BraceCompletion = "braceCompletion"; export const Change = "change"; export const Close = "close"; export const Completions = "completions"; @@ -751,6 +752,12 @@ namespace ts.server { return { position, indentation }; } + private isValidBraceCompletion(args: protocol.BraceCompletionRequestArgs) { + const { file, project } = this.getFileAndProject(args.file); + const position = this.getPosition(args, project.getScriptInfo(file)); + return project.languageService.isValidBraceCompletionAtPostion(file, position, args.openingBrace.charCodeAt(0)); + } + private getQuickInfoWorker(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.QuickInfoResponseBody | QuickInfo { const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); @@ -1302,6 +1309,9 @@ namespace ts.server { [CommandNames.Indentation]: (request: protocol.IndentationRequest) => { return this.requiredResponse(this.getIndentation(request.arguments)); }, + [CommandNames.BraceCompletion]: (request: protocol.BraceCompletionRequest) => { + return this.requiredResponse(this.isValidBraceCompletion(request.arguments)); + }, [CommandNames.DocCommentTemplate]: (request: protocol.FileLocationRequest) => { return this.requiredResponse(this.getDocCommentTemplate(request.arguments)); }, From 9c124b3c0d85879506c307e700c10aeb63a68640 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Sun, 19 Jun 2016 15:57:02 -0700 Subject: [PATCH 049/307] nameOrDottedSpan --- src/server/session.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/server/session.ts b/src/server/session.ts index e20811a0c1b..4bd6f26d482 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -152,6 +152,7 @@ namespace ts.server { export const DocCommentTemplate = "docCommentTemplate"; export const SyntacticDiagnosticsFull = "syntacticDiagnostics-full"; export const CompilerOptionsDiagnosticsFull = "compilerOptionsDiagnostics-full"; + export const NameOrDottedNameSpan = "nameOrDottedNameSpan"; } namespace Errors { @@ -752,6 +753,12 @@ namespace ts.server { return { position, indentation }; } + private getNameOrDottedNameSpan(args: protocol.FileLocationRequestArgs) { + const { file, project } = this.getFileAndProject(args.file); + const position = this.getPosition(args, project.getScriptInfo(file)); + return project.languageService.getNameOrDottedNameSpan(file, position, position); + } + private isValidBraceCompletion(args: protocol.BraceCompletionRequestArgs) { const { file, project } = this.getFileAndProject(args.file); const position = this.getPosition(args, project.getScriptInfo(file)); @@ -1309,6 +1316,9 @@ namespace ts.server { [CommandNames.Indentation]: (request: protocol.IndentationRequest) => { return this.requiredResponse(this.getIndentation(request.arguments)); }, + [CommandNames.NameOrDottedNameSpan]: (request: protocol.FileLocationRequest) => { + return this.requiredResponse(this.getNameOrDottedNameSpan(request.arguments)); + }, [CommandNames.BraceCompletion]: (request: protocol.BraceCompletionRequest) => { return this.requiredResponse(this.isValidBraceCompletion(request.arguments)); }, From a86dd00ec9aaf40c65e0e5fd3e75a55c1b3ce006 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Sun, 19 Jun 2016 16:10:00 -0700 Subject: [PATCH 050/307] breakpointStatement --- src/server/session.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/server/session.ts b/src/server/session.ts index 4bd6f26d482..b9fc872450d 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -153,6 +153,7 @@ namespace ts.server { export const SyntacticDiagnosticsFull = "syntacticDiagnostics-full"; export const CompilerOptionsDiagnosticsFull = "compilerOptionsDiagnostics-full"; export const NameOrDottedNameSpan = "nameOrDottedNameSpan"; + export const BreakpointStatement = "breakpointStatement"; } namespace Errors { @@ -753,6 +754,12 @@ namespace ts.server { return { position, indentation }; } + private getBreakpointStatement(args: protocol.FileLocationRequestArgs) { + const { file, project } = this.getFileAndProject(args.file); + const position = this.getPosition(args, project.getScriptInfo(file)); + return project.languageService.getBreakpointStatementAtPosition(file, position); + } + private getNameOrDottedNameSpan(args: protocol.FileLocationRequestArgs) { const { file, project } = this.getFileAndProject(args.file); const position = this.getPosition(args, project.getScriptInfo(file)); @@ -1319,6 +1326,9 @@ namespace ts.server { [CommandNames.NameOrDottedNameSpan]: (request: protocol.FileLocationRequest) => { return this.requiredResponse(this.getNameOrDottedNameSpan(request.arguments)); }, + [CommandNames.BreakpointStatement]: (request: protocol.FileLocationRequest) => { + return this.requiredResponse(this.getBreakpointStatement(request.arguments)); + }, [CommandNames.BraceCompletion]: (request: protocol.BraceCompletionRequest) => { return this.requiredResponse(this.isValidBraceCompletion(request.arguments)); }, From 5a543a610f13e54be4fc8911d65563d568a97dcb Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Sun, 19 Jun 2016 20:33:40 -0700 Subject: [PATCH 051/307] navto --- src/compiler/core.ts | 1 + src/server/editorServices.ts | 2 +- src/server/session.ts | 132 ++++++++++++++++++++++------------- 3 files changed, 87 insertions(+), 48 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 8a2040a83df..ddfc91aad32 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -156,6 +156,7 @@ namespace ts { return array1.concat(array2); } + // TODO: fixme (N^2) - add optional comparer so collection can be sorted before deduplication. export function deduplicate(array: T[], areEqual?: (a: T, b: T) => boolean): T[] { let result: T[]; if (array) { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index bfbee344b7a..0cefde1e7b8 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -684,7 +684,7 @@ namespace ts.server { // TODO: fixme return this.inferredProjects.length ? this.inferredProjects[0] : undefined; } - return this.findExternalProjectByProjectFileName(projectFileName) || this.findConfiguredProjectByConfigFile(projectFileName); + return this.findExternalProjectByProjectFileName(projectFileName) || this.findConfiguredProjectByConfigFile(normalizePath(projectFileName)); } getFormatCodeOptions(file?: string) { diff --git a/src/server/session.ts b/src/server/session.ts index b9fc872450d..667e0f9639f 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -123,6 +123,7 @@ namespace ts.server { export const NavBar = "navbar"; export const NavBarFull = "navbar-full"; export const Navto = "navto"; + export const NavtoFull = "navto-full"; export const Occurrences = "occurrences"; export const DocumentHighlights = "documentHighlights"; export const DocumentHighlightsFull = "documentHighlights-full"; @@ -375,7 +376,7 @@ namespace ts.server { } private getProject(projectFileName: string) { - return projectFileName && this.projectService.getProject(normalizePath(projectFileName)); + return projectFileName && this.projectService.getProject(projectFileName); } private getCompilerOptionsDiagnostics(args: protocol.ProjectRequestArgs) { @@ -1102,53 +1103,88 @@ namespace ts.server { : items; } - private getNavigateToItems(searchValue: string, fileName: string, maxResultCount?: number): protocol.NavtoItem[] { - const file = ts.normalizePath(fileName); - const info = this.projectService.getScriptInfo(file); - const projects = this.projectService.findReferencingProjects(info); - const defaultProject = projects[0]; - if (!defaultProject) { + private getNavigateToItems(args: protocol.NavtoRequestArgs, simplifiedResult: boolean): protocol.NavtoItem[] | NavigateToItem[] { + let projects: Project[]; + if (args.projectFileName) { + const project = this.getProject(args.projectFileName); + if (project) { + projects = [project]; + } + } + else { + const file = normalizePath(args.file); + const info = this.projectService.getScriptInfo(file); + projects = this.projectService.findReferencingProjects(info); + } + if (!projects || !projects.length) { throw Errors.NoProject; } - const allNavToItems = combineProjectOutput( - projects, - (project: Project) => { - const navItems = project.languageService.getNavigateToItems(searchValue, maxResultCount); - if (!navItems) { - return []; - } + if (simplifiedResult) { + return combineProjectOutput( + projects, + project => { + const navItems = project.languageService.getNavigateToItems(args.searchValue, args.maxResultCount); + if (!navItems) { + return []; + } - return navItems.map((navItem) => { - const scriptInfo = project.getScriptInfo(navItem.fileName); - const start = scriptInfo.positionToLineOffset(navItem.textSpan.start); - const end = scriptInfo.positionToLineOffset(ts.textSpanEnd(navItem.textSpan)); - const bakedItem: protocol.NavtoItem = { - name: navItem.name, - kind: navItem.kind, - file: navItem.fileName, - start: start, - end: end, - }; - if (navItem.kindModifiers && (navItem.kindModifiers !== "")) { - bakedItem.kindModifiers = navItem.kindModifiers; - } - if (navItem.matchKind !== "none") { - bakedItem.matchKind = navItem.matchKind; - } - if (navItem.containerName && (navItem.containerName.length > 0)) { - bakedItem.containerName = navItem.containerName; - } - if (navItem.containerKind && (navItem.containerKind.length > 0)) { - bakedItem.containerKind = navItem.containerKind; - } - return bakedItem; - }); - }, - /*comparer*/ undefined, - areNavToItemsForTheSameLocation - ); - return allNavToItems; + return navItems.map((navItem) => { + const scriptInfo = project.getScriptInfo(navItem.fileName); + const start = scriptInfo.positionToLineOffset(navItem.textSpan.start); + const end = scriptInfo.positionToLineOffset(ts.textSpanEnd(navItem.textSpan)); + const bakedItem: protocol.NavtoItem = { + name: navItem.name, + kind: navItem.kind, + file: navItem.fileName, + start: start, + end: end, + }; + if (navItem.kindModifiers && (navItem.kindModifiers !== "")) { + bakedItem.kindModifiers = navItem.kindModifiers; + } + if (navItem.matchKind !== "none") { + bakedItem.matchKind = navItem.matchKind; + } + if (navItem.containerName && (navItem.containerName.length > 0)) { + bakedItem.containerName = navItem.containerName; + } + if (navItem.containerKind && (navItem.containerKind.length > 0)) { + bakedItem.containerKind = navItem.containerKind; + } + return bakedItem; + }); + }, + /*comparer*/ undefined, + areNavToItemsForTheSameLocation + ); + } + else { + return combineProjectOutput( + projects, + project => project.languageService.getNavigateToItems(args.searchValue, args.maxResultCount), + /*comparer*/ undefined, + navigateToItemIsEqualTo) + } + + function navigateToItemIsEqualTo(a: NavigateToItem, b: NavigateToItem): boolean { + if (a === b) { + return true; + } + if (!a || !b) { + return false; + } + return a.containerKind === b.containerKind && + a.containerName === b.containerName && + a.fileName === b.fileName && + a.isCaseSensitive === b.isCaseSensitive && + a.kind === b.kind && + a.kindModifiers === b.containerName && + a.matchKind === b.matchKind && + a.name === b.name && + a.textSpan.start === b.textSpan.start && + a.textSpan.length === b.textSpan.length; + } function areNavToItemsForTheSameLocation(a: protocol.NavtoItem, b: protocol.NavtoItem) { if (a && b) { @@ -1418,9 +1454,11 @@ namespace ts.server { this.closeClientFile(closeArgs.file); return { responseRequired: false }; }, - [CommandNames.Navto]: (request: protocol.Request) => { - const navtoArgs = request.arguments; - return { response: this.getNavigateToItems(navtoArgs.searchValue, navtoArgs.file, navtoArgs.maxResultCount), responseRequired: true }; + [CommandNames.Navto]: (request: protocol.NavtoRequest) => { + return this.requiredResponse(this.getNavigateToItems(request.arguments, /*simplifiedResult*/ true)); + }, + [CommandNames.NavtoFull]: (request: protocol.NavtoRequest) => { + return this.requiredResponse(this.getNavigateToItems(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.Brace]: (request: protocol.FileLocationRequest) => { return this.requiredResponse(this.getBraceMatching(request.arguments, /*simplifiedResult*/ true)); From bfb2957254cd5070a9ab2e73e26ac988e33a472a Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Sun, 19 Jun 2016 23:41:49 -0700 Subject: [PATCH 052/307] findReferences --- src/server/session.ts | 123 +++++++++++++++++++++++++----------------- 1 file changed, 73 insertions(+), 50 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 667e0f9639f..021779e9f93 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -131,6 +131,7 @@ namespace ts.server { export const Quickinfo = "quickinfo"; export const QuickinfoFull = "quickinfo-full"; export const References = "references"; + export const ReferencesFull = "references-full"; export const Reload = "reload"; export const Rename = "rename"; export const Saveto = "saveto"; @@ -642,59 +643,79 @@ namespace ts.server { } } - private getReferences(line: number, offset: number, fileName: string): protocol.ReferencesResponseBody { - const file = ts.normalizePath(fileName); - const info = this.projectService.getScriptInfo(file); - const projects = this.projectService.findReferencingProjects(info); - if (!projects.length) { + private getReferences(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.ReferencesResponseBody | ReferencedSymbol[] { + const file = ts.normalizePath(args.file); + let projects: Project[]; + if (args.projectFileName) { + const project = this.getProject(args.projectFileName); + if (project) { + projects = [project]; + } + } + else { + const info = this.projectService.getScriptInfo(file); + projects = this.projectService.findReferencingProjects(info); + } + if (!projects || !projects.length) { throw Errors.NoProject; } const defaultProject = projects[0]; const scriptInfo = defaultProject.getScriptInfo(file); - const position = scriptInfo.lineOffsetToPosition(line, offset); - const nameInfo = defaultProject.languageService.getQuickInfoAtPosition(file, position); - if (!nameInfo) { - return undefined; + const position = this.getPosition(args, scriptInfo); + if (simplifiedResult) { + const nameInfo = defaultProject.languageService.getQuickInfoAtPosition(file, position); + if (!nameInfo) { + return undefined; + } + + const displayString = ts.displayPartsToString(nameInfo.displayParts); + const nameSpan = nameInfo.textSpan; + const nameColStart = scriptInfo.positionToLineOffset(nameSpan.start).offset; + const nameText = scriptInfo.snap().getText(nameSpan.start, ts.textSpanEnd(nameSpan)); + const refs = combineProjectOutput( + projects, + (project: Project) => { + const references = project.languageService.getReferencesAtPosition(file, position); + if (!references) { + return []; + } + + return references.map(ref => { + const refScriptInfo = project.getScriptInfo(ref.fileName); + const start = refScriptInfo.positionToLineOffset(ref.textSpan.start); + const refLineSpan = refScriptInfo.lineToTextSpan(start.line - 1); + const lineText = refScriptInfo.snap().getText(refLineSpan.start, ts.textSpanEnd(refLineSpan)).replace(/\r|\n/g, ""); + return { + file: ref.fileName, + start: start, + lineText: lineText, + end: refScriptInfo.positionToLineOffset(ts.textSpanEnd(ref.textSpan)), + isWriteAccess: ref.isWriteAccess, + isDefinition: ref.isDefinition + }; + }); + }, + compareFileStart, + areReferencesResponseItemsForTheSameLocation + ); + + return { + refs, + symbolName: nameText, + symbolStartOffset: nameColStart, + symbolDisplayString: displayString + }; + } + else { + return combineProjectOutput( + projects, + project => project.languageService.findReferences(file, position), + undefined, + // TODO: fixme + undefined + ) } - - const displayString = ts.displayPartsToString(nameInfo.displayParts); - const nameSpan = nameInfo.textSpan; - const nameColStart = scriptInfo.positionToLineOffset(nameSpan.start).offset; - const nameText = scriptInfo.snap().getText(nameSpan.start, ts.textSpanEnd(nameSpan)); - const refs = combineProjectOutput( - projects, - (project: Project) => { - const references = project.languageService.getReferencesAtPosition(file, position); - if (!references) { - return []; - } - - return references.map(ref => { - const refScriptInfo = project.getScriptInfo(ref.fileName); - const start = refScriptInfo.positionToLineOffset(ref.textSpan.start); - const refLineSpan = refScriptInfo.lineToTextSpan(start.line - 1); - const lineText = refScriptInfo.snap().getText(refLineSpan.start, ts.textSpanEnd(refLineSpan)).replace(/\r|\n/g, ""); - return { - file: ref.fileName, - start: start, - lineText: lineText, - end: refScriptInfo.positionToLineOffset(ts.textSpanEnd(ref.textSpan)), - isWriteAccess: ref.isWriteAccess, - isDefinition: ref.isDefinition - }; - }); - }, - compareFileStart, - areReferencesResponseItemsForTheSameLocation - ); - - return { - refs, - symbolName: nameText, - symbolStartOffset: nameColStart, - symbolDisplayString: displayString - }; function areReferencesResponseItemsForTheSameLocation(a: protocol.ReferencesResponseItem, b: protocol.ReferencesResponseItem) { if (a && b) { @@ -1316,9 +1337,11 @@ namespace ts.server { const defArgs = request.arguments; return this.requiredResponse(this.getTypeDefinition(defArgs.line, defArgs.offset, defArgs.file)); }, - [CommandNames.References]: (request: protocol.Request) => { - const defArgs = request.arguments; - return this.requiredResponse(this.getReferences(defArgs.line, defArgs.offset, defArgs.file)); + [CommandNames.References]: (request: protocol.FileLocationRequest) => { + return this.requiredResponse(this.getReferences(request.arguments, /*simplifiedResult*/ true)); + }, + [CommandNames.ReferencesFull]: (request: protocol.FileLocationRequest) => { + return this.requiredResponse(this.getReferences(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.Rename]: (request: protocol.Request) => { const renameArgs = request.arguments; From 5f3b20a440661718ce694df6a7d65256dbbe14bc Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 20 Jun 2016 09:44:31 -0700 Subject: [PATCH 053/307] linter --- src/server/session.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 021779e9f93..37a1476904c 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -714,7 +714,7 @@ namespace ts.server { undefined, // TODO: fixme undefined - ) + ); } function areReferencesResponseItemsForTheSameLocation(a: protocol.ReferencesResponseItem, b: protocol.ReferencesResponseItem) { @@ -1185,7 +1185,7 @@ namespace ts.server { projects, project => project.languageService.getNavigateToItems(args.searchValue, args.maxResultCount), /*comparer*/ undefined, - navigateToItemIsEqualTo) + navigateToItemIsEqualTo); } function navigateToItemIsEqualTo(a: NavigateToItem, b: NavigateToItem): boolean { From 0b7fb0dba838209fe0effd0efafa183c544699b1 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 20 Jun 2016 10:12:24 -0700 Subject: [PATCH 054/307] getRenameInfo --- src/server/session.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/server/session.ts b/src/server/session.ts index 37a1476904c..3d17fff507b 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -134,6 +134,7 @@ namespace ts.server { export const ReferencesFull = "references-full"; export const Reload = "reload"; export const Rename = "rename"; + export const RenameInfoFull = "rename-full"; export const Saveto = "saveto"; export const SignatureHelp = "signatureHelp"; export const SignatureHelpFull = "signatureHelp-full"; @@ -559,6 +560,13 @@ namespace ts.server { return projectInfo; } + private getRenameInfo(args: protocol.FileLocationRequestArgs) { + const { file, project } = this.getFileAndProject(args.file); + const scriptInfo = project.getScriptInfo(file); + const position = this.getPosition(args, scriptInfo) + return project.languageService.getRenameInfo(file, position); + } + private getRenameLocations(line: number, offset: number, fileName: string, findInComments: boolean, findInStrings: boolean): protocol.RenameResponseBody { const file = ts.normalizePath(fileName); const info = this.projectService.getScriptInfo(file); @@ -1347,6 +1355,9 @@ namespace ts.server { const renameArgs = request.arguments; return this.requiredResponse(this.getRenameLocations(renameArgs.line, renameArgs.offset, renameArgs.file, renameArgs.findInComments, renameArgs.findInStrings)); }, + [CommandNames.RenameInfoFull]: (request: protocol.FileLocationRequest) => { + return this.requiredResponse(this.getRenameInfo(request.arguments)); + }, [CommandNames.Open]: (request: protocol.Request) => { const openArgs = request.arguments; let scriptKind: ScriptKind; From bdccb8d0eb34a95dba9d0745d721018598764275 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 20 Jun 2016 11:25:31 -0700 Subject: [PATCH 055/307] renameLocations --- src/server/editorServices.ts | 45 ++-- src/server/session.ts | 193 ++++++++++-------- .../cases/unittests/cachingInServerLSHost.ts | 2 +- 3 files changed, 127 insertions(+), 113 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 0cefde1e7b8..32ce8255570 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -395,7 +395,7 @@ namespace ts.server { } getScriptInfo(fileName: string) { - const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); + const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false, this); if (scriptInfo && !scriptInfo.defaultProject) { scriptInfo.defaultProject = this; } @@ -1157,7 +1157,7 @@ namespace ts.server { let errors: Diagnostic[]; for (const rootFilename of files) { if (this.host.fileExists(rootFilename)) { - const info = this.getOrCreateScriptInfo(rootFilename, /*openedByClient*/ clientFileName == rootFilename); + const info = this.getOrCreateScriptInfo(rootFilename, /*openedByClient*/ clientFileName == rootFilename, project); project.addRoot(info); } else { @@ -1195,7 +1195,7 @@ namespace ts.server { for (const fileName of fileNamesToAdd) { let info = this.getScriptInfo(fileName); if (!info) { - info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); + info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ false, project); } else { // if the root file was opened by client, it would belong to either @@ -1268,7 +1268,7 @@ namespace ts.server { * @param filename is absolute pathname * @param fileContent is a known version of the file content that is more up to date than the one on disk */ - getOrCreateScriptInfo(fileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { + getOrCreateScriptInfo(fileName: string, openedByClient: boolean, containingProject: Project, fileContent?: string, scriptKind?: ScriptKind) { fileName = ts.normalizePath(fileName); let info = ts.lookUp(this.filenameToScriptInfo, fileName); if (!info) { @@ -1284,6 +1284,7 @@ namespace ts.server { if (content !== undefined) { info = new ScriptInfo(this.host, fileName, content, openedByClient); info.scriptKind = scriptKind; + info.defaultProject = containingProject; info.setFormatOptions(toEditorSettings(this.getFormatCodeOptions())); this.filenameToScriptInfo[fileName] = info; if (!info.isOpen) { @@ -1333,27 +1334,27 @@ namespace ts.server { findReferencingProjects(info: ScriptInfo, excludedProject?: Project) { const referencingProjects: Project[] = []; info.defaultProject = undefined; - for (let i = 0, len = this.inferredProjects.length; i < len; i++) { - const inferredProject = this.inferredProjects[i]; - inferredProject.updateGraph(); - if (inferredProject !== excludedProject) { - if (inferredProject.containsScriptInfo(info)) { - info.defaultProject = inferredProject; - referencingProjects.push(inferredProject); - } - } - } - for (let i = 0, len = this.configuredProjects.length; i < len; i++) { - const configuredProject = this.configuredProjects[i]; - configuredProject.updateGraph(); - if (configuredProject.containsScriptInfo(info)) { - info.defaultProject = configuredProject; - referencingProjects.push(configuredProject); - } + this.collectContainingProjects(info, referencingProjects, this.inferredProjects, excludedProject); + this.collectContainingProjects(info, referencingProjects, this.configuredProjects); + this.collectContainingProjects(info, referencingProjects, this.externalProjects); + if (referencingProjects.length) { + info.defaultProject = referencingProjects[0]; } return referencingProjects; } + private collectContainingProjects(info: ScriptInfo, result: Project[], projects: Project[], excludedProject?: Project) { + for (const p of projects) { + if (p === excludedProject) { + continue; + } + p.updateGraph(); + if (p.containsScriptInfo(info)) { + result.push(p); + } + } + } + /** * This function rebuilds the project for every file opened by the client */ @@ -1464,7 +1465,7 @@ namespace ts.server { if (!this.findContainingExternalProject(fileName)) { ({ configFileName, configFileErrors } = this.openOrUpdateConfiguredProjectForFile(fileName)); } - const info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ true, fileContent, scriptKind); + const info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ true, /*containingProject*/ undefined, fileContent, scriptKind); this.addOpenFile(info); this.printProjects(); return { configFileName, configFileErrors }; diff --git a/src/server/session.ts b/src/server/session.ts index 3d17fff507b..a0e0eb67bc0 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -135,6 +135,7 @@ namespace ts.server { export const Reload = "reload"; export const Rename = "rename"; export const RenameInfoFull = "rename-full"; + export const RenameLocationsFull = "renameLocations-full"; export const Saveto = "saveto"; export const SignatureHelp = "signatureHelp"; export const SignatureHelpFull = "signatureHelp-full"; @@ -512,7 +513,7 @@ namespace ts.server { } const scriptInfo = project.getScriptInfo(fileName); - const position = this.getPosition(args, scriptInfo); + const position = this.getPosition(args, scriptInfo); const documentHighlights = project.languageService.getDocumentHighlights(fileName, position, args.filesToSearch); @@ -563,72 +564,109 @@ namespace ts.server { private getRenameInfo(args: protocol.FileLocationRequestArgs) { const { file, project } = this.getFileAndProject(args.file); const scriptInfo = project.getScriptInfo(file); - const position = this.getPosition(args, scriptInfo) + const position = this.getPosition(args, scriptInfo); return project.languageService.getRenameInfo(file, position); } - private getRenameLocations(line: number, offset: number, fileName: string, findInComments: boolean, findInStrings: boolean): protocol.RenameResponseBody { - const file = ts.normalizePath(fileName); - const info = this.projectService.getScriptInfo(file); - const projects = this.projectService.findReferencingProjects(info); - if (!projects.length) { + private getProjects(args: protocol.FileRequestArgs) { + let projects: Project[]; + if (args.projectFileName) { + const project = this.getProject(args.projectFileName); + if (project) { + projects = [project]; + } + } + else { + const file = normalizePath(args.file); + const info = this.projectService.getScriptInfo(file); + projects = this.projectService.findReferencingProjects(info); + } + if (!projects || !projects.length) { throw Errors.NoProject; } + return projects; + } - const defaultProject = projects[0]; - // The rename info should be the same for every project - const scriptInfo = defaultProject.getScriptInfo(file); - const position = scriptInfo.lineOffsetToPosition(line, offset); - const renameInfo = defaultProject.languageService.getRenameInfo(file, position); - if (!renameInfo) { - return undefined; + private getRenameLocations(args: protocol.RenameRequestArgs, simplifiedResult: boolean): protocol.RenameResponseBody | RenameLocation[] { + const file = ts.normalizePath(args.file); + const info = this.projectService.getScriptInfo(file); + const position = this.getPosition(args, info); + const projects = this.getProjects(args); + if (simplifiedResult) { + + const defaultProject = projects[0]; + // The rename info should be the same for every project + const renameInfo = defaultProject.languageService.getRenameInfo(file, position); + if (!renameInfo) { + return undefined; + } + + if (!renameInfo.canRename) { + return { + info: renameInfo, + locs: [] + }; + } + + const fileSpans = combineProjectOutput( + projects, + (project: Project) => { + const renameLocations = project.languageService.findRenameLocations(file, position, args.findInStrings, args.findInComments); + if (!renameLocations) { + return []; + } + + return renameLocations.map(location => { + const locationScriptInfo = project.getScriptInfo(location.fileName); + return { + file: location.fileName, + start: locationScriptInfo.positionToLineOffset(location.textSpan.start), + end: locationScriptInfo.positionToLineOffset(ts.textSpanEnd(location.textSpan)), + }; + }); + }, + compareRenameLocation, + (a, b) => a.file === b.file && a.start.line === b.start.line && a.start.offset === b.start.offset + ); + const locs = fileSpans.reduce((accum, cur) => { + let curFileAccum: protocol.SpanGroup; + if (accum.length > 0) { + curFileAccum = accum[accum.length - 1]; + if (curFileAccum.file !== cur.file) { + curFileAccum = undefined; + } + } + if (!curFileAccum) { + curFileAccum = { file: cur.file, locs: [] }; + accum.push(curFileAccum); + } + curFileAccum.locs.push({ start: cur.start, end: cur.end }); + return accum; + }, []); + + return { info: renameInfo, locs }; + } + else { + return combineProjectOutput( + projects, + p => p.languageService.findRenameLocations(file, position, args.findInStrings, args.findInComments), + /*comparer*/ undefined, + renameLocationIsEqualTo + ); } - if (!renameInfo.canRename) { - return { - info: renameInfo, - locs: [] - }; + function renameLocationIsEqualTo(a: RenameLocation, b: RenameLocation) { + if (a === b) { + return true; + } + if (!a || !b) { + return false; + } + return a.fileName === b.fileName && + a.textSpan.start === b.textSpan.start && + a.textSpan.length === b.textSpan.length; } - const fileSpans = combineProjectOutput( - projects, - (project: Project) => { - const renameLocations = project.languageService.findRenameLocations(file, position, findInStrings, findInComments); - if (!renameLocations) { - return []; - } - - return renameLocations.map(location => { - const locationScriptInfo = project.getScriptInfo(location.fileName); - return { - file: location.fileName, - start: locationScriptInfo.positionToLineOffset(location.textSpan.start), - end: locationScriptInfo.positionToLineOffset(ts.textSpanEnd(location.textSpan)), - }; - }); - }, - compareRenameLocation, - (a, b) => a.file === b.file && a.start.line === b.start.line && a.start.offset === b.start.offset - ); - const locs = fileSpans.reduce((accum, cur) => { - let curFileAccum: protocol.SpanGroup; - if (accum.length > 0) { - curFileAccum = accum[accum.length - 1]; - if (curFileAccum.file !== cur.file) { - curFileAccum = undefined; - } - } - if (!curFileAccum) { - curFileAccum = { file: cur.file, locs: [] }; - accum.push(curFileAccum); - } - curFileAccum.locs.push({ start: cur.start, end: cur.end }); - return accum; - }, []); - - return { info: renameInfo, locs }; - function compareRenameLocation(a: protocol.FileSpan, b: protocol.FileSpan) { if (a.file < b.file) { return -1; @@ -651,22 +689,9 @@ namespace ts.server { } } - private getReferences(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.ReferencesResponseBody | ReferencedSymbol[] { + private getReferences(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.ReferencesResponseBody | ReferencedSymbol[] { const file = ts.normalizePath(args.file); - let projects: Project[]; - if (args.projectFileName) { - const project = this.getProject(args.projectFileName); - if (project) { - projects = [project]; - } - } - else { - const info = this.projectService.getScriptInfo(file); - projects = this.projectService.findReferencingProjects(info); - } - if (!projects || !projects.length) { - throw Errors.NoProject; - } + const projects = this.getProjects(args); const defaultProject = projects[0]; const scriptInfo = defaultProject.getScriptInfo(file); @@ -1133,21 +1158,7 @@ namespace ts.server { } private getNavigateToItems(args: protocol.NavtoRequestArgs, simplifiedResult: boolean): protocol.NavtoItem[] | NavigateToItem[] { - let projects: Project[]; - if (args.projectFileName) { - const project = this.getProject(args.projectFileName); - if (project) { - projects = [project]; - } - } - else { - const file = normalizePath(args.file); - const info = this.projectService.getScriptInfo(file); - projects = this.projectService.findReferencingProjects(info); - } - if (!projects || !projects.length) { - throw Errors.NoProject; - } + const projects = this.getProjects(args); if (simplifiedResult) { return combineProjectOutput( @@ -1352,8 +1363,10 @@ namespace ts.server { return this.requiredResponse(this.getReferences(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.Rename]: (request: protocol.Request) => { - const renameArgs = request.arguments; - return this.requiredResponse(this.getRenameLocations(renameArgs.line, renameArgs.offset, renameArgs.file, renameArgs.findInComments, renameArgs.findInStrings)); + return this.requiredResponse(this.getRenameLocations(request.arguments, /*simplifiedResult*/ true)); + }, + [CommandNames.RenameLocationsFull]: (request: protocol.RenameRequest) => { + return this.requiredResponse(this.getRenameLocations(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.RenameInfoFull]: (request: protocol.FileLocationRequest) => { return this.requiredResponse(this.getRenameInfo(request.arguments)); @@ -1476,7 +1489,7 @@ namespace ts.server { [CommandNames.Reload]: (request: protocol.Request) => { const reloadArgs = request.arguments; this.reload(reloadArgs.file, reloadArgs.tmpfile, request.seq); - return {response: { reloadFinished: true }, responseRequired: true}; + return { response: { reloadFinished: true }, responseRequired: true }; }, [CommandNames.Saveto]: (request: protocol.Request) => { const savetoArgs = request.arguments; diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index b14b9a74350..211dd774919 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -80,7 +80,7 @@ namespace ts { }; const projectService = new server.ProjectService(serverHost, logger, { isCancellationRequested: () => false }); - const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */true); + const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */true, /*containingProject*/ undefined); const project = projectService.createAndAddInferredProject(rootScriptInfo); project.setCompilerOptions({ module: ts.ModuleKind.AMD } ); return { From 3225e555486fc98d528393671dee42c22b8eda20 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 20 Jun 2016 15:22:00 -0700 Subject: [PATCH 056/307] merge with origin/master --- src/server/editorServices.ts | 231 ++++++++++++++---- src/server/session.ts | 32 +-- .../cases/unittests/tsserverProjectSystem.ts | 4 +- 3 files changed, 209 insertions(+), 58 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index b957a99527d..7348f79bfbe 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -123,7 +123,66 @@ namespace ts.server { } } - export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost { + + function throwLanguageServiceIsDisabledError() {; + throw new Error("LanguageService is disabled"); + } + + const nullLanguageService: ts.LanguageService = { + cleanupSemanticCache: (): any => throwLanguageServiceIsDisabledError(), + getSyntacticDiagnostics: (): any => throwLanguageServiceIsDisabledError(), + getSemanticDiagnostics: (): any => throwLanguageServiceIsDisabledError(), + getCompilerOptionsDiagnostics: (): any => throwLanguageServiceIsDisabledError(), + getSyntacticClassifications: (): any => throwLanguageServiceIsDisabledError(), + getEncodedSyntacticClassifications: (): any => throwLanguageServiceIsDisabledError(), + getSemanticClassifications: (): any => throwLanguageServiceIsDisabledError(), + getEncodedSemanticClassifications: (): any => throwLanguageServiceIsDisabledError(), + getCompletionsAtPosition: (): any => throwLanguageServiceIsDisabledError(), + findReferences: (): any => throwLanguageServiceIsDisabledError(), + getCompletionEntryDetails: (): any => throwLanguageServiceIsDisabledError(), + getQuickInfoAtPosition: (): any => throwLanguageServiceIsDisabledError(), + findRenameLocations: (): any => throwLanguageServiceIsDisabledError(), + getNameOrDottedNameSpan: (): any => throwLanguageServiceIsDisabledError(), + getBreakpointStatementAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getBraceMatchingAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getSignatureHelpItems: (): any => throwLanguageServiceIsDisabledError(), + getDefinitionAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getRenameInfo: (): any => throwLanguageServiceIsDisabledError(), + getTypeDefinitionAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getReferencesAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getDocumentHighlights: (): any => throwLanguageServiceIsDisabledError(), + getOccurrencesAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getNavigateToItems: (): any => throwLanguageServiceIsDisabledError(), + getNavigationBarItems: (): any => throwLanguageServiceIsDisabledError(), + getOutliningSpans: (): any => throwLanguageServiceIsDisabledError(), + getTodoComments: (): any => throwLanguageServiceIsDisabledError(), + getIndentationAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getFormattingEditsForRange: (): any => throwLanguageServiceIsDisabledError(), + getFormattingEditsForDocument: (): any => throwLanguageServiceIsDisabledError(), + getFormattingEditsAfterKeystroke: (): any => throwLanguageServiceIsDisabledError(), + getDocCommentTemplateAtPosition: (): any => throwLanguageServiceIsDisabledError(), + isValidBraceCompletionAtPostion: (): any => throwLanguageServiceIsDisabledError(), + getEmitOutput: (): any => throwLanguageServiceIsDisabledError(), + getProgram: (): any => throwLanguageServiceIsDisabledError(), + getNonBoundSourceFile: (): any => throwLanguageServiceIsDisabledError(), + dispose: (): any => throwLanguageServiceIsDisabledError(), + }; + + interface ServerLanguageServiceHost { + getCompilationSettings(): CompilerOptions; + setCompilationSettings(options: CompilerOptions): void; + removeRoot(info: ScriptInfo): void; + removeReferencedFile(info: ScriptInfo): void; + } + + const nullLanguageServiceHost: ServerLanguageServiceHost = { + getCompilationSettings: () => undefined, + setCompilationSettings: () => undefined, + removeRoot: () => undefined, + removeReferencedFile: () => undefined + }; + + export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost, ServerLanguageServiceHost { private compilationSettings: ts.CompilerOptions; private resolvedModuleNames: ts.FileMap>; private resolvedTypeReferenceDirectives: ts.FileMap>; @@ -302,31 +361,62 @@ namespace ts.server { export abstract class Project { private rootFiles: ScriptInfo[] = []; private rootFilesMap: FileMap = createFileMap(); - private readonly lsHost: LSHost; + private lsHost: ServerLanguageServiceHost; - readonly languageService: LanguageService; + languageService: LanguageService; protected program: ts.Program; constructor( readonly projectKind: ProjectKind, readonly projectService: ProjectService, - documentRegistry: ts.DocumentRegistry, + private documentRegistry: ts.DocumentRegistry, hasExplicitListOfFiles: boolean, - compilerOptions: CompilerOptions) { + public languageServiceEnabled: boolean, + private compilerOptions: CompilerOptions) { - if (!compilerOptions) { - compilerOptions = ts.getDefaultCompilerOptions(); - compilerOptions.allowNonTsExtensions = true; - compilerOptions.allowJs = true; + if (!this.compilerOptions) { + this.compilerOptions = ts.getDefaultCompilerOptions(); + this.compilerOptions.allowNonTsExtensions = true; + this.compilerOptions.allowJs = true; } else if (hasExplicitListOfFiles) { // If files are listed explicitly, allow all extensions - compilerOptions.allowNonTsExtensions = true; + this.compilerOptions.allowNonTsExtensions = true; } - this.lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken); - this.lsHost.setCompilationSettings(compilerOptions); - this.languageService = ts.createLanguageService(this.lsHost, documentRegistry); + if (languageServiceEnabled) { + this.enableLanguageServiceWorker(); + } + else { + this.disableLanguageServiceWorker(); + } + } + + enableLanguageService() { + if (!this.languageServiceEnabled) { + this.enableLanguageServiceWorker(); + } + } + + private enableLanguageServiceWorker() { + const lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken); + lsHost.setCompilationSettings(this.compilerOptions); + this.languageService = ts.createLanguageService(lsHost, this.documentRegistry); + + this.lsHost = lsHost; + this.languageServiceEnabled = true; + } + + disableLanguageService() { + if (this.languageServiceEnabled) { + this.disableLanguageServiceWorker(); + } + } + + private disableLanguageServiceWorker() { + this.languageService = nullLanguageService; + this.lsHost = nullLanguageServiceHost; + this.languageServiceEnabled = false; } getProjectFileName(): string { @@ -343,29 +433,29 @@ namespace ts.server { } getRootFiles() { + if (!this.languageServiceEnabled && this.projectKind === ProjectKind.Inferred) { + return undefined; + } return this.rootFiles.map(info => info.fileName); } getFileNames() { - if (this.languageServiceDiabled) { - if (!this.projectOptions) { - return undefined; + if (!this.languageServiceEnabled) { + let rootFiles = this.getRootFiles(); + if (this.compilerOptions) { + const defaultLibrary = getDefaultLibFilePath(this.compilerOptions); + if (defaultLibrary) { + (rootFiles || (rootFiles = [])).push(defaultLibrary); + } } - - const fileNames: string[] = []; - if (this.projectOptions && this.projectOptions.compilerOptions) { - fileNames.push(getDefaultLibFilePath(this.projectOptions.compilerOptions)); - } - ts.addRange(fileNames, this.projectOptions.files); - return fileNames; + return rootFiles; } - const sourceFiles = this.program.getSourceFiles(); return sourceFiles.map(sourceFile => sourceFile.fileName); } containsScriptInfo(info: ScriptInfo): boolean { - return this.program.getSourceFileByPath(info.path) !== undefined; + return this.program && this.program.getSourceFileByPath(info.path) !== undefined; } containsFile(filename: string, requireOpen?: boolean) { @@ -455,8 +545,13 @@ namespace ts.server { // Used to keep track of what directories are watched for this project directoriesWatchedForTsconfig: string[] = []; - constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry) { - super(ProjectKind.Inferred, projectService, documentRegistry, /*files*/ undefined, /*compilerOptions*/ undefined); + constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry, languageServiceEnabled: boolean) { + super(ProjectKind.Inferred, + projectService, + documentRegistry, + /*files*/ undefined, + languageServiceEnabled, + /*compilerOptions*/ undefined); } close() { @@ -483,6 +578,9 @@ namespace ts.server { currentVersion: number = 1; updateGraph() { + if (!this.languageServiceEnabled) { + return; + } const oldProgram = this.program; super.updateGraph(); @@ -538,8 +636,13 @@ namespace ts.server { /** Used for configured projects which may have multiple open roots */ openRefCount = 0; - constructor(readonly configFileName: string, projectService: ProjectService, documentRegistry: ts.DocumentRegistry, hasExplicitListOfFiles: boolean, compilerOptions: CompilerOptions) { - super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, compilerOptions); + constructor(readonly configFileName: string, + projectService: ProjectService, + documentRegistry: ts.DocumentRegistry, + hasExplicitListOfFiles: boolean, + compilerOptions: CompilerOptions, + languageServiceEnabled: boolean) { + super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions); } getProjectFileName() { @@ -551,20 +654,29 @@ namespace ts.server { } watchConfigDirectory(callback: (project: ConfiguredProject, path: string) => void) { + if (this.directoryWatcher) { + return; + } + const directoryToWatch = ts.getDirectoryPath(this.configFileName); this.projectService.log(`Add recursive watcher for: ${directoryToWatch}`); this.directoryWatcher = this.projectService.host.watchDirectory(directoryToWatch, path => callback(this, path), /*recursive*/ true); } + stopWatchingDirectory() { + if (this.directoryWatcher) { + this.directoryWatcher.close(); + this.directoryWatcher = undefined; + } + } + close() { super.close(); if (this.projectFileWatcher) { this.projectFileWatcher.close(); } - if (this.directoryWatcher) { - this.directoryWatcher.close(); - } + this.stopWatchingDirectory(); } addOpenRef() { @@ -578,8 +690,12 @@ namespace ts.server { } class ExternalProject extends VersionedProject { - constructor(readonly projectFileName: string, projectService: ProjectService, documentRegistry: ts.DocumentRegistry, compilerOptions: CompilerOptions) { - super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, compilerOptions); + constructor(readonly projectFileName: string, + projectService: ProjectService, + documentRegistry: ts.DocumentRegistry, + compilerOptions: CompilerOptions, + languageServiceEnabled: boolean) { + super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions); } getProjectFileName() { @@ -1149,7 +1265,10 @@ namespace ts.server { } } - private exceedTotalNonTsFileSizeLimit(fileNames: string[]) { + private exceedTotalNonTsFileSizeLimit(options: CompilerOptions, fileNames: string[]) { + if (options && options.disableSizeLimit) { + return false; + } let totalNonTsFileSize = 0; if (!this.host.getFileSize) { return false; @@ -1168,23 +1287,38 @@ namespace ts.server { } private createAndAddExternalProject(projectFileName: string, files: string[], compilerOptions: CompilerOptions, clientFileName?: string) { - const project = new ExternalProject(projectFileName, this, this.documentRegistry, compilerOptions); + const sizeLimitExceeded = this.exceedTotalNonTsFileSizeLimit(compilerOptions, files); + const project = new ExternalProject(projectFileName, this, this.documentRegistry, compilerOptions, !sizeLimitExceeded); const errors = this.addFilesToProject(project, files, clientFileName); this.externalProjects.push(project); return { project, errors }; } private createAndAddConfiguredProject(configFileName: string, projectOptions: ProjectOptions, clientFileName?: string) { - const project = new ConfiguredProject(configFileName, this, this.documentRegistry, projectOptions.configHasFilesProperty, projectOptions.compilerOptions); + const sizeLimitExceeded = this.exceedTotalNonTsFileSizeLimit(projectOptions.compilerOptions, projectOptions.files); + const project = new ConfiguredProject( + configFileName, + this, + this.documentRegistry, + projectOptions.configHasFilesProperty, + projectOptions.compilerOptions, + !sizeLimitExceeded); + const errors = this.addFilesToProject(project, projectOptions.files, clientFileName); project.watchConfigFile(project => this.onConfigChangedForConfiguredProject(project)); - if (!projectOptions.configHasFilesProperty) { - project.watchConfigDirectory((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path)); + if (!sizeLimitExceeded) { + this.watchConfigDirectoryForProject(project, projectOptions); } this.configuredProjects.push(project); return { project, errors }; } + private watchConfigDirectoryForProject(project: ConfiguredProject, options: ProjectOptions) { + if (!options.configHasFilesProperty) { + project.watchConfigDirectory((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path)); + } + } + private addFilesToProject(project: ConfiguredProject | ExternalProject, files: string[], clientFileName: string): Diagnostic[] { let errors: Diagnostic[]; for (const rootFilename of files) { @@ -1266,13 +1400,28 @@ namespace ts.server { return errors; } else { - this.updateVersionedProjectWorker(project, projectOptions.files, projectOptions.compilerOptions); + if (this.exceedTotalNonTsFileSizeLimit(projectOptions.compilerOptions, projectOptions.files)) { + project.setCompilerOptions(projectOptions.compilerOptions); + if (!project.languageServiceEnabled) { + // language service is already disabled + return; + } + project.disableLanguageService(); + project.stopWatchingDirectory(); + } + else { + if (!project.languageServiceEnabled) { + project.enableLanguageService(); + } + this.watchConfigDirectoryForProject(project, projectOptions); + this.updateVersionedProjectWorker(project, projectOptions.files, projectOptions.compilerOptions); + } } } } createAndAddInferredProject(root: ScriptInfo) { - const project = new InferredProject(this, this.documentRegistry); + const project = new InferredProject(this, this.documentRegistry, /*languageServiceEnabled*/ true); project.addRoot(root); let currentPath = ts.getDirectoryPath(root.fileName); diff --git a/src/server/session.ts b/src/server/session.ts index d34bacc8188..237af69fffd 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -421,7 +421,7 @@ namespace ts.server { private getDefinition(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.FileSpan[] | DefinitionInfo[] { const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); - if (!project || project.languageServiceDiabled) { + if (!project) { throw Errors.NoProject; } @@ -451,7 +451,7 @@ namespace ts.server { private getTypeDefinition(line: number, offset: number, fileName: string): protocol.FileSpan[] { const file = ts.normalizePath(fileName); const project = this.projectService.getProjectForFile(file); - if (!project || project.languageServiceDiabled) { + if (!project) { throw Errors.NoProject; } @@ -477,7 +477,7 @@ namespace ts.server { fileName = ts.normalizePath(fileName); const project = this.projectService.getProjectForFile(fileName); - if (!project || project.languageServiceDiabled) { + if (!project) { throw Errors.NoProject; } @@ -508,7 +508,7 @@ namespace ts.server { const fileName = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(fileName); - if (!project || project.languageServiceDiabled) { + if (!project) { throw Errors.NoProject; } @@ -554,7 +554,8 @@ namespace ts.server { } const projectInfo: protocol.ProjectInfo = { - configFileName: project.getProjectFileName() + configFileName: project.getProjectFileName(), + languageServiceDisabled: !project.languageServiceEnabled }; if (needFileNameList) { @@ -583,6 +584,7 @@ namespace ts.server { const info = this.projectService.getScriptInfo(file); projects = this.projectService.findReferencingProjects(info); } + projects = filter(projects, p => p.languageServiceEnabled); if (!projects || !projects.length) { throw Errors.NoProject; } @@ -781,7 +783,7 @@ namespace ts.server { private getFileAndProject(fileName: string) { const file = ts.normalizePath(fileName); const project = this.projectService.getProjectForFile(file); - if (!project || project.languageServiceDiabled) { + if (!project) { throw Errors.NoProject; } return { file, project }; @@ -862,7 +864,7 @@ namespace ts.server { private getFormattingEditsForRange(line: number, offset: number, endLine: number, endOffset: number, fileName: string): protocol.CodeEdit[] { const file = ts.normalizePath(fileName); const project = this.projectService.getProjectForFile(file); - if (!project || project.languageServiceDiabled) { + if (!project) { throw Errors.NoProject; } @@ -919,7 +921,7 @@ namespace ts.server { const file = ts.normalizePath(fileName); const project = this.projectService.getProjectForFile(file); - if (!project || project.languageServiceDiabled) { + if (!project) { throw Errors.NoProject; } @@ -990,7 +992,7 @@ namespace ts.server { const prefix = args.prefix || ""; const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); - if (!project || project.languageServiceDiabled) { + if (!project) { throw Errors.NoProject; } @@ -1017,7 +1019,7 @@ namespace ts.server { private getCompletionEntryDetails(args: protocol.CompletionDetailsRequestArgs): protocol.CompletionEntryDetails[] { const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); - if (!project || project.languageServiceDiabled) { + if (!project) { throw Errors.NoProject; } @@ -1036,7 +1038,7 @@ namespace ts.server { private getSignatureHelpItems(args: protocol.SignatureHelpRequestArgs, simplifiedResult: boolean): protocol.SignatureHelpItems | SignatureHelpItems { const file = ts.normalizePath(args.file); const project = this.projectService.getProjectForFile(file); - if (!project || project.languageServiceDiabled) { + if (!project) { throw Errors.NoProject; } @@ -1069,7 +1071,7 @@ namespace ts.server { const checkList = fileNames.reduce((accum: PendingErrorCheck[], fileName: string) => { fileName = ts.normalizePath(fileName); const project = this.projectService.getProjectForFile(fileName); - if (project && !project.languageServiceDiabled) { + if (project) { accum.push({ fileName, project }); } return accum; @@ -1099,7 +1101,7 @@ namespace ts.server { const file = ts.normalizePath(fileName); const tmpfile = ts.normalizePath(tempFileName); const project = this.projectService.getProjectForFile(file); - if (project && !project.languageServiceDiabled) { + if (project) { this.changeSeq++; // make sure no changes happen before this one is finished project.reloadScript(file, tmpfile, () => { @@ -1126,7 +1128,7 @@ namespace ts.server { this.projectService.closeClientFile(file); } - private decorateNavigationBarItem(project: Project, fileName: string, items: ts.NavigationBarItem[], lineIndex: LineIndex): protocol.NavigationBarItem[] { + private decorateNavigationBarItem(project: Project, fileName: string, items: ts.NavigationBarItem[]): protocol.NavigationBarItem[] { if (!items) { return undefined; } @@ -1141,7 +1143,7 @@ namespace ts.server { start: scriptInfo.positionToLineOffset(span.start), end: scriptInfo.positionToLineOffset(ts.textSpanEnd(span)) })), - childItems: this.decorateNavigationBarItem(project, fileName, item.childItems, lineIndex), + childItems: this.decorateNavigationBarItem(project, fileName, item.childItems), indent: item.indent })); } diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index afd0813b31a..488c2e8c69c 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -604,7 +604,7 @@ namespace ts { }` }; const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [file1, file2, configFile]); - const projectService = new server.ProjectService(host, nullLogger); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); projectService.openClientFile(file1.path); projectService.closeClientFile(file1.path); projectService.openClientFile(file2.path); @@ -631,7 +631,7 @@ namespace ts { }` }; const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [file1, file2, configFile]); - const projectService = new server.ProjectService(host, nullLogger); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); projectService.openClientFile(file1.path); projectService.closeClientFile(file1.path); projectService.openClientFile(file2.path); From e690f4fdffd95b6440627d7b41be3019796d1b19 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 20 Jun 2016 16:48:57 -0700 Subject: [PATCH 057/307] ported PR #9073 --- src/server/editorServices.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 7348f79bfbe..422b5a5fa30 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1022,6 +1022,9 @@ namespace ts.server { this.findReferencingProjects(info); if (info.defaultProject) { + if (info.defaultProject.projectKind === ProjectKind.Configured) { + (info.defaultProject).addOpenRef(); + } this.openFilesReferenced.push(info); } else { From a45ac2420c7ec453ed56246b5f11e4a8b9109ac0 Mon Sep 17 00:00:00 2001 From: Mohamed Hegazy Date: Tue, 21 Jun 2016 11:34:49 -0700 Subject: [PATCH 058/307] Use document registry to get sourceFiles in SyntaxTreeCache to share trees between semantic and syntaxtic oprations --- src/compiler/parser.ts | 67 +++++++++++++------------ src/harness/harness.ts | 2 +- src/services/services.ts | 105 ++++++++++++++++++++++----------------- 3 files changed, 95 insertions(+), 79 deletions(-) diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index a31cf0cbe93..510b3330771 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -444,7 +444,7 @@ namespace ts { if (result && result.jsDocComment) { // because the jsDocComment was parsed out of the source file, it might // not be covered by the fixupParentReferences. - Parser.fixupParentReferences(result.jsDocComment); + fixupParentReferences(result.jsDocComment); } return result; @@ -456,6 +456,39 @@ namespace ts { return Parser.JSDocParser.parseJSDocTypeExpressionForTests(content, start, length); } + /* @internal */ + export function fixupParentReferences(rootNode: Node) { + // normally parent references are set during binding. However, for clients that only need + // a syntax tree, and no semantic features, then the binding process is an unnecessary + // overhead. This functions allows us to set all the parents, without all the expense of + // binding. + + let parent: Node = rootNode; + forEachChild(rootNode, visitNode); + return; + + function visitNode(n: Node): void { + // walk down setting parents that differ from the parent we think it should be. This + // allows us to quickly bail out of setting parents for subtrees during incremental + // parsing + if (n.parent !== parent) { + n.parent = parent; + + const saveParent = parent; + parent = n; + forEachChild(n, visitNode); + if (n.jsDocComments) { + for (const jsDocComment of n.jsDocComments) { + jsDocComment.parent = n; + parent = jsDocComment; + forEachChild(jsDocComment, visitNode); + } + } + parent = saveParent; + } + } + } + // Implement the parser as a singleton module. We do this for perf reasons because creating // parser instances can actually be expensive enough to impact us on projects with many source // files. @@ -659,38 +692,6 @@ namespace ts { return node; } - export function fixupParentReferences(rootNode: Node) { - // normally parent references are set during binding. However, for clients that only need - // a syntax tree, and no semantic features, then the binding process is an unnecessary - // overhead. This functions allows us to set all the parents, without all the expense of - // binding. - - let parent: Node = rootNode; - forEachChild(rootNode, visitNode); - return; - - function visitNode(n: Node): void { - // walk down setting parents that differ from the parent we think it should be. This - // allows us to quickly bail out of setting parents for subtrees during incremental - // parsing - if (n.parent !== parent) { - n.parent = parent; - - const saveParent = parent; - parent = n; - forEachChild(n, visitNode); - if (n.jsDocComments) { - for (const jsDocComment of n.jsDocComments) { - jsDocComment.parent = n; - parent = jsDocComment; - forEachChild(jsDocComment, visitNode); - } - } - parent = saveParent; - } - } - } - function createSourceFile(fileName: string, languageVersion: ScriptTarget, scriptKind: ScriptKind): SourceFile { // code from createNode is inlined here so createNode won't have to deal with special case of creating source files // this is quite rare comparing to other nodes and createNode should be as fast as possible diff --git a/src/harness/harness.ts b/src/harness/harness.ts index f804dc4eb6a..a872855ce2f 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -366,7 +366,7 @@ namespace Utils { // call this on both nodes to ensure all propagated flags have been set (and thus can be // compared). assert.equal(ts.containsParseError(node1), ts.containsParseError(node2)); - assert.equal(node1.flags, node2.flags, "node1.flags !== node2.flags"); + assert.equal(node1.flags & ~ts.NodeFlags.ReachabilityAndEmitFlags, node2.flags & ~ts.NodeFlags.ReachabilityAndEmitFlags, "node1.flags !== node2.flags"); ts.forEachChild(node1, child1 => { diff --git a/src/services/services.ts b/src/services/services.ts index 9209fc157c2..2e7beb2cb1d 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1894,50 +1894,6 @@ namespace ts { } } - class SyntaxTreeCache { - // For our syntactic only features, we also keep a cache of the syntax tree for the - // currently edited file. - private currentFileName: string; - private currentFileVersion: string; - private currentFileScriptSnapshot: IScriptSnapshot; - private currentSourceFile: SourceFile; - - constructor(private host: LanguageServiceHost) { - } - - public getCurrentSourceFile(fileName: string): SourceFile { - const scriptSnapshot = this.host.getScriptSnapshot(fileName); - if (!scriptSnapshot) { - // The host does not know about this file. - throw new Error("Could not find file: '" + fileName + "'."); - } - - const scriptKind = getScriptKind(fileName, this.host); - const version = this.host.getScriptVersion(fileName); - let sourceFile: SourceFile; - - if (this.currentFileName !== fileName) { - // This is a new file, just parse it - sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, ScriptTarget.Latest, version, /*setNodeParents*/ true, scriptKind); - } - else if (this.currentFileVersion !== version) { - // This is the same file, just a newer version. Incrementally parse the file. - const editRange = scriptSnapshot.getChangeRange(this.currentFileScriptSnapshot); - sourceFile = updateLanguageServiceSourceFile(this.currentSourceFile, scriptSnapshot, version, editRange); - } - - if (sourceFile) { - // All done, ensure state is up to date - this.currentFileVersion = version; - this.currentFileName = fileName; - this.currentFileScriptSnapshot = scriptSnapshot; - this.currentSourceFile = sourceFile; - } - - return this.currentSourceFile; - } - } - function setSourceFileFields(sourceFile: SourceFile, scriptSnapshot: IScriptSnapshot, version: string) { sourceFile.version = version; sourceFile.scriptSnapshot = scriptSnapshot; @@ -2919,7 +2875,7 @@ namespace ts { export function createLanguageService(host: LanguageServiceHost, documentRegistry: DocumentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory())): LanguageService { - const syntaxTreeCache: SyntaxTreeCache = new SyntaxTreeCache(host); + const syntaxTreeCache = createSyntaxTreeCache(); let ruleProvider: formatting.RulesProvider; let program: Program; let lastProjectVersion: string; @@ -3153,6 +3109,64 @@ namespace ts { } } + function createSyntaxTreeCache() { + let currentFileName: string; + let currentFileVersion: string; + let currentFileScriptSnapshot: IScriptSnapshot; + let currentSourceFile: SourceFile; + let currentScriptKind: ScriptKind; + let currentCompilerOptions: CompilerOptions; + return { + getCurrentSourceFile: function (fileName: string) { + const scriptSnapshot = host.getScriptSnapshot(fileName); + if (!scriptSnapshot) { + // The host does not know about this file. + throw new Error("Could not find file: '" + fileName + "'."); + } + const version = host.getScriptVersion(fileName); + const scriptKind = ts.getScriptKind(fileName, host); + const compilerOptions = host.getCompilationSettings(); + let sourceFile: SourceFile; + if (currentFileName !== fileName) { + // Release the current document + if (currentFileName) { + documentRegistry.releaseDocument(currentFileName, currentCompilerOptions); + } + // This is a new file, just parse it + sourceFile = documentRegistry.acquireDocument(fileName, compilerOptions, scriptSnapshot, version, scriptKind); + } + else if (currentFileVersion !== version) { + // This is the same file, just a newer version. Incrementally parse the file. + sourceFile = documentRegistry.updateDocument(fileName, compilerOptions, scriptSnapshot, version, scriptKind); + } + if (sourceFile) { + // All done, ensure state is up to date + currentFileVersion = version; + currentFileName = fileName; + currentScriptKind = scriptKind; + currentFileScriptSnapshot = scriptSnapshot; + currentSourceFile = sourceFile; + currentCompilerOptions = compilerOptions; + } + if (currentSourceFile && !currentSourceFile.locals) { + fixupParentReferences(currentSourceFile); + } + return currentSourceFile; + }, + dispose: function () { + if (currentFileName) { + documentRegistry.releaseDocument(currentFileName, currentCompilerOptions); + currentFileVersion = undefined; + currentFileName = undefined; + currentScriptKind = undefined; + currentFileScriptSnapshot = undefined; + currentSourceFile = undefined; + currentCompilerOptions = undefined; + } + } + }; + } + function getProgram(): Program { synchronizeHostData(); @@ -3170,6 +3184,7 @@ namespace ts { documentRegistry.releaseDocumentWithKey(file.path, key); } } + syntaxTreeCache.dispose(); } /// Diagnostics From c9b82eddda50064cc85ee99a40bcd97bd261833e Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 21 Jun 2016 17:31:54 -0700 Subject: [PATCH 059/307] [in progress] project system work --- Jakefile.js | 4 + src/server/editorServices.ts | 1213 ++++++------------------------ src/server/lshost.ts | 160 ++++ src/server/project.ts | 396 ++++++++++ src/server/protocol.d.ts | 2 +- src/server/scriptInfo.ts | 151 ++++ src/server/scriptVersionCache.ts | 2 +- src/server/session.ts | 92 +-- src/server/utilities.ts | 175 +++++ 9 files changed, 1141 insertions(+), 1054 deletions(-) create mode 100644 src/server/lshost.ts create mode 100644 src/server/project.ts create mode 100644 src/server/scriptInfo.ts create mode 100644 src/server/utilities.ts diff --git a/Jakefile.js b/Jakefile.js index f731cc5060c..933c58c7a39 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -101,7 +101,11 @@ var servicesSources = [ var serverCoreSources = [ "node.d.ts", + "utilities.ts", "scriptVersionCache.ts", + "scriptInfo.ts", + "lsHost.ts", + "project.ts", "editorServices.ts", "protocol.d.ts", "session.ts", diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 1c65db05b73..08014379345 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1,749 +1,16 @@ /// /// /// +/// /// /// +/// +/// namespace ts.server { - export interface Logger { - close(): void; - isVerbose(): boolean; - loggingEnabled(): boolean; - perftrc(s: string): void; - info(s: string): void; - startGroup(): void; - endGroup(): void; - msg(s: string, type?: string): void; - } - - function getDefaultFormatCodeSettings(host: ServerHost): ts.FormatCodeSettings { - return ts.clone({ - indentSize: 4, - tabSize: 4, - newLineCharacter: host.newLine || "\n", - convertTabsToSpaces: true, - indentStyle: ts.IndentStyle.Smart, - insertSpaceAfterCommaDelimiter: true, - insertSpaceAfterSemicolonInForStatements: true, - insertSpaceBeforeAndAfterBinaryOperators: true, - insertSpaceAfterKeywordsInControlFlowStatements: true, - insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, - insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, - placeOpenBraceOnNewLineForFunctions: false, - placeOpenBraceOnNewLineForControlBlocks: false, - }); - } - - function mergeMaps(target: Map, source: Map): void { - for (const key in source) { - if (hasProperty(source, key)) { - target[key] = source[key]; - } - } - } export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; - export class ScriptInfo { - svc: ScriptVersionCache; - defaultProject: Project; // project to use by default for file - fileWatcher: FileWatcher; - formatCodeSettings: ts.FormatCodeSettings; - path: Path; - scriptKind: ScriptKind; - - constructor(private host: ServerHost, public fileName: string, public content: string, public isOpen = false) { - this.path = toPath(fileName, host.getCurrentDirectory(), createGetCanonicalFileName(host.useCaseSensitiveFileNames)); - this.svc = ScriptVersionCache.fromString(host, content); - this.formatCodeSettings = getDefaultFormatCodeSettings(this.host); - } - - setFormatOptions(formatSettings: protocol.FormatOptions): void { - if (formatSettings) { - mergeMaps(this.formatCodeSettings, formatSettings); - } - } - - close() { - this.isOpen = false; - } - - snap() { - return this.svc.getSnapshot(); - } - - getLineInfo(line: number) { - const snap = this.snap(); - return snap.index.lineNumberToInfo(line); - } - - editContent(start: number, end: number, newText: string): void { - this.svc.edit(start, end - start, newText); - } - - /** - * @param line 1 based index - */ - lineToTextSpan(line: number) { - const index = this.snap().index; - const lineInfo = index.lineNumberToInfo(line + 1); - let len: number; - if (lineInfo.leaf) { - len = lineInfo.leaf.text.length; - } - else { - const nextLineInfo = index.lineNumberToInfo(line + 2); - len = nextLineInfo.offset - lineInfo.offset; - } - return ts.createTextSpan(lineInfo.offset, len); - } - - /** - * @param line 1 based index - * @param offset 1 based index - */ - lineOffsetToPosition(line: number, offset: number): number { - const index = this.snap().index; - - const lineInfo = index.lineNumberToInfo(line); - // TODO: assert this offset is actually on the line - return (lineInfo.offset + offset - 1); - } - - /** - * @param line 1-based index - * @param offset 1-based index - */ - positionToLineOffset(position: number): ILineInfo { - const index = this.snap().index; - const lineOffset = index.charOffsetToLineNumberAndPos(position); - return { line: lineOffset.line, offset: lineOffset.offset + 1 }; - } - } - - - function throwLanguageServiceIsDisabledError() {; - throw new Error("LanguageService is disabled"); - } - - const nullLanguageService: ts.LanguageService = { - cleanupSemanticCache: (): any => throwLanguageServiceIsDisabledError(), - getSyntacticDiagnostics: (): any => throwLanguageServiceIsDisabledError(), - getSemanticDiagnostics: (): any => throwLanguageServiceIsDisabledError(), - getCompilerOptionsDiagnostics: (): any => throwLanguageServiceIsDisabledError(), - getSyntacticClassifications: (): any => throwLanguageServiceIsDisabledError(), - getEncodedSyntacticClassifications: (): any => throwLanguageServiceIsDisabledError(), - getSemanticClassifications: (): any => throwLanguageServiceIsDisabledError(), - getEncodedSemanticClassifications: (): any => throwLanguageServiceIsDisabledError(), - getCompletionsAtPosition: (): any => throwLanguageServiceIsDisabledError(), - findReferences: (): any => throwLanguageServiceIsDisabledError(), - getCompletionEntryDetails: (): any => throwLanguageServiceIsDisabledError(), - getQuickInfoAtPosition: (): any => throwLanguageServiceIsDisabledError(), - findRenameLocations: (): any => throwLanguageServiceIsDisabledError(), - getNameOrDottedNameSpan: (): any => throwLanguageServiceIsDisabledError(), - getBreakpointStatementAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getBraceMatchingAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getSignatureHelpItems: (): any => throwLanguageServiceIsDisabledError(), - getDefinitionAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getRenameInfo: (): any => throwLanguageServiceIsDisabledError(), - getTypeDefinitionAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getReferencesAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getDocumentHighlights: (): any => throwLanguageServiceIsDisabledError(), - getOccurrencesAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getNavigateToItems: (): any => throwLanguageServiceIsDisabledError(), - getNavigationBarItems: (): any => throwLanguageServiceIsDisabledError(), - getOutliningSpans: (): any => throwLanguageServiceIsDisabledError(), - getTodoComments: (): any => throwLanguageServiceIsDisabledError(), - getIndentationAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getFormattingEditsForRange: (): any => throwLanguageServiceIsDisabledError(), - getFormattingEditsForDocument: (): any => throwLanguageServiceIsDisabledError(), - getFormattingEditsAfterKeystroke: (): any => throwLanguageServiceIsDisabledError(), - getDocCommentTemplateAtPosition: (): any => throwLanguageServiceIsDisabledError(), - isValidBraceCompletionAtPostion: (): any => throwLanguageServiceIsDisabledError(), - getEmitOutput: (): any => throwLanguageServiceIsDisabledError(), - getProgram: (): any => throwLanguageServiceIsDisabledError(), - getNonBoundSourceFile: (): any => throwLanguageServiceIsDisabledError(), - dispose: (): any => throwLanguageServiceIsDisabledError(), - }; - - interface ServerLanguageServiceHost { - getCompilationSettings(): CompilerOptions; - setCompilationSettings(options: CompilerOptions): void; - removeRoot(info: ScriptInfo): void; - removeReferencedFile(info: ScriptInfo): void; - } - - const nullLanguageServiceHost: ServerLanguageServiceHost = { - getCompilationSettings: () => undefined, - setCompilationSettings: () => undefined, - removeRoot: () => undefined, - removeReferencedFile: () => undefined - }; - - export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost, ServerLanguageServiceHost { - private compilationSettings: ts.CompilerOptions; - private resolvedModuleNames: ts.FileMap>; - private resolvedTypeReferenceDirectives: ts.FileMap>; - private getCanonicalFileName: (fileName: string) => string; - - constructor(private host: ServerHost, private project: Project, private cancellationToken: HostCancellationToken) { - this.getCanonicalFileName = ts.createGetCanonicalFileName(this.host.useCaseSensitiveFileNames); - this.resolvedModuleNames = createFileMap>(); - this.resolvedTypeReferenceDirectives = createFileMap>(); - } - - private resolveNamesWithLocalCache( - names: string[], - containingFile: string, - cache: ts.FileMap>, - loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T, - getResult: (s: T) => R): R[] { - - const path = toPath(containingFile, this.host.getCurrentDirectory(), this.getCanonicalFileName); - const currentResolutionsInFile = cache.get(path); - - const newResolutions: Map = {}; - const resolvedModules: R[] = []; - const compilerOptions = this.getCompilationSettings(); - - for (const name of names) { - // check if this is a duplicate entry in the list - let resolution = lookUp(newResolutions, name); - if (!resolution) { - const existingResolution = currentResolutionsInFile && ts.lookUp(currentResolutionsInFile, name); - if (moduleResolutionIsValid(existingResolution)) { - // ok, it is safe to use existing name resolution results - resolution = existingResolution; - } - else { - newResolutions[name] = resolution = loader(name, containingFile, compilerOptions, this); - } - } - - ts.Debug.assert(resolution !== undefined); - - resolvedModules.push(getResult(resolution)); - } - - // replace old results with a new one - cache.set(path, newResolutions); - return resolvedModules; - - function moduleResolutionIsValid(resolution: T): boolean { - if (!resolution) { - return false; - } - - if (getResult(resolution)) { - // TODO: consider checking failedLookupLocations - return true; - } - - // consider situation if we have no candidate locations as valid resolution. - // after all there is no point to invalidate it if we have no idea where to look for the module. - return resolution.failedLookupLocations.length === 0; - } - } - - getCancellationToken() { - return this.cancellationToken; - } - - resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { - return this.resolveNamesWithLocalCache(typeDirectiveNames, containingFile, this.resolvedTypeReferenceDirectives, resolveTypeReferenceDirective, m => m.resolvedTypeReferenceDirective); - } - - resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModule[] { - return this.resolveNamesWithLocalCache(moduleNames, containingFile, this.resolvedModuleNames, resolveModuleName, m => m.resolvedModule); - } - - getDefaultLibFileName() { - const nodeModuleBinDir = ts.getDirectoryPath(ts.normalizePath(this.host.getExecutingFilePath())); - return ts.combinePaths(nodeModuleBinDir, ts.getDefaultLibFileName(this.compilationSettings)); - } - - getScriptSnapshot(filename: string): ts.IScriptSnapshot { - const scriptInfo = this.project.getScriptInfo(filename); - if (scriptInfo) { - return scriptInfo.snap(); - } - } - - setCompilationSettings(opt: ts.CompilerOptions) { - this.compilationSettings = opt; - // conservatively assume that changing compiler options might affect module resolution strategy - this.resolvedModuleNames.clear(); - this.resolvedTypeReferenceDirectives.clear(); - } - - getCompilationSettings() { - // change this to return active project settings for file - return this.compilationSettings; - } - - getScriptFileNames() { - return this.project.getRootFiles(); - } - - getScriptKind(fileName: string) { - const info = this.project.getScriptInfo(fileName); - if (!info) { - return undefined; - } - - if (!info.scriptKind) { - info.scriptKind = getScriptKindFromFileName(fileName); - } - return info.scriptKind; - } - - getScriptVersion(filename: string) { - return this.project.getScriptInfo(filename).svc.latestVersion().toString(); - } - - getCurrentDirectory(): string { - return ""; - } - - removeReferencedFile(info: ScriptInfo) { - if (!info.isOpen) { - this.resolvedModuleNames.remove(info.path); - this.resolvedTypeReferenceDirectives.remove(info.path); - } - } - - removeRoot(info: ScriptInfo) { - this.resolvedModuleNames.remove(info.path); - this.resolvedTypeReferenceDirectives.remove(info.path); - } - - resolvePath(path: string): string { - return this.host.resolvePath(path); - } - - fileExists(path: string): boolean { - return this.host.fileExists(path); - } - - directoryExists(path: string): boolean { - return this.host.directoryExists(path); - } - - readFile(fileName: string): string { - return this.host.readFile(fileName); - } - - getDirectories(path: string): string[] { - return this.host.getDirectories(path); - } - } - - export interface ProjectOptions { - /** - * true if config file explicitly listed files - **/ - configHasFilesProperty?: boolean; - /** - * these fields can be present in the project file - **/ - files?: string[]; - wildcardDirectories?: Map; - compilerOptions?: CompilerOptions; - } - - export enum ProjectKind { - Inferred, - Configured, - External - } - - export abstract class Project { - private rootFiles: ScriptInfo[] = []; - private rootFilesMap: FileMap = createFileMap(); - private lsHost: ServerLanguageServiceHost; - languageService: LanguageService; - protected program: ts.Program; - - constructor( - readonly projectKind: ProjectKind, - readonly projectService: ProjectService, - private documentRegistry: ts.DocumentRegistry, - hasExplicitListOfFiles: boolean, - public languageServiceEnabled: boolean, - private compilerOptions: CompilerOptions) { - - if (!this.compilerOptions) { - this.compilerOptions = ts.getDefaultCompilerOptions(); - this.compilerOptions.allowNonTsExtensions = true; - this.compilerOptions.allowJs = true; - } - else if (hasExplicitListOfFiles) { - // If files are listed explicitly, allow all extensions - this.compilerOptions.allowNonTsExtensions = true; - } - - if (languageServiceEnabled) { - this.enableLanguageServiceWorker(); - } - else { - this.disableLanguageServiceWorker(); - } - } - - enableLanguageService() { - if (!this.languageServiceEnabled) { - this.enableLanguageServiceWorker(); - } - } - - private enableLanguageServiceWorker() { - const lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken); - lsHost.setCompilationSettings(this.compilerOptions); - this.languageService = ts.createLanguageService(lsHost, this.documentRegistry); - - this.lsHost = lsHost; - this.languageServiceEnabled = true; - } - - disableLanguageService() { - if (this.languageServiceEnabled) { - this.disableLanguageServiceWorker(); - } - } - - private disableLanguageServiceWorker() { - this.languageService = nullLanguageService; - this.lsHost = nullLanguageServiceHost; - this.languageServiceEnabled = false; - } - - getProjectFileName(): string { - return undefined; - } - - close() { - // signal language service to release files acquired from document registry - this.languageService.dispose(); - } - - getCompilerOptions() { - return this.lsHost.getCompilationSettings(); - } - - getRootFiles() { - if (!this.languageServiceEnabled && this.projectKind === ProjectKind.Inferred) { - return undefined; - } - return this.rootFiles.map(info => info.fileName); - } - - getFileNames() { - if (!this.languageServiceEnabled) { - let rootFiles = this.getRootFiles(); - if (this.compilerOptions) { - const defaultLibrary = getDefaultLibFilePath(this.compilerOptions); - if (defaultLibrary) { - (rootFiles || (rootFiles = [])).push(defaultLibrary); - } - } - return rootFiles; - } - const sourceFiles = this.program.getSourceFiles(); - return sourceFiles.map(sourceFile => sourceFile.fileName); - } - - containsScriptInfo(info: ScriptInfo): boolean { - return this.program && this.program.getSourceFileByPath(info.path) !== undefined; - } - - containsFile(filename: string, requireOpen?: boolean) { - const info = this.projectService.getScriptInfo(filename); - if (info) { - if ((!requireOpen) || info.isOpen) { - return this.containsScriptInfo(info); - } - } - } - - isRoot(info: ScriptInfo) { - return this.rootFilesMap.contains(info.path); - } - - // add a root file to project - addRoot(info: ScriptInfo) { - if (!this.isRoot(info)) { - this.rootFiles.push(info); - this.rootFilesMap.set(info.path, info); - } - } - - // remove a root file from project - removeRoot(info: ScriptInfo) { - if (this.isRoot(info)) { - this.rootFiles = copyListRemovingItem(info, this.rootFiles); - this.rootFilesMap.remove(info.path); - this.lsHost.removeRoot(info); - } - } - - removeReferencedFile(info: ScriptInfo) { - if (!info.isOpen) { - this.lsHost.removeReferencedFile(info); - } - this.updateGraph(); - } - - updateGraph() { - this.program = this.languageService.getProgram(); - } - - getScriptInfo(fileName: string) { - const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false, this); - if (scriptInfo && !scriptInfo.defaultProject) { - scriptInfo.defaultProject = this; - } - return scriptInfo; - } - - filesToString() { - if (!this.program) { - return ""; - } - let strBuilder = ""; - for (const file of this.program.getSourceFiles()) { - strBuilder += `${file.fileName}\n`; - } - return strBuilder; - } - - setCompilerOptions(compilerOptions: CompilerOptions) { - if (compilerOptions) { - compilerOptions.allowNonTsExtensions = true; - this.lsHost.setCompilationSettings(compilerOptions); - } - } - - saveTo(filename: string, tmpfilename: string) { - const script = this.getScriptInfo(filename); - if (script) { - const snap = script.snap(); - this.projectService.host.writeFile(tmpfilename, snap.getText(0, snap.getLength())); - } - } - - reloadScript(filename: string, tmpfilename: string, cb: () => any) { - const script = this.getScriptInfo(filename); - if (script) { - script.svc.reloadFromFile(tmpfilename, cb); - } - } - } - - class InferredProject extends Project { - // Used to keep track of what directories are watched for this project - directoriesWatchedForTsconfig: string[] = []; - - constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry, languageServiceEnabled: boolean) { - super(ProjectKind.Inferred, - projectService, - documentRegistry, - /*files*/ undefined, - languageServiceEnabled, - /*compilerOptions*/ undefined); - } - - close() { - super.close(); - - for (const directory of this.directoriesWatchedForTsconfig) { - this.projectService.stopWatchingDirectory(directory); - } - } - } - - function findVersionedProjectByFileName(projectFileName: string, projects: T[]): T { - for (const proj of projects) { - if (proj.getProjectFileName() === projectFileName) { - return proj; - } - } - } - - abstract class VersionedProject extends Project { - - private lastReportedFileNames: Map; - private lastReportedVersion: number = 0; - currentVersion: number = 1; - - updateGraph() { - if (!this.languageServiceEnabled) { - return; - } - const oldProgram = this.program; - - super.updateGraph(); - - if (!oldProgram || !oldProgram.structureIsReused) { - this.currentVersion++; - } - } - - getChangesSinceVersion(lastKnownVersion?: number): protocol.ExternalProjectFiles { - const info = { - projectFileName: this.getProjectFileName(), - version: this.currentVersion - }; - if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) { - if (this.currentVersion == this.lastReportedVersion) { - return { info }; - } - const lastReportedFileNames = this.lastReportedFileNames; - const currentFiles = arrayToMap(this.getFileNames(), x => x); - - const added: string[] = []; - const removed: string[] = []; - for (const id in currentFiles) { - if (hasProperty(currentFiles, id) && !hasProperty(lastReportedFileNames, id)) { - added.push(id); - } - } - for (const id in lastReportedFileNames) { - if (hasProperty(lastReportedFileNames, id) && !hasProperty(currentFiles, id)) { - removed.push(id); - } - } - this.lastReportedFileNames = currentFiles; - - this.lastReportedFileNames = currentFiles; - this.lastReportedVersion = this.currentVersion; - return { info, changes: { added, removed } }; - } - else { - // unknown version - return everything - const projectFileNames = this.getFileNames(); - this.lastReportedFileNames = arrayToMap(projectFileNames, x => x); - this.lastReportedVersion = this.currentVersion; - return { info, files: projectFileNames }; - } - } - } - - class ConfiguredProject extends VersionedProject { - private projectFileWatcher: FileWatcher; - private directoryWatcher: FileWatcher; - private directoriesWatchedForWildcards: Map; - /** Used for configured projects which may have multiple open roots */ - openRefCount = 0; - - constructor(readonly configFileName: string, - projectService: ProjectService, - documentRegistry: ts.DocumentRegistry, - hasExplicitListOfFiles: boolean, - compilerOptions: CompilerOptions, - private wildcardDirectories: Map, - languageServiceEnabled: boolean) { - super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions); - } - - getProjectFileName() { - return this.configFileName; - } - - watchConfigFile(callback: (project: ConfiguredProject) => void) { - this.projectFileWatcher = this.projectService.host.watchFile(this.configFileName, _ => callback(this)); - } - - watchConfigDirectory(callback: (project: ConfiguredProject, path: string) => void) { - if (this.directoryWatcher) { - return; - } - - const directoryToWatch = getDirectoryPath(this.configFileName); - this.projectService.log(`Add recursive watcher for: ${directoryToWatch}`); - this.directoryWatcher = this.projectService.host.watchDirectory(directoryToWatch, path => callback(this, path), /*recursive*/ true); - } - - watchWildcards(callback: (project: ConfiguredProject, path: string) => void) { - if (!this.wildcardDirectories) { - return; - } - const configDirectoryPath = getDirectoryPath(this.configFileName); - this.directoriesWatchedForWildcards = reduceProperties(this.wildcardDirectories, (watchers, flag, directory) => { - if (comparePaths(configDirectoryPath, directory, ".", !this.projectService.host.useCaseSensitiveFileNames) !== Comparison.EqualTo) { - const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; - this.projectService.log(`Add ${recursive ? "recursive " : ""}watcher for: ${directory}`); - watchers[directory] = this.projectService.host.watchDirectory( - directory, - path => callback(this, path), - recursive - ); - } - return watchers; - }, >{}); - } - - stopWatchingDirectory() { - if (this.directoryWatcher) { - this.directoryWatcher.close(); - this.directoryWatcher = undefined; - } - } - - close() { - super.close(); - - if (this.projectFileWatcher) { - this.projectFileWatcher.close(); - } - - forEachValue(this.directoriesWatchedForWildcards, watcher => { watcher.close(); }); - this.directoriesWatchedForWildcards = undefined; - - this.stopWatchingDirectory(); - } - - addOpenRef() { - this.openRefCount++; - } - - deleteOpenRef() { - this.openRefCount--; - return this.openRefCount; - } - } - - class ExternalProject extends VersionedProject { - constructor(readonly projectFileName: string, - projectService: ProjectService, - documentRegistry: ts.DocumentRegistry, - compilerOptions: CompilerOptions, - languageServiceEnabled: boolean) { - super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions); - } - - getProjectFileName() { - return this.projectFileName; - } - } - - export interface ProjectOpenResult { - success?: boolean; - errorMsg?: string; - project?: Project; - } - - function copyListRemovingItem(item: T, list: T[]) { - const copiedList: T[] = []; - for (let i = 0, len = list.length; i < len; i++) { - if (list[i] != item) { - copiedList.push(list[i]); - } - } - return copiedList; - } - /** * This helper funciton processes a list of projects and return the concatenated, sortd and deduplicated output of processing each project. */ @@ -753,7 +20,7 @@ namespace ts.server { } export interface ProjectServiceEventHandler { - (eventName: string, project: Project, fileName: string): void; + (eventName: string, project: Project, fileName: NormalizedPath): void; } export interface HostConfiguration { @@ -761,17 +28,26 @@ namespace ts.server { hostInfo: string; } - export class ProjectService { - private filenameToScriptInfo: ts.Map = {}; - /** - * open, non-configured root files - **/ - openFileRoots: ScriptInfo[] = []; + function findVersionedProjectByFileName(projectName: string, projects: T[]): T { + for (const proj of projects) { + if (proj.getProjectName() === projectName) { + return proj; + } + } + } + export interface ProjectOpenResult { + success?: boolean; + errorMsg?: string; + project?: Project; + } + + export class ProjectService { + private filenameToScriptInfo = createNormalizedPathMap(); /** * maps external project file name to list of config files that were the part of this project */ - externalProjectToConfiguredProjectMap: ts.Map = {}; + externalProjectToConfiguredProjectMap: Map; /** * external projects (configuration and list of root files is not controlled by tsserver) @@ -785,6 +61,11 @@ namespace ts.server { * projects specified by a tsconfig.json file **/ configuredProjects: ConfiguredProject[] = []; + /** + * open, non-configured root files + **/ + + openFileRoots: ScriptInfo[] = []; /** * open files referenced by a project **/ @@ -793,6 +74,7 @@ namespace ts.server { * open files that are roots of a configured project **/ openFileRootsConfigured: ScriptInfo[] = []; + /** * a path to directory watcher map that detects added tsconfig files **/ @@ -834,18 +116,19 @@ namespace ts.server { } } - getProject(projectFileName: string): Project { - // TODO: fixme - if (!projectFileName) { - // TODO: fixme - return this.inferredProjects.length ? this.inferredProjects[0] : undefined; + findProject(projectName: string): Project { + if (projectName === undefined) { + return undefined; } - return this.findExternalProjectByProjectFileName(projectFileName) || this.findConfiguredProjectByConfigFile(normalizePath(projectFileName)); + if (isInferredProjectName(projectName)) { + return forEach(this.inferredProjects, p => p.getProjectName() === projectName && p); + } + return this.findExternalProjectByProjectName(projectName) || this.findConfiguredProjectByProjectName(toNormalizedPath(projectName)); } - getFormatCodeOptions(file?: string) { + getFormatCodeOptions(file?: NormalizedPath) { if (file) { - const info = this.filenameToScriptInfo[file]; + const info = this.filenameToScriptInfo.get(file); if (info) { return info.formatCodeSettings; } @@ -853,8 +136,8 @@ namespace ts.server { return this.hostConfiguration.formatCodeOptions; } - private onSourceFileChanged(fileName: string) { - const info = this.filenameToScriptInfo[fileName]; + private onSourceFileChanged(fileName: NormalizedPath) { + const info = this.filenameToScriptInfo.get(fileName); if (!info) { this.psLogger.info("Error: got watch notification for unknown file: " + fileName); } @@ -865,7 +148,7 @@ namespace ts.server { } else { if (info && (!info.isOpen)) { - info.svc.reloadFromFile(info.fileName); + info.reloadFromFile(info.fileName); } } } @@ -873,30 +156,22 @@ namespace ts.server { private handleDeletedFile(info: ScriptInfo) { this.psLogger.info(info.fileName + " deleted"); - if (info.fileWatcher) { - info.fileWatcher.close(); - info.fileWatcher = undefined; - } + info.stopWatcher(); if (!info.isOpen) { - this.filenameToScriptInfo[info.fileName] = undefined; - const referencingProjects = this.findReferencingProjects(info); - if (info.defaultProject) { - info.defaultProject.removeRoot(info); - } - for (let i = 0, len = referencingProjects.length; i < len; i++) { - referencingProjects[i].removeReferencedFile(info); - } - for (let j = 0, flen = this.openFileRoots.length; j < flen; j++) { - const openFile = this.openFileRoots[j]; + this.filenameToScriptInfo.remove(info.fileName); + + info.detachAllProjects(); + + for (const openFile of this.openFileRoots) { if (this.eventHandler) { - this.eventHandler("context", openFile.defaultProject, openFile.fileName); + this.eventHandler("context", openFile.getDefaultProject(), openFile.fileName); } } - for (let j = 0, flen = this.openFilesReferenced.length; j < flen; j++) { - const openFile = this.openFilesReferenced[j]; + + for (const openFile of this.openFilesReferenced) { if (this.eventHandler) { - this.eventHandler("context", openFile.defaultProject, openFile.fileName); + this.eventHandler("context", openFile.getDefaultProject(), openFile.fileName); } } } @@ -981,10 +256,14 @@ namespace ts.server { return ts.normalizePath(name); } - private refreshConfiguredProjects() { + private releaseNonReferencedConfiguredProjects() { + if (this.configuredProjects.every(p => p.openRefCount > 0)) { + return; + } + const configuredProjects: ConfiguredProject[] = []; - for (let i = 0, len = this.configuredProjects.length; i < len; i++) { - const proj = this.configuredProjects[i]; + + for (const proj of this.configuredProjects) { if (proj.openRefCount > 0) { configuredProjects.push(proj); } @@ -992,6 +271,7 @@ namespace ts.server { proj.close(); } } + this.configuredProjects = configuredProjects; } @@ -1002,27 +282,20 @@ namespace ts.server { switch (project.projectKind) { case ProjectKind.External: - this.externalProjects = copyListRemovingItem(project, this.externalProjects); + removeItemFromSet(this.externalProjects, project); break; case ProjectKind.Configured: - this.configuredProjects = copyListRemovingItem((project), this.configuredProjects); + removeItemFromSet(this.configuredProjects, project); break; case ProjectKind.Inferred: - this.inferredProjects = copyListRemovingItem((project), this.inferredProjects); + removeItemFromSet(this.inferredProjects, project); break; } - - for (const fileName of project.getFileNames()) { - const info = this.getScriptInfo(fileName); - if (info.defaultProject === project) { - info.defaultProject = undefined; - } - } } private findContainingConfiguredProject(info: ScriptInfo): ConfiguredProject { for (const proj of this.configuredProjects) { - if (proj.isRoot(info)) { + if (proj.containsScriptInfo(info)) { return proj; } } @@ -1032,49 +305,42 @@ namespace ts.server { private addOpenFile(info: ScriptInfo) { const externalProject = this.findContainingExternalProject(info.fileName); if (externalProject) { - info.defaultProject = externalProject; + // info.defaultProject = externalProject; return; } const configuredProject = this.findContainingConfiguredProject(info); if (configuredProject) { - info.defaultProject = configuredProject; + // info.defaultProject = configuredProject; configuredProject.addOpenRef(); - - // ?? better do this on file close - this.refreshConfiguredProjects(); + if (configuredProject.isRoot(info)) { + this.openFileRootsConfigured.push(info); + } + else { + this.openFilesReferenced.push(info); + } return; } - - this.findReferencingProjects(info); - if (info.defaultProject) { - if (info.defaultProject.projectKind === ProjectKind.Configured) { - (info.defaultProject).addOpenRef(); + // create new inferred project p with the newly opened file as root + const inferredProject = this.createAndAddInferredProject(info); + const openFileRoots: ScriptInfo[] = []; + // for each inferred project root r + for (const rootFile of this.openFileRoots) { + // if r referenced by the new project + if (inferredProject.containsScriptInfo(rootFile)) { + // remove project rooted at r + this.removeProject(rootFile.getDefaultProject()); + // put r in referenced open file list + this.openFilesReferenced.push(rootFile); + // set default project of r to the new project + rootFile.attachToProject(inferredProject); } - this.openFilesReferenced.push(info); - } - else { - // create new inferred project p with the newly opened file as root - info.defaultProject = this.createAndAddInferredProject(info); - const openFileRoots: ScriptInfo[] = []; - // for each inferred project root r - for (const rootFile of this.openFileRoots) { - // if r referenced by the new project - if (info.defaultProject.containsScriptInfo(rootFile)) { - // remove project rooted at r - this.removeProject(rootFile.defaultProject); - // put r in referenced open file list - this.openFilesReferenced.push(rootFile); - // set default project of r to the new project - rootFile.defaultProject = info.defaultProject; - } - else { - // otherwise, keep r as root of inferred project - openFileRoots.push(rootFile); - } + else { + // otherwise, keep r as root of inferred project + openFileRoots.push(rootFile); } - this.openFileRoots = openFileRoots; - this.openFileRoots.push(info); } + this.openFileRoots = openFileRoots; + this.openFileRoots.push(info); } /** @@ -1085,47 +351,36 @@ namespace ts.server { // Closing file should trigger re-reading the file content from disk. This is // because the user may chose to discard the buffer content before saving // to the disk, and the server's version of the file can be out of sync. - info.svc.reloadFromFile(info.fileName); + info.reloadFromFile(info.fileName); - const openFileRoots: ScriptInfo[] = []; - let removedProject: Project; - for (const rootFile of this.openFileRoots) { - // if closed file is root of project - if (info === rootFile) { - // remove that project and remember it - removedProject = info.defaultProject; - } - else { - openFileRoots.push(rootFile); - } - } + this.openFileRoots = copyListRemovingItem(info, this.openFileRoots); + this.openFileRootsConfigured = copyListRemovingItem(info, this.openFileRootsConfigured); - this.openFileRoots = openFileRoots; - if (!removedProject) { - const openFileRootsConfigured: ScriptInfo[] = []; - - for (const configuredRoot of this.openFileRootsConfigured) { - if (info === configuredRoot) { - if ((info.defaultProject).deleteOpenRef() === 0) { - removedProject = info.defaultProject; - } - } - else { - openFileRootsConfigured.push(configuredRoot); + // collect all projects that should be removed + let projectsToRemove: Project[]; + for (const p of info.containingProjects) { + if ( p.projectKind === ProjectKind.Configured) { + // last open file in configured project - close it + if ((p).deleteOpenRef() === 0) { + (projectsToRemove || (projectsToRemove = [])).push(p); } } - - this.openFileRootsConfigured = openFileRootsConfigured; + else if (p.projectKind === ProjectKind.Inferred && p.isRoot(info)) { + // open file in inferred project + (projectsToRemove || (projectsToRemove = [])).push(p); + } } - if (removedProject) { - this.removeProject(removedProject); + if (projectsToRemove) { + for (const project of projectsToRemove) { + this.removeProject(project); + } + const openFilesReferenced: ScriptInfo[] = []; const orphanFiles: ScriptInfo[] = []; // for all open, referenced files f for (const f of this.openFilesReferenced) { - // if f was referenced by the removed project, remember it - if (f.defaultProject === removedProject || !f.defaultProject) { - f.defaultProject = undefined; + // collect orphanted files and try to re-add them as newly opened + if (f.containingProjects.length === 0) { orphanFiles.push(f); } else { @@ -1142,11 +397,13 @@ namespace ts.server { else { this.openFilesReferenced = copyListRemovingItem(info, this.openFilesReferenced); } - info.close(); + + this.releaseNonReferencedConfiguredProjects(); + + info.isOpen = false; } - private findContainingExternalProject(fileName: string): ExternalProject { - fileName = normalizePath(fileName); + private findContainingExternalProject(fileName: NormalizedPath): ExternalProject { for (const proj of this.externalProjects) { if (proj.containsFile(fileName)) { return proj; @@ -1161,13 +418,13 @@ namespace ts.server { * the tsconfig file content and update the project; otherwise we create a new one. */ private openOrUpdateConfiguredProjectForFile(fileName: string): { configFileName?: string, configFileErrors?: Diagnostic[] } { - const searchPath = getDirectoryPath(normalizePath(fileName)); + const searchPath = asNormalizedPath(getDirectoryPath(toNormalizedPath(fileName))); this.log("Search path: " + searchPath, "Info"); // check if this file is already included in one of external projects const configFileName = this.findConfigFile(searchPath); if (configFileName) { this.log("Config file name: " + configFileName, "Info"); - const project = this.findConfiguredProjectByConfigFile(configFileName); + const project = this.findConfiguredProjectByProjectName(configFileName); if (!project) { const { success, errors } = this.openConfigFile(configFileName, fileName); if (!success) { @@ -1197,19 +454,19 @@ namespace ts.server { // current directory (the directory in which tsc was invoked). // The server must start searching from the directory containing // the newly opened file. - private findConfigFile(searchPath: string): string { + private findConfigFile(searchPath: NormalizedPath): NormalizedPath { while (true) { - const tsconfigFileName = ts.combinePaths(searchPath, "tsconfig.json"); + const tsconfigFileName = asNormalizedPath(combinePaths(searchPath, "tsconfig.json")); if (this.host.fileExists(tsconfigFileName)) { return tsconfigFileName; } - const jsconfigFileName = ts.combinePaths(searchPath, "jsconfig.json"); + const jsconfigFileName = asNormalizedPath(combinePaths(searchPath, "jsconfig.json")); if (this.host.fileExists(jsconfigFileName)) { return jsconfigFileName; } - const parentPath = ts.getDirectoryPath(searchPath); + const parentPath = asNormalizedPath(getDirectoryPath(searchPath)); if (parentPath === searchPath) { break; } @@ -1243,7 +500,7 @@ namespace ts.server { } this.psLogger.info("Open files referenced by inferred or configured projects: "); for (const referencedFile of this.openFilesReferenced) { - const fileInfo = `${referencedFile.fileName} ${ProjectKind[referencedFile.defaultProject.projectKind]}`; + const fileInfo = `${referencedFile.fileName} ${ProjectKind[referencedFile.getDefaultProject().projectKind]}`; this.psLogger.info(fileInfo); } this.psLogger.info("Open file roots of configured projects: "); @@ -1253,11 +510,11 @@ namespace ts.server { this.psLogger.endGroup(); } - private findConfiguredProjectByConfigFile(configFileName: string) { + private findConfiguredProjectByProjectName(configFileName: NormalizedPath) { return findVersionedProjectByFileName(configFileName, this.configuredProjects); } - private findExternalProjectByProjectFileName(projectFileName: string) { + private findExternalProjectByProjectName(projectFileName: string) { return findVersionedProjectByFileName(projectFileName, this.externalProjects); } @@ -1354,7 +611,7 @@ namespace ts.server { let errors: Diagnostic[]; for (const rootFilename of files) { if (this.host.fileExists(rootFilename)) { - const info = this.getOrCreateScriptInfo(rootFilename, /*openedByClient*/ clientFileName == rootFilename, project); + const info = this.getOrCreateScriptInfo(toNormalizedPath(rootFilename), /*openedByClient*/ clientFileName == rootFilename); project.addRoot(info); } else { @@ -1378,21 +635,21 @@ namespace ts.server { private updateVersionedProjectWorker(project: VersionedProject, newRootFiles: string[], newOptions: CompilerOptions) { const oldRootFiles = project.getRootFiles(); - const newFileNames = ts.filter(newRootFiles, f => this.host.fileExists(f)); + const newFileNames = asNormalizedPathArray(filter(newRootFiles, f => this.host.fileExists(f))); const fileNamesToRemove = oldRootFiles.filter(f => !contains(newFileNames, f)); const fileNamesToAdd = newFileNames.filter(f => !contains(oldRootFiles, f)); for (const fileName of fileNamesToRemove) { - const info = this.getScriptInfo(fileName); + const info = this.getScriptInfoForNormalizedPath(fileName); if (info) { - project.removeRoot(info); + project.removeFile(info); } } for (const fileName of fileNamesToAdd) { let info = this.getScriptInfo(fileName); if (!info) { - info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ false, project); + info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); } else { // if the root file was opened by client, it would belong to either @@ -1400,49 +657,28 @@ namespace ts.server { if (info.isOpen) { if (contains(this.openFileRoots, info)) { this.openFileRoots = copyListRemovingItem(info, this.openFileRoots); - if (info.defaultProject && info.defaultProject.projectKind === ProjectKind.Inferred) { - this.removeProject(info.defaultProject); + // delete inferred project + let toRemove: Project[]; + for (const p of info.containingProjects) { + if (p.projectKind === ProjectKind.Inferred && p.isRoot(info)) { + (toRemove || (toRemove = [])).push(p); + } + } + if (toRemove) { + for (const p of toRemove) { + this.removeProject(p); + } } } if (contains(this.openFilesReferenced, info)) { - this.openFilesReferenced = copyListRemovingItem(info, this.openFilesReferenced); + removeItemFromSet(this.openFilesReferenced, info); } if (project.projectKind === ProjectKind.Configured) { this.openFileRootsConfigured.push(info); } - info.defaultProject = project; } } project.addRoot(info); -// ======= -// project.finishGraph(); -// project.projectFileWatcher = this.host.watchFile(configFilename, _ => this.watchedProjectConfigFileChanged(project)); - -// const configDirectoryPath = ts.getDirectoryPath(configFilename); - -// this.log("Add recursive watcher for: " + configDirectoryPath); -// project.directoryWatcher = this.host.watchDirectory( -// configDirectoryPath, -// path => this.directoryWatchedForSourceFilesChanged(project, path), -// /*recursive*/ true -// ); - -// project.directoriesWatchedForWildcards = reduceProperties(projectOptions.wildcardDirectories, (watchers, flag, directory) => { -// if (comparePaths(configDirectoryPath, directory, ".", !this.host.useCaseSensitiveFileNames) !== Comparison.EqualTo) { -// const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; -// this.log(`Add ${ recursive ? "recursive " : ""}watcher for: ${directory}`); -// watchers[directory] = this.host.watchDirectory( -// directory, -// path => this.directoryWatchedForSourceFilesChanged(project, path), -// recursive -// ); -// } - -// return watchers; -// }, >{}); - -// return { success: true, project: project, errors }; -// >>>>>>> origin/master } project.setCompilerOptions(newOptions); @@ -1509,9 +745,8 @@ namespace ts.server { * @param filename is absolute pathname * @param fileContent is a known version of the file content that is more up to date than the one on disk */ - getOrCreateScriptInfo(fileName: string, openedByClient: boolean, containingProject: Project, fileContent?: string, scriptKind?: ScriptKind) { - fileName = ts.normalizePath(fileName); - let info = ts.lookUp(this.filenameToScriptInfo, fileName); + getOrCreateScriptInfo(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { + let info = this.filenameToScriptInfo.get(fileName); if (!info) { let content: string; if (this.host.fileExists(fileName)) { @@ -1523,19 +758,17 @@ namespace ts.server { } } if (content !== undefined) { - info = new ScriptInfo(this.host, fileName, content, openedByClient); - info.scriptKind = scriptKind; - info.defaultProject = containingProject; + info = new ScriptInfo(this.host, fileName, content, scriptKind, openedByClient); info.setFormatOptions(toEditorSettings(this.getFormatCodeOptions())); - this.filenameToScriptInfo[fileName] = info; + this.filenameToScriptInfo.set(fileName, info); if (!info.isOpen) { - info.fileWatcher = this.host.watchFile(fileName, _ => { this.onSourceFileChanged(fileName); }); + info.setWatcher(this.host.watchFile(fileName, _ => this.onSourceFileChanged(fileName))); } } } if (info) { if (fileContent) { - info.svc.reload(fileContent); + info.reload(fileContent); } if (openedByClient) { info.isOpen = true; @@ -1550,7 +783,7 @@ namespace ts.server { setHostConfiguration(args: ts.server.protocol.ConfigureRequestArguments) { if (args.file) { - const info = this.filenameToScriptInfo[args.file]; + const info = this.filenameToScriptInfo.get(toNormalizedPath(args.file)); if (info) { info.setFormatOptions(args.formatOptions); this.log("Host configuration update for file " + args.file, "Info"); @@ -1572,30 +805,6 @@ namespace ts.server { this.psLogger.close(); } - findReferencingProjects(info: ScriptInfo, excludedProject?: Project) { - const referencingProjects: Project[] = []; - info.defaultProject = undefined; - this.collectContainingProjects(info, referencingProjects, this.inferredProjects, excludedProject); - this.collectContainingProjects(info, referencingProjects, this.configuredProjects); - this.collectContainingProjects(info, referencingProjects, this.externalProjects); - if (referencingProjects.length) { - info.defaultProject = referencingProjects[0]; - } - return referencingProjects; - } - - private collectContainingProjects(info: ScriptInfo, result: Project[], projects: Project[], excludedProject?: Project) { - for (const p of projects) { - if (p === excludedProject) { - continue; - } - p.updateGraph(); - if (p.containsScriptInfo(info)) { - result.push(p); - } - } - } - /** * This function rebuilds the project for every file opened by the client */ @@ -1619,15 +828,22 @@ namespace ts.server { const unattachedOpenFiles: ScriptInfo[] = []; const openFileRootsConfigured: ScriptInfo[] = []; + // collect all orphanted script infos that used to be roots of configured projects for (const info of this.openFileRootsConfigured) { - const project = info.defaultProject; - if (!project || !(project.containsScriptInfo(info))) { - info.defaultProject = undefined; + if(info.containingProjects.length === 0) { unattachedOpenFiles.push(info); } else { openFileRootsConfigured.push(info); } + // const project = info.defaultProject; + // if (!project || !(project.containsScriptInfo(info))) { + // info.defaultProject = undefined; + // unattachedOpenFiles.push(info); + // } + // else { + // openFileRootsConfigured.push(info); + // } } this.openFileRootsConfigured = openFileRootsConfigured; @@ -1637,13 +853,20 @@ namespace ts.server { // If not, add the file to an unattached list, to be rechecked later. const openFilesReferenced: ScriptInfo[] = []; for (const referencedFile of this.openFilesReferenced) { - referencedFile.defaultProject.updateGraph(); - if (referencedFile.defaultProject.containsScriptInfo(referencedFile)) { - openFilesReferenced.push(referencedFile); - } - else { + // check if any of projects that used to reference this file are still referencing it + if (referencedFile.containingProjects.length === 0) { unattachedOpenFiles.push(referencedFile); } + else { + openFilesReferenced.push(referencedFile); + } + // referencedFile.defaultProject.updateGraph(); + // if (referencedFile.defaultProject.containsScriptInfo(referencedFile)) { + // openFilesReferenced.push(referencedFile); + // } + // else { + // unattachedOpenFiles.push(referencedFile); + // } } this.openFilesReferenced = openFilesReferenced; @@ -1656,57 +879,79 @@ namespace ts.server { // the file to the open, referenced file list. const openFileRoots: ScriptInfo[] = []; for (const rootFile of this.openFileRoots) { - const rootedProject = rootFile.defaultProject; - const referencingProjects = this.findReferencingProjects(rootFile, rootedProject); - if (rootFile.defaultProject && rootFile.defaultProject.projectKind !== ProjectKind.Inferred) { - // If the root file has already been added into a configured project, - // meaning the original inferred project is gone already. - if (rootedProject.projectKind === ProjectKind.Inferred) { - this.removeProject(rootedProject); + let inInferredProjectOnly = true; + for (const p of rootFile.containingProjects) { + if (p.projectKind !== ProjectKind.Inferred) { + // file was included in non-inferred project - drop old inferred project + inInferredProjectOnly = false; + break; } - this.openFileRootsConfigured.push(rootFile); + } + if (inInferredProjectOnly) { + openFileRoots.push(rootFile); } else { - if (referencingProjects.length === 0) { - rootFile.defaultProject = rootedProject; - openFileRoots.push(rootFile); - } - else { - // remove project from inferred projects list because root captured - this.removeProject(rootedProject); - this.openFilesReferenced.push(rootFile); - } } + + // const rootedProject = rootFile.defaultProject; + // const referencingProjects = this.findReferencingProjects(rootFile, rootedProject); + + + // if (rootFile.defaultProject && rootFile.defaultProject.projectKind !== ProjectKind.Inferred) { + // // If the root file has already been added into a configured project, + // // meaning the original inferred project is gone already. + // if (rootedProject.projectKind === ProjectKind.Inferred) { + // this.removeProject(rootedProject); + // } + // this.openFileRootsConfigured.push(rootFile); + // } + // else { + // if (referencingProjects.length === 0) { + // rootFile.defaultProject = rootedProject; + // openFileRoots.push(rootFile); + // } + // else { + // // remove project from inferred projects list because root captured + // this.removeProject(rootedProject); + // this.openFilesReferenced.push(rootFile); + // } + // } } this.openFileRoots = openFileRoots; // Finally, if we found any open, referenced files that are no longer // referenced by their default project, treat them as newly opened // by the editor. - for (let i = 0, len = unattachedOpenFiles.length; i < len; i++) { - this.addOpenFile(unattachedOpenFiles[i]); + for (const f of unattachedOpenFiles) { + this.addOpenFile(f); } this.printProjects(); } - getScriptInfo(filename: string) { - filename = ts.normalizePath(filename); - return ts.lookUp(this.filenameToScriptInfo, filename); + getScriptInfo(uncheckedFileName: string) { + return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); } + getScriptInfoForNormalizedPath(fileName: NormalizedPath) { + return this.filenameToScriptInfo.get(fileName); + } /** * Open file whose contents is managed by the client * @param filename is absolute pathname * @param fileContent is a known version of the file content that is more up to date than the one on disk */ - openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind): { configFileName?: string, configFileErrors?: Diagnostic[] } { + openClientFile(uncheckedFileName: string, fileContent?: string, scriptKind?: ScriptKind): { configFileName?: string, configFileErrors?: Diagnostic[] } { let configFileName: string; let configFileErrors: Diagnostic[]; + + const fileName = toNormalizedPath(uncheckedFileName); if (!this.findContainingExternalProject(fileName)) { ({ configFileName, configFileErrors } = this.openOrUpdateConfiguredProjectForFile(fileName)); } - const info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ true, /*containingProject*/ undefined, fileContent, scriptKind); + + // at this point if file is the part of some configured/external project then this project should be created + const info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ true, fileContent, scriptKind); this.addOpenFile(info); this.printProjects(); return { configFileName, configFileErrors }; @@ -1716,8 +961,8 @@ namespace ts.server { * Close file whose contents is managed by the client * @param filename is absolute pathname */ - closeClientFile(filename: string) { - const info = ts.lookUp(this.filenameToScriptInfo, filename); + closeClientFile(uncheckedFileName: string) { + const info = this.filenameToScriptInfo.get(toNormalizedPath(uncheckedFileName)); if (info) { this.closeOpenFile(info); info.isOpen = false; @@ -1725,16 +970,16 @@ namespace ts.server { this.printProjects(); } - getProjectForFile(filename: string) { - const scriptInfo = ts.lookUp(this.filenameToScriptInfo, filename); - if (scriptInfo) { - return scriptInfo.defaultProject; - } - } + // getProjectForFile(filename: string) { + // const scriptInfo = ts.lookUp(this.filenameToScriptInfo, filename); + // if (scriptInfo) { + // return scriptInfo.defaultProject; + // } + // } private addExternalProjectFilesForVersionedProjects(knownProjects: protocol.ExternalProjectInfo[], projects: VersionedProject[], result: protocol.ExternalProjectFiles[]): void { for (const proj of projects) { - const knownProject = ts.forEach(knownProjects, p => p.projectFileName === proj.getProjectFileName() && p); + const knownProject = ts.forEach(knownProjects, p => p.projectName === proj.getProjectName() && p); result.push(proj.getChangesSinceVersion(knownProject && knownProject.version)); } } @@ -1772,12 +1017,12 @@ namespace ts.server { this.updateProjectStructure(); } - closeExternalProject(fileName: string): void { - fileName = normalizePath(fileName); + closeExternalProject(uncheckedFileName: string): void { + const fileName = toNormalizedPath(uncheckedFileName); const configFiles = this.externalProjectToConfiguredProjectMap[fileName]; if (configFiles) { for (const configFile of configFiles) { - const configuredProject = this.findConfiguredProjectByConfigFile(configFile); + const configuredProject = this.findConfiguredProjectByProjectName(configFile); if (configuredProject) { this.removeProject(configuredProject); this.updateProjectStructure(); @@ -1786,7 +1031,7 @@ namespace ts.server { } else { // close external project - const externalProject = this.findExternalProjectByProjectFileName(fileName); + const externalProject = this.findExternalProjectByProjectName(uncheckedFileName); if (externalProject) { this.removeProject(externalProject); this.updateProjectStructure(); @@ -1795,16 +1040,16 @@ namespace ts.server { } openExternalProject(proj: protocol.ExternalProject): void { - const externalProject = this.findExternalProjectByProjectFileName(proj.projectFileName); + const externalProject = this.findExternalProjectByProjectName(proj.projectFileName); if (externalProject) { this.updateVersionedProjectWorker(externalProject, proj.rootFiles, proj.options); } else { - let tsConfigFiles: string[]; + let tsConfigFiles: NormalizedPath[]; const rootFiles: string[] = []; for (const file of proj.rootFiles) { if (getBaseFileName(file) === "tsconfig.json") { - (tsConfigFiles || (tsConfigFiles = [])).push(file); + (tsConfigFiles || (tsConfigFiles = [])).push(toNormalizedPath(file)); } else { rootFiles.push(file); diff --git a/src/server/lshost.ts b/src/server/lshost.ts new file mode 100644 index 00000000000..c5b44834e2f --- /dev/null +++ b/src/server/lshost.ts @@ -0,0 +1,160 @@ +/// +/// +/// + +namespace ts.server { + export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost, ServerLanguageServiceHost { + private compilationSettings: ts.CompilerOptions; + private readonly resolvedModuleNames: ts.FileMap>; + private readonly resolvedTypeReferenceDirectives: ts.FileMap>; + private readonly getCanonicalFileName: (fileName: string) => string; + + constructor(private readonly host: ServerHost, private readonly project: Project, private readonly cancellationToken: HostCancellationToken) { + this.getCanonicalFileName = ts.createGetCanonicalFileName(this.host.useCaseSensitiveFileNames); + this.resolvedModuleNames = createFileMap>(); + this.resolvedTypeReferenceDirectives = createFileMap>(); + } + + private resolveNamesWithLocalCache( + names: string[], + containingFile: string, + cache: ts.FileMap>, + loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T, + getResult: (s: T) => R): R[] { + + const path = toPath(containingFile, this.host.getCurrentDirectory(), this.getCanonicalFileName); + const currentResolutionsInFile = cache.get(path); + + const newResolutions: Map = {}; + const resolvedModules: R[] = []; + const compilerOptions = this.getCompilationSettings(); + + for (const name of names) { + // check if this is a duplicate entry in the list + let resolution = lookUp(newResolutions, name); + if (!resolution) { + const existingResolution = currentResolutionsInFile && ts.lookUp(currentResolutionsInFile, name); + if (moduleResolutionIsValid(existingResolution)) { + // ok, it is safe to use existing name resolution results + resolution = existingResolution; + } + else { + newResolutions[name] = resolution = loader(name, containingFile, compilerOptions, this); + } + } + + ts.Debug.assert(resolution !== undefined); + + resolvedModules.push(getResult(resolution)); + } + + // replace old results with a new one + cache.set(path, newResolutions); + return resolvedModules; + + function moduleResolutionIsValid(resolution: T): boolean { + if (!resolution) { + return false; + } + + if (getResult(resolution)) { + // TODO: consider checking failedLookupLocations + return true; + } + + // consider situation if we have no candidate locations as valid resolution. + // after all there is no point to invalidate it if we have no idea where to look for the module. + return resolution.failedLookupLocations.length === 0; + } + } + + getProjectVersion() { + return this.project.getProjectVersion(); + } + + getCancellationToken() { + return this.cancellationToken; + } + + resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { + return this.resolveNamesWithLocalCache(typeDirectiveNames, containingFile, this.resolvedTypeReferenceDirectives, resolveTypeReferenceDirective, m => m.resolvedTypeReferenceDirective); + } + + resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModule[] { + return this.resolveNamesWithLocalCache(moduleNames, containingFile, this.resolvedModuleNames, resolveModuleName, m => m.resolvedModule); + } + + getDefaultLibFileName() { + const nodeModuleBinDir = ts.getDirectoryPath(ts.normalizePath(this.host.getExecutingFilePath())); + return ts.combinePaths(nodeModuleBinDir, ts.getDefaultLibFileName(this.compilationSettings)); + } + + getScriptSnapshot(filename: string): ts.IScriptSnapshot { + const scriptInfo = this.project.getScriptInfo(filename); + if (scriptInfo) { + return scriptInfo.snap(); + } + } + + setCompilationSettings(opt: ts.CompilerOptions) { + this.compilationSettings = opt; + // conservatively assume that changing compiler options might affect module resolution strategy + this.resolvedModuleNames.clear(); + this.resolvedTypeReferenceDirectives.clear(); + } + + getCompilationSettings() { + // change this to return active project settings for file + return this.compilationSettings; + } + + getScriptFileNames() { + return this.project.getRootFiles(); + } + + getScriptKind(fileName: string) { + const info = this.project.getScriptInfo(fileName); + return info && info.scriptKind; + } + + getScriptVersion(filename: string) { + return this.project.getScriptInfo(filename).getLatestVersion(); + } + + getCurrentDirectory(): string { + return ""; + } + + removeReferencedFile(info: ScriptInfo) { + if (!info.isOpen) { + this.resolvedModuleNames.remove(info.path); + this.resolvedTypeReferenceDirectives.remove(info.path); + } + } + + removeRoot(info: ScriptInfo) { + this.resolvedModuleNames.remove(info.path); + this.resolvedTypeReferenceDirectives.remove(info.path); + } + + resolvePath(path: string): string { + return this.host.resolvePath(path); + } + + fileExists(path: string): boolean { + return this.host.fileExists(path); + } + + directoryExists(path: string): boolean { + return this.host.directoryExists(path); + } + + readFile(fileName: string): string { + return this.host.readFile(fileName); + } + + getDirectories(path: string): string[] { + return this.host.getDirectories(path); + } + } +} \ No newline at end of file diff --git a/src/server/project.ts b/src/server/project.ts new file mode 100644 index 00000000000..1b122802f56 --- /dev/null +++ b/src/server/project.ts @@ -0,0 +1,396 @@ +/// +/// +/// + +namespace ts.server { + export enum ProjectKind { + Inferred, + Configured, + External + } + + export abstract class Project { + private rootFiles: ScriptInfo[] = []; + private rootFilesMap: FileMap = createFileMap(); + private lsHost: ServerLanguageServiceHost; + protected program: ts.Program; + private version = 0; + + languageService: LanguageService; + + constructor( + readonly projectKind: ProjectKind, + readonly projectService: ProjectService, + private documentRegistry: ts.DocumentRegistry, + hasExplicitListOfFiles: boolean, + public languageServiceEnabled: boolean, + private compilerOptions: CompilerOptions) { + + if (!this.compilerOptions) { + this.compilerOptions = ts.getDefaultCompilerOptions(); + this.compilerOptions.allowNonTsExtensions = true; + this.compilerOptions.allowJs = true; + } + else if (hasExplicitListOfFiles) { + // If files are listed explicitly, allow all extensions + this.compilerOptions.allowNonTsExtensions = true; + } + + if (languageServiceEnabled) { + this.enableLanguageService(); + } + else { + this.disableLanguageService(); + } + this.markAsDirty(); + } + + getProjectVersion() { + return this.version.toString(); + } + + enableLanguageService() { + const lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken); + lsHost.setCompilationSettings(this.compilerOptions); + this.languageService = ts.createLanguageService(lsHost, this.documentRegistry); + + this.lsHost = lsHost; + this.languageServiceEnabled = true; + } + + disableLanguageService() { + this.languageService = nullLanguageService; + this.lsHost = nullLanguageServiceHost; + this.languageServiceEnabled = false; + } + + abstract getProjectName(): string; + + close() { + for (const fileName of this.getFileNames()) { + const info = this.projectKind.getScriptInfoForNormalizedPath(fileName); + info.detachFromProject(project); + } + // signal language service to release files acquired from document registry + this.languageService.dispose(); + + } + + getCompilerOptions() { + return this.compilerOptions; + } + + getRootFiles() { + return this.rootFiles.map(info => info.fileName); + } + + getFileNames() { + if (!this.languageServiceEnabled) { + // if language service is disabled assume that all files in program are root files + default library + let rootFiles = this.getRootFiles(); + if (this.compilerOptions) { + const defaultLibrary = getDefaultLibFilePath(this.compilerOptions); + if (defaultLibrary) { + (rootFiles || (rootFiles = [])).push(asNormalizedPath(defaultLibrary)); + } + } + return rootFiles; + } + const sourceFiles = this.program.getSourceFiles(); + return sourceFiles.map(sourceFile => asNormalizedPath(sourceFile.fileName)); + } + + containsScriptInfo(info: ScriptInfo): boolean { + return this.program && this.program.getSourceFileByPath(info.path) !== undefined; + } + + containsFile(filename: NormalizedPath, requireOpen?: boolean) { + const info = this.projectService.getScriptInfoForNormalizedPath(filename); + if (info) { + if ((!requireOpen) || info.isOpen) { + return this.containsScriptInfo(info); + } + } + } + + isRoot(info: ScriptInfo) { + return this.rootFilesMap.contains(info.path); + } + + // add a root file to project + addRoot(info: ScriptInfo) { + if (!this.isRoot(info)) { + this.rootFiles.push(info); + this.rootFilesMap.set(info.path, info); + info.attachToProject(this); + + this.markAsDirty(); + } + } + + removeFile(info: ScriptInfo) { + if (!this.removeRoot(info)) { + this.removeReferencedFile(info) + } + info.detachFromProject(this); + this.markAsDirty(); + } + + markAsDirty() { + this.version++; + } + + // remove a root file from project + private removeRoot(info: ScriptInfo): boolean { + if (this.isRoot(info)) { + this.rootFiles = copyListRemovingItem(info, this.rootFiles); + this.rootFilesMap.remove(info.path); + this.lsHost.removeRoot(info); + return true; + } + return false; + } + + private removeReferencedFile(info: ScriptInfo) { + this.lsHost.removeReferencedFile(info) + this.updateGraph(); + } + + updateGraph() { + this.program = this.languageService.getProgram(); + } + + getScriptInfo(uncheckedFileName: string) { + const scriptInfo = this.projectService.getOrCreateScriptInfo(toNormalizedPath(uncheckedFileName), /*openedByClient*/ false); + if (scriptInfo.attachToProject(this)) { + this.markAsDirty(); + } + return scriptInfo; + } + + filesToString() { + if (!this.program) { + return ""; + } + let strBuilder = ""; + for (const file of this.program.getSourceFiles()) { + strBuilder += `${file.fileName}\n`; + } + return strBuilder; + } + + setCompilerOptions(compilerOptions: CompilerOptions) { + if (compilerOptions) { + compilerOptions.allowNonTsExtensions = true; + this.compilerOptions = compilerOptions; + this.lsHost.setCompilationSettings(compilerOptions); + + this.markAsDirty(); + } + } + + saveTo(filename: string, tmpfilename: string) { + const script = this.getScriptInfo(filename); + if (script) { + const snap = script.snap(); + this.projectService.host.writeFile(tmpfilename, snap.getText(0, snap.getLength())); + } + } + + reloadScript(filename: string, tmpfilename: string, cb: () => void) { + const script = this.getScriptInfo(filename); + if (script) { + script.reloadFromFile(filename, cb); + } + } + } + + export class InferredProject extends Project { + + static NextId = 0; + + readonly inferredProjectName; + // Used to keep track of what directories are watched for this project + directoriesWatchedForTsconfig: string[] = []; + + constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry, languageServiceEnabled: boolean) { + super(ProjectKind.Inferred, + projectService, + documentRegistry, + /*files*/ undefined, + languageServiceEnabled, + /*compilerOptions*/ undefined); + + this.inferredProjectName = makeInferredProjectName(InferredProject.NextId++); + } + + getProjectName() { + return this.inferredProjectName; + } + + close() { + super.close(); + + for (const directory of this.directoriesWatchedForTsconfig) { + this.projectService.stopWatchingDirectory(directory); + } + } + } + + export abstract class VersionedProject extends Project { + + private lastReportedFileNames: Map; + private lastReportedVersion: number = 0; + currentVersion: number = 1; + + updateGraph() { + if (!this.languageServiceEnabled) { + return; + } + const oldProgram = this.program; + + super.updateGraph(); + + if (!oldProgram || !oldProgram.structureIsReused) { + this.currentVersion++; + } + } + + getChangesSinceVersion(lastKnownVersion?: number): protocol.ExternalProjectFiles { + const info = { + projectName: this.getProjectName(), + version: this.currentVersion + }; + if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) { + if (this.currentVersion == this.lastReportedVersion) { + return { info }; + } + const lastReportedFileNames = this.lastReportedFileNames; + const currentFiles = arrayToMap(this.getFileNames(), x => x); + + const added: string[] = []; + const removed: string[] = []; + for (const id in currentFiles) { + if (hasProperty(currentFiles, id) && !hasProperty(lastReportedFileNames, id)) { + added.push(id); + } + } + for (const id in lastReportedFileNames) { + if (hasProperty(lastReportedFileNames, id) && !hasProperty(currentFiles, id)) { + removed.push(id); + } + } + this.lastReportedFileNames = currentFiles; + + this.lastReportedFileNames = currentFiles; + this.lastReportedVersion = this.currentVersion; + return { info, changes: { added, removed } }; + } + else { + // unknown version - return everything + const projectFileNames = this.getFileNames(); + this.lastReportedFileNames = arrayToMap(projectFileNames, x => x); + this.lastReportedVersion = this.currentVersion; + return { info, files: projectFileNames }; + } + } + } + + export class ConfiguredProject extends VersionedProject { + private projectFileWatcher: FileWatcher; + private directoryWatcher: FileWatcher; + private directoriesWatchedForWildcards: Map; + /** Used for configured projects which may have multiple open roots */ + openRefCount = 0; + + constructor(readonly configFileName: string, + projectService: ProjectService, + documentRegistry: ts.DocumentRegistry, + hasExplicitListOfFiles: boolean, + compilerOptions: CompilerOptions, + private wildcardDirectories: Map, + languageServiceEnabled: boolean) { + super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions); + } + + getProjectName() { + return this.configFileName; + } + + watchConfigFile(callback: (project: ConfiguredProject) => void) { + this.projectFileWatcher = this.projectService.host.watchFile(this.configFileName, _ => callback(this)); + } + + watchConfigDirectory(callback: (project: ConfiguredProject, path: string) => void) { + if (this.directoryWatcher) { + return; + } + + const directoryToWatch = getDirectoryPath(this.configFileName); + this.projectService.log(`Add recursive watcher for: ${directoryToWatch}`); + this.directoryWatcher = this.projectService.host.watchDirectory(directoryToWatch, path => callback(this, path), /*recursive*/ true); + } + + watchWildcards(callback: (project: ConfiguredProject, path: string) => void) { + if (!this.wildcardDirectories) { + return; + } + const configDirectoryPath = getDirectoryPath(this.configFileName); + this.directoriesWatchedForWildcards = reduceProperties(this.wildcardDirectories, (watchers, flag, directory) => { + if (comparePaths(configDirectoryPath, directory, ".", !this.projectService.host.useCaseSensitiveFileNames) !== Comparison.EqualTo) { + const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; + this.projectService.log(`Add ${recursive ? "recursive " : ""}watcher for: ${directory}`); + watchers[directory] = this.projectService.host.watchDirectory( + directory, + path => callback(this, path), + recursive + ); + } + return watchers; + }, >{}); + } + + stopWatchingDirectory() { + if (this.directoryWatcher) { + this.directoryWatcher.close(); + this.directoryWatcher = undefined; + } + } + + close() { + super.close(); + + if (this.projectFileWatcher) { + this.projectFileWatcher.close(); + } + + forEachValue(this.directoriesWatchedForWildcards, watcher => { watcher.close(); }); + this.directoriesWatchedForWildcards = undefined; + + this.stopWatchingDirectory(); + } + + addOpenRef() { + this.openRefCount++; + } + + deleteOpenRef() { + this.openRefCount--; + return this.openRefCount; + } + } + + export class ExternalProject extends VersionedProject { + constructor(readonly externalProjectName: string, + projectService: ProjectService, + documentRegistry: ts.DocumentRegistry, + compilerOptions: CompilerOptions, + languageServiceEnabled: boolean) { + super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions); + } + + getProjectName() { + return this.externalProjectName; + } + } +} \ No newline at end of file diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 599b7ac5b5a..e1eb1ecb015 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -495,7 +495,7 @@ declare namespace ts.server.protocol { } export interface ExternalProjectInfo { - projectFileName: string; + projectName: string; version: number; } diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts new file mode 100644 index 00000000000..ddf8008600e --- /dev/null +++ b/src/server/scriptInfo.ts @@ -0,0 +1,151 @@ +/// + +namespace ts.server { + + export class ScriptInfo { + private svc: ScriptVersionCache; + /** + * All projects that include this file + */ + readonly containingProjects: Project[] = []; + + private fileWatcher: FileWatcher; + formatCodeSettings: ts.FormatCodeSettings; + readonly path: Path; + + constructor( + private readonly host: ServerHost, + readonly fileName: NormalizedPath, + content: string, + readonly scriptKind: ScriptKind, + public isOpen = false) { + + this.path = toPath(fileName, host.getCurrentDirectory(), createGetCanonicalFileName(host.useCaseSensitiveFileNames)); + this.svc = ScriptVersionCache.fromString(host, content); + this.formatCodeSettings = getDefaultFormatCodeSettings(this.host); + this.scriptKind = scriptKind && scriptKind !== ScriptKind.Unknown + ? scriptKind + : getScriptKindFromFileName(fileName); + } + + attachToProject(project: Project): boolean { + if (!contains(this.containingProjects, project)) { + this.containingProjects.push(project); + return true; + } + return false; + } + + detachFromProject(project: Project) { + const index = this.containingProjects.indexOf(project); + if (index < 0) { + // TODO: (assert?) attempt to detach file from project that didn't include this file + return; + } + removeItemFromSet(this.containingProjects, project); + } + + detachAllProjects() { + for (const p of this.containingProjects) { + p.removeFile(this); + } + this.containingProjects.length = 0; + } + + getDefaultProject() { + Debug.assert(this.containingProjects.length !== 0); + return this.containingProjects[0]; + } + + setFormatOptions(formatSettings: protocol.FormatOptions): void { + if (formatSettings) { + mergeMaps(this.formatCodeSettings, formatSettings); + } + } + + setWatcher(watcher: FileWatcher): void { + this.stopWatcher(); + this.fileWatcher = watcher; + } + + stopWatcher() { + if (this.fileWatcher) { + this.fileWatcher.close(); + this.fileWatcher = undefined; + } + } + + getLatestVersion() { + return this.svc.latestVersion().toString(); + } + + reload(script: string) { + this.svc.reload(script); + this.markContainingProjectsAsDirty(); + } + + reloadFromFile(fileName: string, cb?: () => void) { + this.svc.reloadFromFile(fileName, cb) + this.markContainingProjectsAsDirty(); + } + + snap() { + return this.svc.getSnapshot(); + } + + getLineInfo(line: number) { + const snap = this.snap(); + return snap.index.lineNumberToInfo(line); + } + + editContent(start: number, end: number, newText: string): void { + this.svc.edit(start, end - start, newText); + this.markContainingProjectsAsDirty(); + } + + markContainingProjectsAsDirty() { + for (const p of this.containingProjects) { + p.markAsDirty(); + } + } + + /** + * @param line 1 based index + */ + lineToTextSpan(line: number) { + const index = this.snap().index; + const lineInfo = index.lineNumberToInfo(line + 1); + let len: number; + if (lineInfo.leaf) { + len = lineInfo.leaf.text.length; + } + else { + const nextLineInfo = index.lineNumberToInfo(line + 2); + len = nextLineInfo.offset - lineInfo.offset; + } + return ts.createTextSpan(lineInfo.offset, len); + } + + /** + * @param line 1 based index + * @param offset 1 based index + */ + lineOffsetToPosition(line: number, offset: number): number { + const index = this.snap().index; + + const lineInfo = index.lineNumberToInfo(line); + // TODO: assert this offset is actually on the line + return (lineInfo.offset + offset - 1); + } + + /** + * @param line 1-based index + * @param offset 1-based index + */ + positionToLineOffset(position: number): ILineInfo { + const index = this.snap().index; + const lineOffset = index.charOffsetToLineNumberAndPos(position); + return { line: lineOffset.line, offset: lineOffset.offset + 1 }; + } + } +} \ No newline at end of file diff --git a/src/server/scriptVersionCache.ts b/src/server/scriptVersionCache.ts index 4acb99b66d2..8269896059f 100644 --- a/src/server/scriptVersionCache.ts +++ b/src/server/scriptVersionCache.ts @@ -297,7 +297,7 @@ namespace ts.server { return this.currentVersion; } - reloadFromFile(filename: string, cb?: () => any) { + reloadFromFile(filename: string, cb?: () => void) { let content = this.host.readFile(filename); // If the file doesn't exist or cannot be read, we should // wipe out its cached content on the server to avoid side effects. diff --git a/src/server/session.ts b/src/server/session.ts index 237af69fffd..e3d7fff3068 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -86,7 +86,7 @@ namespace ts.server { } export interface PendingErrorCheck { - fileName: string; + fileName: NormalizedPath; project: Project; } @@ -187,7 +187,7 @@ namespace ts.server { }); } - private handleEvent(eventName: string, project: Project, fileName: string) { + private handleEvent(eventName: string, project: Project, fileName: NormalizedPath) { if (eventName == "context") { this.projectService.log("got context event, updating diagnostics for" + fileName, "Info"); this.updateErrorCheck([{ fileName, project }], this.changeSeq, @@ -370,16 +370,12 @@ namespace ts.server { } private getEncodedSemanticClassifications(args: protocol.FileSpanRequestArgs) { - const file = normalizePath(args.file); - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } + const { file, project } = this.getFileAndProject(args); return project.languageService.getEncodedSemanticClassifications(file, args); } private getProject(projectFileName: string) { - return projectFileName && this.projectService.getProject(projectFileName); + return projectFileName && this.projectService.findProject(projectFileName); } private getCompilerOptionsDiagnostics(args: protocol.ProjectRequestArgs) { @@ -400,11 +396,7 @@ namespace ts.server { } private getDiagnosticsWorker(args: protocol.FileRequestArgs, selector: (project: Project, file: string) => Diagnostic[]) { - const file = normalizePath(args.file); - const project = this.getProject(args.projectFileName) || this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } + const { project, file } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfo(file); const diagnostics = selector(project, file); return this.convertDiagnostics(diagnostics, scriptInfo); @@ -419,11 +411,7 @@ namespace ts.server { } private getDefinition(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.FileSpan[] | DefinitionInfo[] { - const file = ts.normalizePath(args.file); - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } + const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfo(file); const position = this.getPosition(args, scriptInfo); @@ -780,9 +768,9 @@ namespace ts.server { return args.position !== undefined ? args.position : scriptInfo.lineOffsetToPosition(args.line, args.offset); } - private getFileAndProject(fileName: string) { - const file = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(file); + private getFileAndProject(args: protocol.FileLocationRequestArgs) { + const file = ts.normalizePath(args.file); + const project: Project = this.getProject(args.projectFileName) || this.projectService.getProjectForFile(file); if (!project) { throw Errors.NoProject; } @@ -790,54 +778,49 @@ namespace ts.server { } private getOutliningSpans(args: protocol.FileRequestArgs) { - const { file, project } = this.getFileAndProject(args.file); + const { file, project } = this.getFileAndProject(args); return project.languageService.getOutliningSpans(file); } private getTodoComments(args: protocol.TodoCommentRequestArgs) { - const { file, project } = this.getFileAndProject(args.file); + const { file, project } = this.getFileAndProject(args); return project.languageService.getTodoComments(file, args.descriptors); } private getDocCommentTemplate(args: protocol.FileLocationRequestArgs) { - const { file, project } = this.getFileAndProject(args.file); + const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfo(file); const position = this.getPosition(args, scriptInfo); return project.languageService.getDocCommentTemplateAtPosition(file, position); } private getIndentation(args: protocol.IndentationRequestArgs) { - const { file, project } = this.getFileAndProject(args.file); + const { file, project } = this.getFileAndProject(args); const position = this.getPosition(args, project.getScriptInfo(file)); const indentation = project.languageService.getIndentationAtPosition(file, position, args.options); return { position, indentation }; } private getBreakpointStatement(args: protocol.FileLocationRequestArgs) { - const { file, project } = this.getFileAndProject(args.file); + const { file, project } = this.getFileAndProject(args); const position = this.getPosition(args, project.getScriptInfo(file)); return project.languageService.getBreakpointStatementAtPosition(file, position); } private getNameOrDottedNameSpan(args: protocol.FileLocationRequestArgs) { - const { file, project } = this.getFileAndProject(args.file); + const { file, project } = this.getFileAndProject(args); const position = this.getPosition(args, project.getScriptInfo(file)); return project.languageService.getNameOrDottedNameSpan(file, position, position); } private isValidBraceCompletion(args: protocol.BraceCompletionRequestArgs) { - const { file, project } = this.getFileAndProject(args.file); + const { file, project } = this.getFileAndProject(args); const position = this.getPosition(args, project.getScriptInfo(file)); return project.languageService.isValidBraceCompletionAtPostion(file, position, args.openingBrace.charCodeAt(0)); } private getQuickInfoWorker(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.QuickInfoResponseBody | QuickInfo { - const file = ts.normalizePath(args.file); - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } - + const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfo(file); const quickInfo = project.languageService.getQuickInfoAtPosition(file, this.getPosition(args, scriptInfo)); if (!quickInfo) { @@ -889,31 +872,17 @@ namespace ts.server { } private getFormattingEditsForRangeFull(args: protocol.FormatRequestArgs) { - const file = ts.normalizePath(args.file); - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } - + const { file, project } = this.getFileAndProject(args); return project.languageService.getFormattingEditsForRange(file, args.position, args.endPosition, args.options); } private getFormattingEditsForDocumentFull(args: protocol.FormatRequestArgs) { - const file = ts.normalizePath(args.file); - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } - + const { file, project } = this.getFileAndProject(args); return project.languageService.getFormattingEditsForDocument(file, args.options); } private getFormattingEditsAfterKeystrokeFull(args: protocol.FormatOnKeyRequestArgs) { - const file = ts.normalizePath(args.file); - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } + const { file, project } = this.getFileAndProject(args); return project.languageService.getFormattingEditsAfterKeystroke(file, args.position, args.key, args.options); } @@ -990,11 +959,7 @@ namespace ts.server { private getCompletions(args: protocol.CompletionsRequestArgs, simplifiedResult: boolean): protocol.CompletionEntry[] | CompletionInfo { const prefix = args.prefix || ""; - const file = ts.normalizePath(args.file); - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } + const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfo(file); const position = this.getPosition(args, scriptInfo); @@ -1017,12 +982,7 @@ namespace ts.server { } private getCompletionEntryDetails(args: protocol.CompletionDetailsRequestArgs): protocol.CompletionEntryDetails[] { - const file = ts.normalizePath(args.file); - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } - + const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfo(file); const position = this.getPosition(args, scriptInfo); @@ -1036,12 +996,7 @@ namespace ts.server { } private getSignatureHelpItems(args: protocol.SignatureHelpRequestArgs, simplifiedResult: boolean): protocol.SignatureHelpItems | SignatureHelpItems { - const file = ts.normalizePath(args.file); - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } - + const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfo(file); const position = this.getPosition(args, scriptInfo); const helpItems = project.languageService.getSignatureHelpItems(file, position); @@ -1069,6 +1024,7 @@ namespace ts.server { private getDiagnostics(delay: number, fileNames: string[]) { const checkList = fileNames.reduce((accum: PendingErrorCheck[], fileName: string) => { + fileName = ts.normalizePath(fileName); const project = this.projectService.getProjectForFile(fileName); if (project) { diff --git a/src/server/utilities.ts b/src/server/utilities.ts new file mode 100644 index 00000000000..6fbff20fa96 --- /dev/null +++ b/src/server/utilities.ts @@ -0,0 +1,175 @@ +/// +/// + +namespace ts.server { + export interface Logger { + close(): void; + isVerbose(): boolean; + loggingEnabled(): boolean; + perftrc(s: string): void; + info(s: string): void; + startGroup(): void; + endGroup(): void; + msg(s: string, type?: string): void; + } + + export function getDefaultFormatCodeSettings(host: ServerHost): FormatCodeSettings { + return { + indentSize: 4, + tabSize: 4, + newLineCharacter: host.newLine || "\n", + convertTabsToSpaces: true, + indentStyle: ts.IndentStyle.Smart, + insertSpaceAfterCommaDelimiter: true, + insertSpaceAfterSemicolonInForStatements: true, + insertSpaceBeforeAndAfterBinaryOperators: true, + insertSpaceAfterKeywordsInControlFlowStatements: true, + insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, + insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, + placeOpenBraceOnNewLineForFunctions: false, + placeOpenBraceOnNewLineForControlBlocks: false, + }; + } + + export function mergeMaps(target: Map, source: Map): void { + for (const key in source) { + if (hasProperty(source, key)) { + target[key] = source[key]; + } + } + } + + export function removeItemFromSet(items: T[], itemToRemove: T) { + const index = items.indexOf(itemToRemove); + if (index < 0) { + return; + } + if (items.length === 0) { + items.pop(); + } + else { + items[index] = items.pop(); + } + } + + + export type NormalizedPath = string & { __normalizedPathTag: any }; + + export function toNormalizedPath(fileName: string): NormalizedPath { + return normalizePath(fileName); + } + + export function asNormalizedPath(fileName: string): NormalizedPath { + return fileName; + } + + export function asNormalizedPathArray(fileNames: string[]): NormalizedPath[] { + return fileNames; + } + + export interface NormalizedPathMap { + get (path: NormalizedPath): T; + set (path: NormalizedPath, value: T): void; + contains(path: NormalizedPath): boolean; + remove(path: NormalizedPath): void; + } + + export function createNormalizedPathMap(): NormalizedPathMap { + const map: Map = Object.create(null); + return { + get(path) { + return map[path]; + }, + set(path, value) { + map[path] = value; + }, + contains(path) { + return hasProperty(map, path); + }, + remove(path) { + delete map[path] + } + } + } + function throwLanguageServiceIsDisabledError() {; + throw new Error("LanguageService is disabled"); + } + + export const nullLanguageService: LanguageService = { + cleanupSemanticCache: (): any => throwLanguageServiceIsDisabledError(), + getSyntacticDiagnostics: (): any => throwLanguageServiceIsDisabledError(), + getSemanticDiagnostics: (): any => throwLanguageServiceIsDisabledError(), + getCompilerOptionsDiagnostics: (): any => throwLanguageServiceIsDisabledError(), + getSyntacticClassifications: (): any => throwLanguageServiceIsDisabledError(), + getEncodedSyntacticClassifications: (): any => throwLanguageServiceIsDisabledError(), + getSemanticClassifications: (): any => throwLanguageServiceIsDisabledError(), + getEncodedSemanticClassifications: (): any => throwLanguageServiceIsDisabledError(), + getCompletionsAtPosition: (): any => throwLanguageServiceIsDisabledError(), + findReferences: (): any => throwLanguageServiceIsDisabledError(), + getCompletionEntryDetails: (): any => throwLanguageServiceIsDisabledError(), + getQuickInfoAtPosition: (): any => throwLanguageServiceIsDisabledError(), + findRenameLocations: (): any => throwLanguageServiceIsDisabledError(), + getNameOrDottedNameSpan: (): any => throwLanguageServiceIsDisabledError(), + getBreakpointStatementAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getBraceMatchingAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getSignatureHelpItems: (): any => throwLanguageServiceIsDisabledError(), + getDefinitionAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getRenameInfo: (): any => throwLanguageServiceIsDisabledError(), + getTypeDefinitionAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getReferencesAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getDocumentHighlights: (): any => throwLanguageServiceIsDisabledError(), + getOccurrencesAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getNavigateToItems: (): any => throwLanguageServiceIsDisabledError(), + getNavigationBarItems: (): any => throwLanguageServiceIsDisabledError(), + getOutliningSpans: (): any => throwLanguageServiceIsDisabledError(), + getTodoComments: (): any => throwLanguageServiceIsDisabledError(), + getIndentationAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getFormattingEditsForRange: (): any => throwLanguageServiceIsDisabledError(), + getFormattingEditsForDocument: (): any => throwLanguageServiceIsDisabledError(), + getFormattingEditsAfterKeystroke: (): any => throwLanguageServiceIsDisabledError(), + getDocCommentTemplateAtPosition: (): any => throwLanguageServiceIsDisabledError(), + isValidBraceCompletionAtPostion: (): any => throwLanguageServiceIsDisabledError(), + getEmitOutput: (): any => throwLanguageServiceIsDisabledError(), + getProgram: (): any => throwLanguageServiceIsDisabledError(), + getNonBoundSourceFile: (): any => throwLanguageServiceIsDisabledError(), + dispose: (): any => throwLanguageServiceIsDisabledError(), + }; + + export interface ServerLanguageServiceHost { + getCompilationSettings(): CompilerOptions; + setCompilationSettings(options: CompilerOptions): void; + removeRoot(info: ScriptInfo): void; + removeReferencedFile(info: ScriptInfo): void; + } + + export const nullLanguageServiceHost: ServerLanguageServiceHost = { + getCompilationSettings: () => undefined, + setCompilationSettings: () => undefined, + removeRoot: () => undefined, + removeReferencedFile: () => undefined + }; + + export interface ProjectOptions { + /** + * true if config file explicitly listed files + **/ + configHasFilesProperty?: boolean; + /** + * these fields can be present in the project file + **/ + files?: string[]; + wildcardDirectories?: Map; + compilerOptions?: CompilerOptions; + } + + export function isInferredProjectName(name: string) { + // POSIX defines /dev/null as a device - there should be no file with this prefix + return /dev\/null\/inferredProject\d+\*/.test(name); + } + + export function makeInferredProjectName(counter: number) { + return `/dev/null/inferredProject${counter}*`; + } +} \ No newline at end of file From c8d37dc87e12efa333cbc4bb42674300bac18cd3 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 22 Jun 2016 16:51:09 -0700 Subject: [PATCH 060/307] [in progress] project system work - versions --- src/server/editorServices.ts | 207 +++++++++------ src/server/project.ts | 188 ++++++++------ src/server/protocol.d.ts | 13 +- src/server/scriptInfo.ts | 8 +- src/server/session.ts | 241 +++++++----------- src/server/utilities.ts | 6 +- .../cases/unittests/cachingInServerLSHost.ts | 8 +- 7 files changed, 354 insertions(+), 317 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 08014379345..e111053315a 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -28,7 +28,7 @@ namespace ts.server { hostInfo: string; } - function findVersionedProjectByFileName(projectName: string, projects: T[]): T { + function findProjectByName(projectName: string, projects: T[]): T { for (const proj of projects) { if (proj.getProjectName() === projectName) { return proj; @@ -42,7 +42,54 @@ namespace ts.server { project?: Project; } + class DirectoryWatchers { + /** + * a path to directory watcher map that detects added tsconfig files + **/ + private directoryWatchersForTsconfig: ts.Map = {}; + /** + * count of how many projects are using the directory watcher. + * If the number becomes 0 for a watcher, then we should close it. + **/ + private directoryWatchersRefCount: ts.Map = {}; + + constructor(private readonly projectService: ProjectService) { + } + + stopWatchingDirectory(directory: string) { + // if the ref count for this directory watcher drops to 0, it's time to close it + this.directoryWatchersRefCount[directory]--; + if (this.directoryWatchersRefCount[directory] === 0) { + this.projectService.log("Close directory watcher for: " + directory); + this.directoryWatchersForTsconfig[directory].close(); + delete this.directoryWatchersForTsconfig[directory]; + } + } + + startWatchingContainingDirectoriesForFile(fileName: string, project: InferredProject, callback: (fileName: string) => void) { + let currentPath = ts.getDirectoryPath(fileName); + let parentPath = ts.getDirectoryPath(currentPath); + while (currentPath != parentPath) { + if (!this.directoryWatchersForTsconfig[currentPath]) { + this.projectService.log("Add watcher for: " + currentPath); + this.directoryWatchersForTsconfig[currentPath] = this.projectService.host.watchDirectory(currentPath, callback); + this.directoryWatchersRefCount[currentPath] = 1; + } + else { + this.directoryWatchersRefCount[currentPath] += 1; + } + project.directoriesWatchedForTsconfig.push(currentPath); + currentPath = parentPath; + parentPath = ts.getDirectoryPath(parentPath); + } + + } + } + export class ProjectService { + /** + * Container of all known scripts + */ private filenameToScriptInfo = createNormalizedPathMap(); /** * maps external project file name to list of config files that were the part of this project @@ -64,10 +111,9 @@ namespace ts.server { /** * open, non-configured root files **/ - openFileRoots: ScriptInfo[] = []; /** - * open files referenced by a project + * open files referenced by some project **/ openFilesReferenced: ScriptInfo[] = []; /** @@ -75,25 +121,19 @@ namespace ts.server { **/ openFileRootsConfigured: ScriptInfo[] = []; - /** - * a path to directory watcher map that detects added tsconfig files - **/ - private directoryWatchersForTsconfig: ts.Map = {}; - /** - * count of how many projects are using the directory watcher. - * If the number becomes 0 for a watcher, then we should close it. - **/ - private directoryWatchersRefCount: ts.Map = {}; + private directoryWatchers: DirectoryWatchers; private hostConfiguration: HostConfiguration; private timerForDetectingProjectFileListChanges: Map = {}; private documentRegistry: ts.DocumentRegistry; - constructor(public host: ServerHost, - public psLogger: Logger, - public cancellationToken: HostCancellationToken, - public eventHandler?: ProjectServiceEventHandler) { + constructor(public readonly host: ServerHost, + public readonly psLogger: Logger, + public readonly cancellationToken: HostCancellationToken, + private readonly eventHandler?: ProjectServiceEventHandler) { + + this.directoryWatchers = new DirectoryWatchers(this); // ts.disableIncrementalParsing = true; this.setDefaultHostConfiguration(); this.documentRegistry = ts.createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); @@ -107,13 +147,7 @@ namespace ts.server { } stopWatchingDirectory(directory: string) { - // if the ref count for this directory watcher drops to 0, it's time to close it - this.directoryWatchersRefCount[directory]--; - if (this.directoryWatchersRefCount[directory] === 0) { - this.log("Close directory watcher for: " + directory); - this.directoryWatchersForTsconfig[directory].close(); - delete this.directoryWatchersForTsconfig[directory]; - } + this.directoryWatchers.stopWatchingDirectory(directory); } findProject(projectName: string): Project { @@ -302,10 +336,18 @@ namespace ts.server { return undefined; } + private findContainingExternalProject(fileName: NormalizedPath): ExternalProject { + for (const proj of this.externalProjects) { + if (proj.containsFile(fileName)) { + return proj; + } + } + return undefined; + } + private addOpenFile(info: ScriptInfo) { const externalProject = this.findContainingExternalProject(info.fileName); if (externalProject) { - // info.defaultProject = externalProject; return; } const configuredProject = this.findContainingConfiguredProject(info); @@ -403,15 +445,6 @@ namespace ts.server { info.isOpen = false; } - private findContainingExternalProject(fileName: NormalizedPath): ExternalProject { - for (const proj of this.externalProjects) { - if (proj.containsFile(fileName)) { - return proj; - } - } - return undefined; - } - /** * This function 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 @@ -511,11 +544,11 @@ namespace ts.server { } private findConfiguredProjectByProjectName(configFileName: NormalizedPath) { - return findVersionedProjectByFileName(configFileName, this.configuredProjects); + return findProjectByName(configFileName, this.configuredProjects); } private findExternalProjectByProjectName(projectFileName: string) { - return findVersionedProjectByFileName(projectFileName, this.externalProjects); + return findProjectByName(projectFileName, this.externalProjects); } private configFileToProjectOptions(configFilename: string): { succeeded: boolean, projectOptions?: ProjectOptions, errors?: Diagnostic[] } { @@ -580,7 +613,7 @@ namespace ts.server { return { project, errors }; } - private createAndAddConfiguredProject(configFileName: string, projectOptions: ProjectOptions, clientFileName?: string) { + private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, clientFileName?: string) { const sizeLimitExceeded = this.exceedTotalNonTsFileSizeLimit(projectOptions.compilerOptions, projectOptions.files); const project = new ConfiguredProject( configFileName, @@ -611,7 +644,7 @@ namespace ts.server { let errors: Diagnostic[]; for (const rootFilename of files) { if (this.host.fileExists(rootFilename)) { - const info = this.getOrCreateScriptInfo(toNormalizedPath(rootFilename), /*openedByClient*/ clientFileName == rootFilename); + const info = this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(rootFilename), /*openedByClient*/ clientFileName == rootFilename); project.addRoot(info); } else { @@ -622,7 +655,7 @@ namespace ts.server { return errors; } - private openConfigFile(configFileName: string, clientFileName?: string): { success: boolean, project?: ConfiguredProject, errors?: Diagnostic[] } { + private openConfigFile(configFileName: NormalizedPath, clientFileName?: string): { success: boolean, project?: ConfiguredProject, errors?: Diagnostic[] } { const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(configFileName); if (!succeeded) { return { success: false, errors }; @@ -633,7 +666,7 @@ namespace ts.server { } } - private updateVersionedProjectWorker(project: VersionedProject, newRootFiles: string[], newOptions: CompilerOptions) { + private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newRootFiles: string[], newOptions: CompilerOptions) { const oldRootFiles = project.getRootFiles(); const newFileNames = asNormalizedPathArray(filter(newRootFiles, f => this.host.fileExists(f))); const fileNamesToRemove = oldRootFiles.filter(f => !contains(newFileNames, f)); @@ -649,7 +682,7 @@ namespace ts.server { for (const fileName of fileNamesToAdd) { let info = this.getScriptInfo(fileName); if (!info) { - info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); + info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false); } else { // if the root file was opened by client, it would belong to either @@ -710,7 +743,7 @@ namespace ts.server { project.enableLanguageService(); } this.watchConfigDirectoryForProject(project, projectOptions); - this.updateVersionedProjectWorker(project, projectOptions.files, projectOptions.compilerOptions); + this.updateNonInferredProject(project, projectOptions.files, projectOptions.compilerOptions); } } } @@ -720,21 +753,10 @@ namespace ts.server { const project = new InferredProject(this, this.documentRegistry, /*languageServiceEnabled*/ true); project.addRoot(root); - let currentPath = ts.getDirectoryPath(root.fileName); - let parentPath = ts.getDirectoryPath(currentPath); - while (currentPath != parentPath) { - if (!this.directoryWatchersForTsconfig[currentPath]) { - this.log("Add watcher for: " + currentPath); - this.directoryWatchersForTsconfig[currentPath] = this.host.watchDirectory(currentPath, fileName => this.onConfigChangeForInferredProject(fileName)); - this.directoryWatchersRefCount[currentPath] = 1; - } - else { - this.directoryWatchersRefCount[currentPath] += 1; - } - project.directoriesWatchedForTsconfig.push(currentPath); - currentPath = parentPath; - parentPath = ts.getDirectoryPath(parentPath); - } + this.directoryWatchers.startWatchingContainingDirectoriesForFile( + root.fileName, + project, + fileName => this.onConfigChangeForInferredProject(fileName)); project.updateGraph(); this.inferredProjects.push(project); @@ -745,7 +767,11 @@ namespace ts.server { * @param filename is absolute pathname * @param fileContent is a known version of the file content that is more up to date than the one on disk */ - getOrCreateScriptInfo(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { + + getOrCreateScriptInfo(fileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { + return this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(fileName), openedByClient, fileContent, scriptKind); + } + getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { let info = this.filenameToScriptInfo.get(fileName); if (!info) { let content: string; @@ -879,12 +905,37 @@ namespace ts.server { // the file to the open, referenced file list. const openFileRoots: ScriptInfo[] = []; for (const rootFile of this.openFileRoots) { + let inConfiguredProject = false; + let inExternalProject = false; + for (const p of rootFile.containingProjects) { + inConfiguredProject = inConfiguredProject || p.projectKind === ProjectKind.Configured; + inExternalProject = inExternalProject || p.projectKind === ProjectKind.External; + } + if (inConfiguredProject || inExternalProject) { + const inferredProjects = rootFile.containingProjects.filter(p => p.projectKind === ProjectKind.Inferred); + for (const p of inferredProjects) { + this.removeProject(p); + } + if (inConfiguredProject) { + this.openFileRootsConfigured.push(rootFile); + } + } + else { + // + openFileRoots.push(rootFile); + } + if (rootFile.containingProjects.some(p => p.projectKind !== ProjectKind.Inferred)) { + // file was included in non-inferred project - drop old inferred project - let inInferredProjectOnly = true; + } + else { + openFileRoots.push(rootFile); + } + let inferredProjectsToRemove: Project[]; for (const p of rootFile.containingProjects) { if (p.projectKind !== ProjectKind.Inferred) { // file was included in non-inferred project - drop old inferred project - inInferredProjectOnly = false; + infe break; } } @@ -892,6 +943,7 @@ namespace ts.server { openFileRoots.push(rootFile); } else { + } // const rootedProject = rootFile.defaultProject; @@ -941,17 +993,20 @@ namespace ts.server { * @param filename is absolute pathname * @param fileContent is a known version of the file content that is more up to date than the one on disk */ - openClientFile(uncheckedFileName: string, fileContent?: string, scriptKind?: ScriptKind): { configFileName?: string, configFileErrors?: Diagnostic[] } { + openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind): { configFileName?: string, configFileErrors?: Diagnostic[] } { + return this.openClientFileWithNormalizedPath(toNormalizedPath(fileName), fileContent, scriptKind); + } + + openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind): { configFileName?: string, configFileErrors?: Diagnostic[] } { let configFileName: string; let configFileErrors: Diagnostic[]; - const fileName = toNormalizedPath(uncheckedFileName); if (!this.findContainingExternalProject(fileName)) { ({ configFileName, configFileErrors } = this.openOrUpdateConfiguredProjectForFile(fileName)); } // at this point if file is the part of some configured/external project then this project should be created - const info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ true, fileContent, scriptKind); + const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind); this.addOpenFile(info); this.printProjects(); return { configFileName, configFileErrors }; @@ -970,27 +1025,23 @@ namespace ts.server { this.printProjects(); } - // getProjectForFile(filename: string) { - // const scriptInfo = ts.lookUp(this.filenameToScriptInfo, filename); - // if (scriptInfo) { - // return scriptInfo.defaultProject; - // } - // } + getDefaultProjectForFile(fileName: NormalizedPath) { + const scriptInfo = this.filenameToScriptInfo.get(fileName); + return scriptInfo && scriptInfo.getDefaultProject(); + } - private addExternalProjectFilesForVersionedProjects(knownProjects: protocol.ExternalProjectInfo[], projects: VersionedProject[], result: protocol.ExternalProjectFiles[]): void { + private syncExternalFilesList(knownProjects: protocol.ProjectVersionInfo[], projects: Project[], result: protocol.ProjectFiles[]): void { for (const proj of projects) { const knownProject = ts.forEach(knownProjects, p => p.projectName === proj.getProjectName() && p); result.push(proj.getChangesSinceVersion(knownProject && knownProject.version)); } } - synchronizeProjectList(knownProjects: protocol.ExternalProjectInfo[]): protocol.ExternalProjectFiles[] { - const files: protocol.ExternalProjectFiles[] = []; - this.addExternalProjectFilesForVersionedProjects(knownProjects, this.externalProjects, files); - this.addExternalProjectFilesForVersionedProjects(knownProjects, this.configuredProjects, files); - for (const inferredProject of this.inferredProjects) { - files.push({ files: inferredProject.getFileNames() }); - } + synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[]): protocol.ProjectFiles[] { + const files: protocol.ProjectFiles[] = []; + this.syncExternalFilesList(knownProjects, this.externalProjects, files); + this.syncExternalFilesList(knownProjects, this.configuredProjects, files); + this.syncExternalFilesList(knownProjects, this.inferredProjects, files); return files; } @@ -998,7 +1049,7 @@ namespace ts.server { for (const file of openFiles) { const scriptInfo = this.getScriptInfo(file.fileName); Debug.assert(!scriptInfo || !scriptInfo.isOpen); - this.openClientFile(file.fileName, file.content); + this.openClientFileWithNormalizedPath(toNormalizedPath(file.fileName), file.content); } for (const file of changedFiles) { @@ -1042,7 +1093,7 @@ namespace ts.server { openExternalProject(proj: protocol.ExternalProject): void { const externalProject = this.findExternalProjectByProjectName(proj.projectFileName); if (externalProject) { - this.updateVersionedProjectWorker(externalProject, proj.rootFiles, proj.options); + this.updateNonInferredProject(externalProject, proj.rootFiles, proj.options); } else { let tsConfigFiles: NormalizedPath[]; diff --git a/src/server/project.ts b/src/server/project.ts index 1b122802f56..0a36c3ee62f 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1,4 +1,5 @@ /// +/// /// /// @@ -11,13 +12,33 @@ namespace ts.server { export abstract class Project { private rootFiles: ScriptInfo[] = []; - private rootFilesMap: FileMap = createFileMap(); + private readonly rootFilesMap: FileMap = createFileMap(); private lsHost: ServerLanguageServiceHost; - protected program: ts.Program; - private version = 0; + private program: ts.Program; languageService: LanguageService; + /** + * Set of files that was returned from the last call to getChangesSinceVersion. + */ + private lastReportedFileNames: Map; + /** + * Last version that was reported. + */ + private lastReportedVersion = 0; + /** + * Current project structure version. + * This property is changed in 'updateGraph' based on the set of files in program + */ + private projectStructureVersion = 0; + /** + * Current version of the project state. It is changed when: + * - new root file was added/removed + * - edit happen in some file that is currently included in the project. + * This property is different from projectStructureVersion since in most cases edits don't affect set of files in the project + */ + private projectStateVersion = 0; + constructor( readonly projectKind: ProjectKind, readonly projectService: ProjectService, @@ -46,7 +67,7 @@ namespace ts.server { } getProjectVersion() { - return this.version.toString(); + return this.projectStateVersion.toString(); } enableLanguageService() { @@ -68,8 +89,8 @@ namespace ts.server { close() { for (const fileName of this.getFileNames()) { - const info = this.projectKind.getScriptInfoForNormalizedPath(fileName); - info.detachFromProject(project); + const info = this.projectService.getScriptInfoForNormalizedPath(fileName); + info.detachFromProject(this); } // signal language service to release files acquired from document registry this.languageService.dispose(); @@ -85,6 +106,10 @@ namespace ts.server { } getFileNames() { + if (!this.program) { + return []; + } + if (!this.languageServiceEnabled) { // if language service is disabled assume that all files in program are root files + default library let rootFiles = this.getRootFiles(); @@ -128,16 +153,18 @@ namespace ts.server { } } - removeFile(info: ScriptInfo) { + removeFile(info: ScriptInfo, detachFromProject: boolean = true) { if (!this.removeRoot(info)) { this.removeReferencedFile(info) } - info.detachFromProject(this); + if (detachFromProject) { + info.detachFromProject(this); + } this.markAsDirty(); } markAsDirty() { - this.version++; + this.projectStateVersion++; } // remove a root file from project @@ -153,21 +180,36 @@ namespace ts.server { private removeReferencedFile(info: ScriptInfo) { this.lsHost.removeReferencedFile(info) - this.updateGraph(); } updateGraph() { + if (!this.languageServiceEnabled) { + return; + } + + const oldProgram = this.program; this.program = this.languageService.getProgram(); + + // bump up the version if + // - oldProgram is not set - this is a first time updateGraph is called + // - newProgram is different from the old program and structure of the old program was not reused. + if (!oldProgram || (this.program !== oldProgram && !oldProgram.structureIsReused)) { + this.projectStructureVersion++; + } } - getScriptInfo(uncheckedFileName: string) { - const scriptInfo = this.projectService.getOrCreateScriptInfo(toNormalizedPath(uncheckedFileName), /*openedByClient*/ false); - if (scriptInfo.attachToProject(this)) { + getScriptInfoFromNormalizedPath(fileName: NormalizedPath) { + const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false); + if (scriptInfo && scriptInfo.attachToProject(this)) { this.markAsDirty(); } return scriptInfo; } + getScriptInfo(uncheckedFileName: string) { + return this.getScriptInfoFromNormalizedPath(toNormalizedPath(uncheckedFileName)); + } + filesToString() { if (!this.program) { return ""; @@ -197,19 +239,66 @@ namespace ts.server { } } - reloadScript(filename: string, tmpfilename: string, cb: () => void) { - const script = this.getScriptInfo(filename); + reloadScript(filename: NormalizedPath, cb: () => void) { + const script = this.getScriptInfoFromNormalizedPath(filename); if (script) { script.reloadFromFile(filename, cb); } } + + getChangesSinceVersion(lastKnownVersion?: number): protocol.ProjectFiles { + const info = { + projectName: this.getProjectName(), + version: this.projectStructureVersion, + isInferred: this.projectKind === ProjectKind.Inferred + }; + // check if requested version is the same that we have reported last time + if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) { + // if current structure version is the same - return info witout any changes + if (this.projectStructureVersion == this.lastReportedVersion) { + return { info }; + } + // compute and return the difference + const lastReportedFileNames = this.lastReportedFileNames; + const currentFiles = arrayToMap(this.getFileNames(), x => x); + + const added: string[] = []; + const removed: string[] = []; + for (const id in currentFiles) { + if (hasProperty(currentFiles, id) && !hasProperty(lastReportedFileNames, id)) { + added.push(id); + } + } + for (const id in lastReportedFileNames) { + if (hasProperty(lastReportedFileNames, id) && !hasProperty(currentFiles, id)) { + removed.push(id); + } + } + this.lastReportedFileNames = currentFiles; + + this.lastReportedFileNames = currentFiles; + this.lastReportedVersion = this.projectStructureVersion; + return { info, changes: { added, removed } }; + } + else { + // unknown version - return everything + const projectFileNames = this.getFileNames(); + this.lastReportedFileNames = arrayToMap(projectFileNames, x => x); + this.lastReportedVersion = this.projectStructureVersion; + return { info, files: projectFileNames }; + } + } } export class InferredProject extends Project { - static NextId = 0; + private static NextId = 1; + + /** + * Unique name that identifies this particular inferred project + */ + private readonly inferredProjectName: string; - readonly inferredProjectName; // Used to keep track of what directories are watched for this project directoriesWatchedForTsconfig: string[] = []; @@ -237,73 +326,14 @@ namespace ts.server { } } - export abstract class VersionedProject extends Project { - - private lastReportedFileNames: Map; - private lastReportedVersion: number = 0; - currentVersion: number = 1; - - updateGraph() { - if (!this.languageServiceEnabled) { - return; - } - const oldProgram = this.program; - - super.updateGraph(); - - if (!oldProgram || !oldProgram.structureIsReused) { - this.currentVersion++; - } - } - - getChangesSinceVersion(lastKnownVersion?: number): protocol.ExternalProjectFiles { - const info = { - projectName: this.getProjectName(), - version: this.currentVersion - }; - if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) { - if (this.currentVersion == this.lastReportedVersion) { - return { info }; - } - const lastReportedFileNames = this.lastReportedFileNames; - const currentFiles = arrayToMap(this.getFileNames(), x => x); - - const added: string[] = []; - const removed: string[] = []; - for (const id in currentFiles) { - if (hasProperty(currentFiles, id) && !hasProperty(lastReportedFileNames, id)) { - added.push(id); - } - } - for (const id in lastReportedFileNames) { - if (hasProperty(lastReportedFileNames, id) && !hasProperty(currentFiles, id)) { - removed.push(id); - } - } - this.lastReportedFileNames = currentFiles; - - this.lastReportedFileNames = currentFiles; - this.lastReportedVersion = this.currentVersion; - return { info, changes: { added, removed } }; - } - else { - // unknown version - return everything - const projectFileNames = this.getFileNames(); - this.lastReportedFileNames = arrayToMap(projectFileNames, x => x); - this.lastReportedVersion = this.currentVersion; - return { info, files: projectFileNames }; - } - } - } - - export class ConfiguredProject extends VersionedProject { + export class ConfiguredProject extends Project { private projectFileWatcher: FileWatcher; private directoryWatcher: FileWatcher; private directoriesWatchedForWildcards: Map; /** Used for configured projects which may have multiple open roots */ openRefCount = 0; - constructor(readonly configFileName: string, + constructor(readonly configFileName: NormalizedPath, projectService: ProjectService, documentRegistry: ts.DocumentRegistry, hasExplicitListOfFiles: boolean, @@ -380,7 +410,7 @@ namespace ts.server { } } - export class ExternalProject extends VersionedProject { + export class ExternalProject extends Project { constructor(readonly externalProjectName: string, projectService: ProjectService, documentRegistry: ts.DocumentRegistry, diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index e1eb1ecb015..a4e9062717a 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -494,12 +494,13 @@ declare namespace ts.server.protocol { options: CompilerOptions; } - export interface ExternalProjectInfo { + export interface ProjectVersionInfo { projectName: string; + isInferred: boolean; version: number; } - export interface ExternalProjectChanges { + export interface ProjectChanges { added: string[]; removed: string[]; } @@ -511,10 +512,10 @@ declare namespace ts.server.protocol { * if changes is set - then this is the set of changes that should be applied to existing project * otherwise - assume that nothing is changed */ - export interface ExternalProjectFiles { - info?: ExternalProjectInfo; + export interface ProjectFiles { + info?: ProjectVersionInfo; files?: string[]; - changes?: ExternalProjectChanges; + changes?: ProjectChanges; } /** @@ -674,7 +675,7 @@ declare namespace ts.server.protocol { } export interface SynchronizeProjectListRequestArgs { - knownProjects: protocol.ExternalProjectInfo[]; + knownProjects: protocol.ProjectVersionInfo[]; } export interface ApplyChangedToOpenFilesRequest extends Request { diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index ddf8008600e..52e1baf36b4 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -37,17 +37,13 @@ namespace ts.server { } detachFromProject(project: Project) { - const index = this.containingProjects.indexOf(project); - if (index < 0) { - // TODO: (assert?) attempt to detach file from project that didn't include this file - return; - } removeItemFromSet(this.containingProjects, project); } detachAllProjects() { for (const p of this.containingProjects) { - p.removeFile(this); + // detach is unnecessary since we'll clean the list of containing projects anyways + p.removeFile(this, /*detachFromProjects*/ false); } this.containingProjects.length = 0; } diff --git a/src/server/session.ts b/src/server/session.ts index e3d7fff3068..8a480580ddd 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -397,7 +397,7 @@ namespace ts.server { private getDiagnosticsWorker(args: protocol.FileRequestArgs, selector: (project: Project, file: string) => Diagnostic[]) { const { project, file } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfo(file); + const scriptInfo = project.getScriptInfoFromNormalizedPath(file); const diagnostics = selector(project, file); return this.convertDiagnostics(diagnostics, scriptInfo); } @@ -436,15 +436,10 @@ namespace ts.server { } } - private getTypeDefinition(line: number, offset: number, fileName: string): protocol.FileSpan[] { - const file = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } - - const scriptInfo = project.getScriptInfo(file); - const position = scriptInfo.lineOffsetToPosition(line, offset); + private getTypeDefinition(args: protocol.FileLocationRequestArgs): protocol.FileSpan[] { + const { file, project } = this.getFileAndProject(args) + const scriptInfo = project.getScriptInfoFromNormalizedPath(file); + const position = this.getPosition(args, scriptInfo); const definitions = project.languageService.getTypeDefinitionAtPosition(file, position); if (!definitions) { @@ -461,18 +456,12 @@ namespace ts.server { }); } - private getOccurrences(line: number, offset: number, fileName: string): protocol.OccurrencesResponseItem[] { - fileName = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(fileName); + private getOccurrences(args: protocol.FileLocationRequestArgs): protocol.OccurrencesResponseItem[] { + const { file, project } = this.getFileAndProject(args); + const scriptInfo = project.getScriptInfoFromNormalizedPath(file); + const position = this.getPosition(args, scriptInfo); - if (!project) { - throw Errors.NoProject; - } - - const scriptInfo = project.getScriptInfo(fileName); - const position = scriptInfo.lineOffsetToPosition(line, offset); - - const occurrences = project.languageService.getOccurrencesAtPosition(fileName, position); + const occurrences = project.languageService.getOccurrencesAtPosition(file, position); if (!occurrences) { return undefined; @@ -493,17 +482,11 @@ namespace ts.server { } private getDocumentHighlights(args: protocol.DocumentHighlightsRequestArgs, simplifiedResult: boolean): protocol.DocumentHighlightsItem[] | DocumentHighlights[] { - const fileName = ts.normalizePath(args.file); - const project = this.projectService.getProjectForFile(fileName); - - if (!project) { - throw Errors.NoProject; - } - - const scriptInfo = project.getScriptInfo(fileName); + const { file, project } = this.getFileAndProject(args); + const scriptInfo = project.getScriptInfoFromNormalizedPath(file); const position = this.getPosition(args, scriptInfo); - const documentHighlights = project.languageService.getDocumentHighlights(fileName, position, args.filesToSearch); + const documentHighlights = project.languageService.getDocumentHighlights(file, position, args.filesToSearch); if (!documentHighlights) { return undefined; @@ -534,27 +517,23 @@ namespace ts.server { } } - private getProjectInfo(fileName: string, needFileNameList: boolean): protocol.ProjectInfo { - fileName = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(fileName); - if (!project) { - throw Errors.NoProject; - } + private getProjectInfo(args: protocol.ProjectInfoRequestArgs): protocol.ProjectInfo { + return this.getProjectInfoWorker(args.file, args.projectFileName, args.needFileNameList); + } - const projectInfo: protocol.ProjectInfo = { - configFileName: project.getProjectFileName(), - languageServiceDisabled: !project.languageServiceEnabled + private getProjectInfoWorker(uncheckedFileName: string, projectFileName: string, needFileNameList: boolean) { + const { file, project } = this.getFileAndProjectWorker(uncheckedFileName, projectFileName, /*errorOnMissingProject*/ true); + const projectInfo = { + configFileName: project.getProjectName(), + languageServiceDisabled: !project.languageServiceEnabled, + fileNames: needFileNameList ? project.getFileNames() : undefined }; - - if (needFileNameList) { - projectInfo.fileNames = project.getFileNames(); - } return projectInfo; } private getRenameInfo(args: protocol.FileLocationRequestArgs) { - const { file, project } = this.getFileAndProject(args.file); - const scriptInfo = project.getScriptInfo(file); + const { file, project } = this.getFileAndProject(args); + const scriptInfo = project.getScriptInfoFromNormalizedPath(file); const position = this.getPosition(args, scriptInfo); return project.languageService.getRenameInfo(file, position); } @@ -568,10 +547,10 @@ namespace ts.server { } } else { - const file = normalizePath(args.file); - const info = this.projectService.getScriptInfo(file); - projects = this.projectService.findReferencingProjects(info); + const scriptInfo = this.projectService.getScriptInfo(args.file); + projects = scriptInfo.containingProjects; } + // ts.filter handles case when 'projects' is undefined projects = filter(projects, p => p.languageServiceEnabled); if (!projects || !projects.length) { throw Errors.NoProject; @@ -756,9 +735,8 @@ namespace ts.server { * @param fileName is the name of the file to be opened * @param fileContent is a version of the file content that is known to be more up to date than the one on disk */ - private openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind) { - const file = ts.normalizePath(fileName); - const { configFileName, configFileErrors } = this.projectService.openClientFile(file, fileContent, scriptKind); + private openClientFile(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind) { + const { configFileName, configFileErrors } = this.projectService.openClientFileWithNormalizedPath(fileName, fileContent, scriptKind); if (configFileErrors) { this.configFileDiagnosticEvent(fileName, configFileName, configFileErrors); } @@ -768,10 +746,14 @@ namespace ts.server { return args.position !== undefined ? args.position : scriptInfo.lineOffsetToPosition(args.line, args.offset); } - private getFileAndProject(args: protocol.FileLocationRequestArgs) { - const file = ts.normalizePath(args.file); - const project: Project = this.getProject(args.projectFileName) || this.projectService.getProjectForFile(file); - if (!project) { + private getFileAndProject(args: protocol.FileRequestArgs, errorOnMissingProject = true) { + return this.getFileAndProjectWorker(args.file, args.projectFileName, errorOnMissingProject); + } + + private getFileAndProjectWorker(uncheckedFileName: string, projectFileName: string, errorOnMissingProject: boolean) { + const file = toNormalizedPath(uncheckedFileName); + const project: Project = this.getProject(projectFileName) || this.projectService.getDefaultProjectForFile(file); + if (!project && errorOnMissingProject) { throw Errors.NoProject; } return { file, project }; @@ -844,16 +826,12 @@ namespace ts.server { } } - private getFormattingEditsForRange(line: number, offset: number, endLine: number, endOffset: number, fileName: string): protocol.CodeEdit[] { - const file = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } - + private getFormattingEditsForRange(args: protocol.FormatRequestArgs): protocol.CodeEdit[] { + const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfo(file); - const startPosition = scriptInfo.lineOffsetToPosition(line, offset); - const endPosition = scriptInfo.lineOffsetToPosition(endLine, endOffset); + + const startPosition = scriptInfo.lineOffsetToPosition(args.line, args.offset); + const endPosition = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); // TODO: avoid duplicate code (with formatonkey) const edits = project.languageService.getFormattingEditsForRange(file, startPosition, endPosition, @@ -886,18 +864,12 @@ namespace ts.server { return project.languageService.getFormattingEditsAfterKeystroke(file, args.position, args.key, args.options); } - private getFormattingEditsAfterKeystroke(line: number, offset: number, key: string, fileName: string): protocol.CodeEdit[] { - const file = ts.normalizePath(fileName); - - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } - + private getFormattingEditsAfterKeystroke(args: protocol.FormatOnKeyRequestArgs): protocol.CodeEdit[] { + const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfo(file); - const position = scriptInfo.lineOffsetToPosition(line, offset); + const position = scriptInfo.lineOffsetToPosition(args.line, args.offset); const formatOptions = this.projectService.getFormatCodeOptions(file); - const edits = project.languageService.getFormattingEditsAfterKeystroke(file, position, key, + const edits = project.languageService.getFormattingEditsAfterKeystroke(file, position, args.key, formatOptions); // Check whether we should auto-indent. This will be when // the position is on a line containing only whitespace. @@ -905,8 +877,8 @@ namespace ts.server { // getFormattingEditsAfterKeystroke either empty or pertaining // only to the previous line. If all this is true, then // add edits necessary to properly indent the current line. - if ((key == "\n") && ((!edits) || (edits.length === 0) || allEditsBeforePos(edits, position))) { - const lineInfo = scriptInfo.getLineInfo(line); + if ((args.key == "\n") && ((!edits) || (edits.length === 0) || allEditsBeforePos(edits, position))) { + const lineInfo = scriptInfo.getLineInfo(args.line); if (lineInfo && (lineInfo.leaf) && (lineInfo.leaf.text)) { const lineText = lineInfo.leaf.text; if (lineText.search("\\S") < 0) { @@ -1023,10 +995,9 @@ namespace ts.server { } private getDiagnostics(delay: number, fileNames: string[]) { - const checkList = fileNames.reduce((accum: PendingErrorCheck[], fileName: string) => { - - fileName = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(fileName); + const checkList = fileNames.reduce((accum: PendingErrorCheck[], uncheckedFileName: string) => { + const fileName = toNormalizedPath(uncheckedFileName); + const project = this.projectService.getDefaultProjectForFile(fileName); if (project) { accum.push({ fileName, project }); } @@ -1038,39 +1009,37 @@ namespace ts.server { } } - private change(line: number, offset: number, endLine: number, endOffset: number, insertString: string, fileName: string) { - const file = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(file); + private change(args: protocol.ChangeRequestArgs) { + const { file, project } = this.getFileAndProject(args, /*errorOnMissingProject*/ false); if (project) { const scriptInfo = project.getScriptInfo(file); - const start = scriptInfo.lineOffsetToPosition(line, offset); - const end = scriptInfo.lineOffsetToPosition(endLine, endOffset); + const start = scriptInfo.lineOffsetToPosition(args.line, args.offset); + const end = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); if (start >= 0) { - scriptInfo.editContent(start, end, insertString); + scriptInfo.editContent(start, end, args.insertString); this.changeSeq++; } this.updateProjectStructure(this.changeSeq, (n) => n === this.changeSeq); } } - private reload(fileName: string, tempFileName: string, reqSeq = 0) { - const file = ts.normalizePath(fileName); - const tmpfile = ts.normalizePath(tempFileName); - const project = this.projectService.getProjectForFile(file); + private reload(args: protocol.ReloadRequestArgs, reqSeq: number) { + const file = toNormalizedPath(args.file); + const project = this.projectService.getDefaultProjectForFile(file); if (project) { this.changeSeq++; // make sure no changes happen before this one is finished - project.reloadScript(file, tmpfile, () => { + project.reloadScript(file, () => { this.output(undefined, CommandNames.Reload, reqSeq); }); } } private saveToTmp(fileName: string, tempFileName: string) { - const file = ts.normalizePath(fileName); + const file = toNormalizedPath(fileName); const tmpfile = ts.normalizePath(tempFileName); - const project = this.projectService.getProjectForFile(file); + const project = this.projectService.getDefaultProjectForFile(file); if (project) { project.saveTo(file, tmpfile); } @@ -1084,12 +1053,12 @@ namespace ts.server { this.projectService.closeClientFile(file); } - private decorateNavigationBarItem(project: Project, fileName: string, items: ts.NavigationBarItem[]): protocol.NavigationBarItem[] { + private decorateNavigationBarItem(project: Project, fileName: NormalizedPath, items: ts.NavigationBarItem[]): protocol.NavigationBarItem[] { if (!items) { return undefined; } - const scriptInfo = project.getScriptInfo(fileName); + const scriptInfo = project.getScriptInfoFromNormalizedPath(fileName); return items.map(item => ({ text: item.text, @@ -1104,16 +1073,15 @@ namespace ts.server { })); } - private getNavigationBarItems(fileName: string, simplifiedResult: boolean): protocol.NavigationBarItem[] | NavigationBarItem[] { - const { file, project } = this.getFileAndProject(fileName); - + private getNavigationBarItems(args: protocol.FileRequestArgs, simplifiedResult: boolean): protocol.NavigationBarItem[] | NavigationBarItem[] { + const { file, project } = this.getFileAndProject(args); const items = project.languageService.getNavigationBarItems(file); if (!items) { return undefined; } return simplifiedResult - ? this.decorateNavigationBarItem(project, fileName, items) + ? this.decorateNavigationBarItem(project, file, items) : items; } @@ -1197,7 +1165,7 @@ namespace ts.server { } private getBraceMatching(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.TextSpan[] | TextSpan[] { - const { file, project } = this.getFileAndProject(args.file); + const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfo(file); const position = this.getPosition(args, scriptInfo); @@ -1219,7 +1187,7 @@ namespace ts.server { } getDiagnosticsForProject(delay: number, fileName: string) { - const { fileNames, languageServiceDisabled } = this.getProjectInfo(fileName, /*needFileNameList*/ true); + const { fileNames, languageServiceDisabled } = this.getProjectInfoWorker(fileName, /*projectFileName*/ undefined, /*needFileNameList*/ true); if (languageServiceDisabled) { return; } @@ -1228,12 +1196,12 @@ namespace ts.server { let fileNamesInProject = fileNames.filter((value, index, array) => value.indexOf("lib.d.ts") < 0); // Sort the file name list to make the recently touched files come first - const highPriorityFiles: string[] = []; - const mediumPriorityFiles: string[] = []; - const lowPriorityFiles: string[] = []; - const veryLowPriorityFiles: string[] = []; - const normalizedFileName = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(normalizedFileName); + const highPriorityFiles: NormalizedPath[] = []; + const mediumPriorityFiles: NormalizedPath[] = []; + const lowPriorityFiles: NormalizedPath[] = []; + const veryLowPriorityFiles: NormalizedPath[] = []; + const normalizedFileName = toNormalizedPath(fileName); + const project = this.projectService.getDefaultProjectForFile(normalizedFileName); for (const fileNameInProject of fileNamesInProject) { if (this.getCanonicalFileName(fileNameInProject) == this.getCanonicalFileName(fileName)) highPriorityFiles.push(fileNameInProject); @@ -1253,10 +1221,7 @@ namespace ts.server { fileNamesInProject = highPriorityFiles.concat(mediumPriorityFiles).concat(lowPriorityFiles).concat(veryLowPriorityFiles); if (fileNamesInProject.length > 0) { - const checkList = fileNamesInProject.map((fileName: string) => { - const normalizedFileName = ts.normalizePath(fileName); - return { fileName: normalizedFileName, project }; - }); + const checkList = fileNamesInProject.map(fileName => ({ fileName, project })); // Project level error analysis runs on background files too, therefore // doesn't require the file to be opened this.updateErrorCheck(checkList, this.changeSeq, (n) => n == this.changeSeq, delay, 200, /*requireOpen*/ false); @@ -1316,9 +1281,8 @@ namespace ts.server { [CommandNames.DefinitionFull]: (request: protocol.DefinitionRequest) => { return this.requiredResponse(this.getDefinition(request.arguments, /*simplifiedResult*/ false)); }, - [CommandNames.TypeDefinition]: (request: protocol.Request) => { - const defArgs = request.arguments; - return this.requiredResponse(this.getTypeDefinition(defArgs.line, defArgs.offset, defArgs.file)); + [CommandNames.TypeDefinition]: (request: protocol.FileLocationRequest) => { + return this.requiredResponse(this.getTypeDefinition(request.arguments)); }, [CommandNames.References]: (request: protocol.FileLocationRequest) => { return this.requiredResponse(this.getReferences(request.arguments, /*simplifiedResult*/ true)); @@ -1352,7 +1316,7 @@ namespace ts.server { scriptKind = ScriptKind.JSX; break; } - this.openClientFile(openArgs.file, openArgs.fileContent, scriptKind); + this.openClientFile(toNormalizedPath(openArgs.file), openArgs.fileContent, scriptKind); return this.notRequired(); }, [CommandNames.Quickinfo]: (request: protocol.QuickInfoRequest) => { @@ -1382,13 +1346,11 @@ namespace ts.server { [CommandNames.DocCommentTemplate]: (request: protocol.FileLocationRequest) => { return this.requiredResponse(this.getDocCommentTemplate(request.arguments)); }, - [CommandNames.Format]: (request: protocol.Request) => { - const formatArgs = request.arguments; - return this.requiredResponse(this.getFormattingEditsForRange(formatArgs.line, formatArgs.offset, formatArgs.endLine, formatArgs.endOffset, formatArgs.file)); + [CommandNames.Format]: (request: protocol.FormatRequest) => { + return this.requiredResponse(this.getFormattingEditsForRange(request.arguments)); }, - [CommandNames.Formatonkey]: (request: protocol.Request) => { - const formatOnKeyArgs = request.arguments; - return this.requiredResponse(this.getFormattingEditsAfterKeystroke(formatOnKeyArgs.line, formatOnKeyArgs.offset, formatOnKeyArgs.key, formatOnKeyArgs.file)); + [CommandNames.Formatonkey]: (request: protocol.FormatOnKeyRequest) => { + return this.requiredResponse(this.getFormattingEditsAfterKeystroke(request.arguments)); }, [CommandNames.FormatFull]: (request: protocol.FormatRequest) => { return this.requiredResponse(this.getFormattingEditsForDocumentFull(request.arguments)); @@ -1438,32 +1400,29 @@ namespace ts.server { const { file, delay } = request.arguments; return { response: this.getDiagnosticsForProject(delay, file), responseRequired: false }; }, - [CommandNames.Change]: (request: protocol.Request) => { - const changeArgs = request.arguments; - this.change(changeArgs.line, changeArgs.offset, changeArgs.endLine, changeArgs.endOffset, - changeArgs.insertString, changeArgs.file); - return { responseRequired: false }; + [CommandNames.Change]: (request: protocol.ChangeRequest) => { + this.change(request.arguments); + return this.notRequired(); }, [CommandNames.Configure]: (request: protocol.Request) => { const configureArgs = request.arguments; this.projectService.setHostConfiguration(configureArgs); this.output(undefined, CommandNames.Configure, request.seq); - return { responseRequired: false }; + return this.notRequired(); }, - [CommandNames.Reload]: (request: protocol.Request) => { - const reloadArgs = request.arguments; - this.reload(reloadArgs.file, reloadArgs.tmpfile, request.seq); - return { response: { reloadFinished: true }, responseRequired: true }; + [CommandNames.Reload]: (request: protocol.ReloadRequest) => { + this.reload(request.arguments, request.seq); + return this.requiredResponse({ reloadFinished: true }); }, [CommandNames.Saveto]: (request: protocol.Request) => { const savetoArgs = request.arguments; this.saveToTmp(savetoArgs.file, savetoArgs.tmpfile); - return { responseRequired: false }; + return this.notRequired(); }, [CommandNames.Close]: (request: protocol.Request) => { const closeArgs = request.arguments; this.closeClientFile(closeArgs.file); - return { responseRequired: false }; + return this.notRequired(); }, [CommandNames.Navto]: (request: protocol.NavtoRequest) => { return this.requiredResponse(this.getNavigateToItems(request.arguments, /*simplifiedResult*/ true)); @@ -1478,14 +1437,13 @@ namespace ts.server { return this.requiredResponse(this.getBraceMatching(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.NavBar]: (request: protocol.FileRequest) => { - return this.requiredResponse(this.getNavigationBarItems(request.arguments.file, /*simplifiedResult*/ true)); + return this.requiredResponse(this.getNavigationBarItems(request.arguments, /*simplifiedResult*/ true)); }, [CommandNames.NavBarFull]: (request: protocol.FileRequest) => { - return this.requiredResponse(this.getNavigationBarItems(request.arguments.file, /*simplifiedResult*/ false)); + return this.requiredResponse(this.getNavigationBarItems(request.arguments, /*simplifiedResult*/ false)); }, - [CommandNames.Occurrences]: (request: protocol.Request) => { - const { line, offset, file: fileName } = request.arguments; - return { response: this.getOccurrences(line, offset, fileName), responseRequired: true }; + [CommandNames.Occurrences]: (request: protocol.FileLocationRequest) => { + return this.requiredResponse(this.getOccurrences(request.arguments));; }, [CommandNames.DocumentHighlights]: (request: protocol.DocumentHighlightsRequest) => { return this.requiredResponse(this.getDocumentHighlights(request.arguments, /*simplifiedResult*/ true)); @@ -1493,13 +1451,12 @@ namespace ts.server { [CommandNames.DocumentHighlightsFull]: (request: protocol.DocumentHighlightsRequest) => { return this.requiredResponse(this.getDocumentHighlights(request.arguments, /*simplifiedResult*/ false)); }, - [CommandNames.ProjectInfo]: (request: protocol.Request) => { - const { file, needFileNameList } = request.arguments; - return { response: this.getProjectInfo(file, needFileNameList), responseRequired: true }; + [CommandNames.ProjectInfo]: (request: protocol.ProjectInfoRequest) => { + return this.requiredResponse(this.getProjectInfo(request.arguments)); }, [CommandNames.ReloadProjects]: (request: protocol.ReloadProjectsRequest) => { this.reloadProjects(); - return { responseRequired: false }; + return this.notRequired(); } }; public addProtocolHandler(command: string, handler: (request: protocol.Request) => { response?: any, responseRequired: boolean }) { diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 6fbff20fa96..c6da27505ac 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -1,5 +1,4 @@ /// -/// namespace ts.server { export interface Logger { @@ -42,11 +41,14 @@ namespace ts.server { } export function removeItemFromSet(items: T[], itemToRemove: T) { + if (items.length === 0) { + return; + } const index = items.indexOf(itemToRemove); if (index < 0) { return; } - if (items.length === 0) { + if (items.length === 1) { items.pop(); } else { diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index d0f63e7c339..2402102496d 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -118,7 +118,7 @@ namespace ts { const newContent = `import {x} from "f1" var x: string = 1;`; - rootScriptInfo.editContent(0, rootScriptInfo.content.length, newContent); + rootScriptInfo.editContent(0, root.content.length, newContent); // trigger synchronization to make sure that import will be fetched from the cache diags = project.languageService.getSemanticDiagnostics(imported.name); // ensure file has correct number of errors after edit @@ -135,7 +135,7 @@ namespace ts { return originalFileExists.call(serverHost, fileName); }; const newContent = `import {x} from "f2"`; - rootScriptInfo.editContent(0, rootScriptInfo.content.length, newContent); + rootScriptInfo.editContent(0, root.content.length, newContent); try { // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk @@ -160,7 +160,7 @@ namespace ts { }; const newContent = `import {x} from "f1"`; - rootScriptInfo.editContent(0, rootScriptInfo.content.length, newContent); + rootScriptInfo.editContent(0, root.content.length, newContent); project.languageService.getSemanticDiagnostics(imported.name); assert.isTrue(fileExistsCalled); @@ -213,7 +213,7 @@ namespace ts { // assert that import will success once file appear on disk fileMap[imported.name] = imported; fileExistsCalledForBar = false; - rootScriptInfo.editContent(0, rootScriptInfo.content.length, `import {y} from "bar"`); + rootScriptInfo.editContent(0, root.content.length, `import {y} from "bar"`); diags = project.languageService.getSemanticDiagnostics(root.name); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); From cf616dc2924f345cfe68a5280c8e9d75baed0fbe Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 22 Jun 2016 17:02:08 -0700 Subject: [PATCH 061/307] [in progress] project system work - path normalization --- src/server/editorServices.ts | 12 ++++---- src/server/project.ts | 37 ++++++++++++----------- src/server/session.ts | 57 ++++++++++++++++++------------------ 3 files changed, 51 insertions(+), 55 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index e111053315a..f95899925e5 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -935,16 +935,14 @@ namespace ts.server { for (const p of rootFile.containingProjects) { if (p.projectKind !== ProjectKind.Inferred) { // file was included in non-inferred project - drop old inferred project - infe - break; } } - if (inInferredProjectOnly) { - openFileRoots.push(rootFile); - } - else { + // if (inInferredProjectOnly) { + // openFileRoots.push(rootFile); + // } + // else { - } + // } // const rootedProject = rootFile.defaultProject; // const referencingProjects = this.findReferencingProjects(rootFile, rootedProject); diff --git a/src/server/project.ts b/src/server/project.ts index 0a36c3ee62f..01c7939ca82 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -94,7 +94,6 @@ namespace ts.server { } // signal language service to release files acquired from document registry this.languageService.dispose(); - } getCompilerOptions() { @@ -167,21 +166,6 @@ namespace ts.server { this.projectStateVersion++; } - // remove a root file from project - private removeRoot(info: ScriptInfo): boolean { - if (this.isRoot(info)) { - this.rootFiles = copyListRemovingItem(info, this.rootFiles); - this.rootFilesMap.remove(info.path); - this.lsHost.removeRoot(info); - return true; - } - return false; - } - - private removeReferencedFile(info: ScriptInfo) { - this.lsHost.removeReferencedFile(info) - } - updateGraph() { if (!this.languageServiceEnabled) { return; @@ -198,7 +182,7 @@ namespace ts.server { } } - getScriptInfoFromNormalizedPath(fileName: NormalizedPath) { + getScriptInfoForNormalizedPath(fileName: NormalizedPath) { const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false); if (scriptInfo && scriptInfo.attachToProject(this)) { this.markAsDirty(); @@ -207,7 +191,7 @@ namespace ts.server { } getScriptInfo(uncheckedFileName: string) { - return this.getScriptInfoFromNormalizedPath(toNormalizedPath(uncheckedFileName)); + return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); } filesToString() { @@ -240,7 +224,7 @@ namespace ts.server { } reloadScript(filename: NormalizedPath, cb: () => void) { - const script = this.getScriptInfoFromNormalizedPath(filename); + const script = this.getScriptInfoForNormalizedPath(filename); if (script) { script.reloadFromFile(filename, cb); } @@ -288,6 +272,21 @@ namespace ts.server { return { info, files: projectFileNames }; } } + + // remove a root file from project + private removeRoot(info: ScriptInfo): boolean { + if (this.isRoot(info)) { + this.rootFiles = copyListRemovingItem(info, this.rootFiles); + this.rootFilesMap.remove(info.path); + this.lsHost.removeRoot(info); + return true; + } + return false; + } + + private removeReferencedFile(info: ScriptInfo) { + this.lsHost.removeReferencedFile(info) + } } export class InferredProject extends Project { diff --git a/src/server/session.ts b/src/server/session.ts index 8a480580ddd..29c8dcd4abd 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -68,8 +68,8 @@ namespace ts.server { } } - function formatDiag(fileName: string, project: Project, diag: ts.Diagnostic): protocol.Diagnostic { - const scriptInfo = project.getScriptInfo(fileName); + function formatDiag(fileName: NormalizedPath, project: Project, diag: ts.Diagnostic): protocol.Diagnostic { + const scriptInfo = project.getScriptInfoForNormalizedPath(fileName); return { start: scriptInfo.positionToLineOffset(diag.start), end: scriptInfo.positionToLineOffset(diag.start + diag.length), @@ -271,7 +271,7 @@ namespace ts.server { return { line, offset: offset + 1 }; } - private semanticCheck(file: string, project: Project) { + private semanticCheck(file: NormalizedPath, project: Project) { try { const diags = project.languageService.getSemanticDiagnostics(file); @@ -285,7 +285,7 @@ namespace ts.server { } } - private syntacticCheck(file: string, project: Project) { + private syntacticCheck(file: NormalizedPath, project: Project) { try { const diags = project.languageService.getSyntacticDiagnostics(file); if (diags) { @@ -397,7 +397,7 @@ namespace ts.server { private getDiagnosticsWorker(args: protocol.FileRequestArgs, selector: (project: Project, file: string) => Diagnostic[]) { const { project, file } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoFromNormalizedPath(file); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); const diagnostics = selector(project, file); return this.convertDiagnostics(diagnostics, scriptInfo); } @@ -412,8 +412,7 @@ namespace ts.server { private getDefinition(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.FileSpan[] | DefinitionInfo[] { const { file, project } = this.getFileAndProject(args); - - const scriptInfo = project.getScriptInfo(file); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); const definitions = project.languageService.getDefinitionAtPosition(file, position); @@ -438,7 +437,7 @@ namespace ts.server { private getTypeDefinition(args: protocol.FileLocationRequestArgs): protocol.FileSpan[] { const { file, project } = this.getFileAndProject(args) - const scriptInfo = project.getScriptInfoFromNormalizedPath(file); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); const definitions = project.languageService.getTypeDefinitionAtPosition(file, position); @@ -458,7 +457,7 @@ namespace ts.server { private getOccurrences(args: protocol.FileLocationRequestArgs): protocol.OccurrencesResponseItem[] { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoFromNormalizedPath(file); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); const occurrences = project.languageService.getOccurrencesAtPosition(file, position); @@ -483,7 +482,7 @@ namespace ts.server { private getDocumentHighlights(args: protocol.DocumentHighlightsRequestArgs, simplifiedResult: boolean): protocol.DocumentHighlightsItem[] | DocumentHighlights[] { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoFromNormalizedPath(file); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); const documentHighlights = project.languageService.getDocumentHighlights(file, position, args.filesToSearch); @@ -533,7 +532,7 @@ namespace ts.server { private getRenameInfo(args: protocol.FileLocationRequestArgs) { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoFromNormalizedPath(file); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); return project.languageService.getRenameInfo(file, position); } @@ -559,8 +558,8 @@ namespace ts.server { } private getRenameLocations(args: protocol.RenameRequestArgs, simplifiedResult: boolean): protocol.RenameResponseBody | RenameLocation[] { - const file = ts.normalizePath(args.file); - const info = this.projectService.getScriptInfo(file); + const file = toNormalizedPath(args.file); + const info = this.projectService.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, info); const projects = this.getProjects(args); if (simplifiedResult) { @@ -661,11 +660,11 @@ namespace ts.server { } private getReferences(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.ReferencesResponseBody | ReferencedSymbol[] { - const file = ts.normalizePath(args.file); + const file = toNormalizedPath(args.file); const projects = this.getProjects(args); const defaultProject = projects[0]; - const scriptInfo = defaultProject.getScriptInfo(file); + const scriptInfo = defaultProject.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); if (simplifiedResult) { const nameInfo = defaultProject.languageService.getQuickInfoAtPosition(file, position); @@ -771,39 +770,39 @@ namespace ts.server { private getDocCommentTemplate(args: protocol.FileLocationRequestArgs) { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfo(file); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); return project.languageService.getDocCommentTemplateAtPosition(file, position); } private getIndentation(args: protocol.IndentationRequestArgs) { const { file, project } = this.getFileAndProject(args); - const position = this.getPosition(args, project.getScriptInfo(file)); + const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file)); const indentation = project.languageService.getIndentationAtPosition(file, position, args.options); return { position, indentation }; } private getBreakpointStatement(args: protocol.FileLocationRequestArgs) { const { file, project } = this.getFileAndProject(args); - const position = this.getPosition(args, project.getScriptInfo(file)); + const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file)); return project.languageService.getBreakpointStatementAtPosition(file, position); } private getNameOrDottedNameSpan(args: protocol.FileLocationRequestArgs) { const { file, project } = this.getFileAndProject(args); - const position = this.getPosition(args, project.getScriptInfo(file)); + const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file)); return project.languageService.getNameOrDottedNameSpan(file, position, position); } private isValidBraceCompletion(args: protocol.BraceCompletionRequestArgs) { const { file, project } = this.getFileAndProject(args); - const position = this.getPosition(args, project.getScriptInfo(file)); + const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file)); return project.languageService.isValidBraceCompletionAtPostion(file, position, args.openingBrace.charCodeAt(0)); } private getQuickInfoWorker(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.QuickInfoResponseBody | QuickInfo { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfo(file); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); const quickInfo = project.languageService.getQuickInfoAtPosition(file, this.getPosition(args, scriptInfo)); if (!quickInfo) { return undefined; @@ -828,7 +827,7 @@ namespace ts.server { private getFormattingEditsForRange(args: protocol.FormatRequestArgs): protocol.CodeEdit[] { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfo(file); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); const startPosition = scriptInfo.lineOffsetToPosition(args.line, args.offset); const endPosition = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); @@ -866,7 +865,7 @@ namespace ts.server { private getFormattingEditsAfterKeystroke(args: protocol.FormatOnKeyRequestArgs): protocol.CodeEdit[] { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfo(file); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); const position = scriptInfo.lineOffsetToPosition(args.line, args.offset); const formatOptions = this.projectService.getFormatCodeOptions(file); const edits = project.languageService.getFormattingEditsAfterKeystroke(file, position, args.key, @@ -933,7 +932,7 @@ namespace ts.server { const prefix = args.prefix || ""; const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfo(file); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); const completions = project.languageService.getCompletionsAtPosition(file, position); @@ -955,7 +954,7 @@ namespace ts.server { private getCompletionEntryDetails(args: protocol.CompletionDetailsRequestArgs): protocol.CompletionEntryDetails[] { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfo(file); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); return args.entryNames.reduce((accum: protocol.CompletionEntryDetails[], entryName: string) => { @@ -969,7 +968,7 @@ namespace ts.server { private getSignatureHelpItems(args: protocol.SignatureHelpRequestArgs, simplifiedResult: boolean): protocol.SignatureHelpItems | SignatureHelpItems { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfo(file); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); const helpItems = project.languageService.getSignatureHelpItems(file, position); if (!helpItems) { @@ -1012,7 +1011,7 @@ namespace ts.server { private change(args: protocol.ChangeRequestArgs) { const { file, project } = this.getFileAndProject(args, /*errorOnMissingProject*/ false); if (project) { - const scriptInfo = project.getScriptInfo(file); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); const start = scriptInfo.lineOffsetToPosition(args.line, args.offset); const end = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); if (start >= 0) { @@ -1058,7 +1057,7 @@ namespace ts.server { return undefined; } - const scriptInfo = project.getScriptInfoFromNormalizedPath(fileName); + const scriptInfo = project.getScriptInfoForNormalizedPath(fileName); return items.map(item => ({ text: item.text, @@ -1167,7 +1166,7 @@ namespace ts.server { private getBraceMatching(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.TextSpan[] | TextSpan[] { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfo(file); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); const spans = project.languageService.getBraceMatchingAtPosition(file, position); From cefaa171eb462efc4674dcbc4595b046fe7deb30 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 24 Jun 2016 14:30:45 -0700 Subject: [PATCH 062/307] [in progress] project system work - major code reorg --- src/server/editorServices.ts | 557 +++++++++++++++++-------------- src/server/lshost.ts | 47 ++- src/server/project.ts | 71 ++-- src/server/scriptInfo.ts | 20 +- src/server/scriptVersionCache.ts | 4 +- src/server/session.ts | 9 +- src/server/utilities.ts | 8 +- 7 files changed, 386 insertions(+), 330 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f95899925e5..bc645cb9f8e 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -8,7 +8,6 @@ /// namespace ts.server { - export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; /** @@ -24,10 +23,29 @@ namespace ts.server { } export interface HostConfiguration { - formatCodeOptions: ts.FormatCodeSettings; + formatCodeOptions: FormatCodeSettings; hostInfo: string; } + interface ConfigFileConversionResult { + success: boolean; + errors?: Diagnostic[]; + + projectOptions?: ProjectOptions; + } + + interface OpenConfigFileResult { + success: boolean, + errors?: Diagnostic[] + + project?: ConfiguredProject, + } + + interface OpenConfiguredProjectResult { + configFileName?: string; + configFileErrors?: Diagnostic[]; + } + function findProjectByName(projectName: string, projects: T[]): T { for (const proj of projects) { if (proj.getProjectName() === projectName) { @@ -36,22 +54,16 @@ namespace ts.server { } } - export interface ProjectOpenResult { - success?: boolean; - errorMsg?: string; - project?: Project; - } - class DirectoryWatchers { /** * a path to directory watcher map that detects added tsconfig files **/ - private directoryWatchersForTsconfig: ts.Map = {}; + private directoryWatchersForTsconfig: Map = {}; /** * count of how many projects are using the directory watcher. * If the number becomes 0 for a watcher, then we should close it. **/ - private directoryWatchersRefCount: ts.Map = {}; + private directoryWatchersRefCount: Map = {}; constructor(private readonly projectService: ProjectService) { } @@ -60,18 +72,18 @@ namespace ts.server { // if the ref count for this directory watcher drops to 0, it's time to close it this.directoryWatchersRefCount[directory]--; if (this.directoryWatchersRefCount[directory] === 0) { - this.projectService.log("Close directory watcher for: " + directory); + this.projectService.log(`Close directory watcher for: ${directory}`); this.directoryWatchersForTsconfig[directory].close(); delete this.directoryWatchersForTsconfig[directory]; } } startWatchingContainingDirectoriesForFile(fileName: string, project: InferredProject, callback: (fileName: string) => void) { - let currentPath = ts.getDirectoryPath(fileName); - let parentPath = ts.getDirectoryPath(currentPath); + let currentPath = getDirectoryPath(fileName); + let parentPath = getDirectoryPath(currentPath); while (currentPath != parentPath) { if (!this.directoryWatchersForTsconfig[currentPath]) { - this.projectService.log("Add watcher for: " + currentPath); + this.projectService.log(`Add watcher for: ${currentPath}`); this.directoryWatchersForTsconfig[currentPath] = this.projectService.host.watchDirectory(currentPath, callback); this.directoryWatchersRefCount[currentPath] = 1; } @@ -80,30 +92,32 @@ namespace ts.server { } project.directoriesWatchedForTsconfig.push(currentPath); currentPath = parentPath; - parentPath = ts.getDirectoryPath(parentPath); + parentPath = getDirectoryPath(parentPath); } } } export class ProjectService { + private readonly documentRegistry: DocumentRegistry; + /** * Container of all known scripts */ - private filenameToScriptInfo = createNormalizedPathMap(); + private readonly filenameToScriptInfo = createNormalizedPathMap(); /** * maps external project file name to list of config files that were the part of this project */ - externalProjectToConfiguredProjectMap: Map; + private readonly externalProjectToConfiguredProjectMap: Map; /** * external projects (configuration and list of root files is not controlled by tsserver) */ - externalProjects: ExternalProject[] = []; + readonly externalProjects: ExternalProject[] = []; /** * projects built from openFileRoots **/ - inferredProjects: InferredProject[] = []; + readonly inferredProjects: InferredProject[] = []; /** * projects specified by a tsconfig.json file **/ @@ -121,22 +135,21 @@ namespace ts.server { **/ openFileRootsConfigured: ScriptInfo[] = []; - private directoryWatchers: DirectoryWatchers; + private readonly directoryWatchers: DirectoryWatchers; private hostConfiguration: HostConfiguration; + private timerForDetectingProjectFileListChanges: Map = {}; - private documentRegistry: ts.DocumentRegistry; - constructor(public readonly host: ServerHost, - public readonly psLogger: Logger, + public readonly logger: Logger, public readonly cancellationToken: HostCancellationToken, private readonly eventHandler?: ProjectServiceEventHandler) { this.directoryWatchers = new DirectoryWatchers(this); // ts.disableIncrementalParsing = true; this.setDefaultHostConfiguration(); - this.documentRegistry = ts.createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); + this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); } private setDefaultHostConfiguration() { @@ -162,7 +175,7 @@ namespace ts.server { getFormatCodeOptions(file?: NormalizedPath) { if (file) { - const info = this.filenameToScriptInfo.get(file); + const info = this.getScriptInfoForNormalizedPath(file); if (info) { return info.formatCodeSettings; } @@ -171,9 +184,9 @@ namespace ts.server { } private onSourceFileChanged(fileName: NormalizedPath) { - const info = this.filenameToScriptInfo.get(fileName); + const info = this.getScriptInfoForNormalizedPath(fileName); if (!info) { - this.psLogger.info("Error: got watch notification for unknown file: " + fileName); + this.logger.info(`Error: got watch notification for unknown file: ${fileName}`); } if (!this.host.fileExists(fileName)) { @@ -182,13 +195,13 @@ namespace ts.server { } else { if (info && (!info.isOpen)) { - info.reloadFromFile(info.fileName); + info.reloadFromFile(); } } } private handleDeletedFile(info: ScriptInfo) { - this.psLogger.info(info.fileName + " deleted"); + this.logger.info(`${info.fileName} deleted`); info.stopWatcher(); @@ -212,6 +225,7 @@ namespace ts.server { this.printProjects(); } + /** * This is the callback function when a watched directory has added or removed source code files. * @param project the project that associates with this directory watcher @@ -225,7 +239,7 @@ namespace ts.server { return; } - this.log("Detected source file changes: " + fileName); + this.log(`Detected source file changes: ${fileName}`); const timeoutId = this.timerForDetectingProjectFileListChanges[project.configFileName]; if (timeoutId) { this.host.clearTimeout(timeoutId); @@ -237,13 +251,13 @@ namespace ts.server { } private handleChangeInSourceFileForConfiguredProject(project: ConfiguredProject) { - const { projectOptions } = this.configFileToProjectOptions(project.configFileName); + const { projectOptions } = this.convertConfigFileContentToProjectOptions(project.configFileName); const newRootFiles = projectOptions.files.map((f => this.getCanonicalFileName(f))); const currentRootFiles = project.getRootFiles().map((f => this.getCanonicalFileName(f))); // We check if the project file list has changed. If so, we update the project. - if (!arrayIsEqualTo(currentRootFiles && currentRootFiles.sort(), newRootFiles && newRootFiles.sort())) { + if (!arrayIsEqualTo(currentRootFiles.sort(), newRootFiles.sort())) { // For configured projects, the change is made outside the tsconfig file, and // it is not likely to affect the project for other files opened by the client. We can // just update the current project. @@ -256,7 +270,7 @@ namespace ts.server { } private onConfigChangedForConfiguredProject(project: ConfiguredProject) { - this.log("Config file changed: " + project.configFileName); + this.log(`Config file changed: ${project.configFileName}`); this.updateConfiguredProject(project); this.updateProjectStructure(); } @@ -264,39 +278,38 @@ namespace ts.server { /** * This is the callback function when a watched directory has an added tsconfig file. */ - private onConfigChangeForInferredProject(fileName: string) { - if (ts.getBaseFileName(fileName) != "tsconfig.json") { - this.log(fileName + " is not tsconfig.json"); + private onConfigFileAddedForInferredProject(fileName: string) { + // TODO: check directory separators + if (getBaseFileName(fileName) != "tsconfig.json") { + this.log(`${fileName} is not tsconfig.json`); return; } - this.log("Detected newly added tsconfig file: " + fileName); - - const { projectOptions } = this.configFileToProjectOptions(fileName); - + this.log(`Detected newly added tsconfig file: ${fileName}`); + const { projectOptions } = this.convertConfigFileContentToProjectOptions(fileName); const rootFilesInTsconfig = projectOptions.files.map(f => this.getCanonicalFileName(f)); // We should only care about the new tsconfig file if it contains any // opened root files of existing inferred projects for (const rootFile of this.openFileRoots) { if (contains(rootFilesInTsconfig, this.getCanonicalFileName(rootFile.fileName))) { this.reloadProjects(); - return; + break; } } } private getCanonicalFileName(fileName: string) { const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); - return ts.normalizePath(name); + return normalizePath(name); } + // TODO: delete if unused private releaseNonReferencedConfiguredProjects() { if (this.configuredProjects.every(p => p.openRefCount > 0)) { return; } const configuredProjects: ConfiguredProject[] = []; - for (const proj of this.configuredProjects) { if (proj.openRefCount > 0) { configuredProjects.push(proj); @@ -310,7 +323,7 @@ namespace ts.server { } private removeProject(project: Project) { - this.log("remove project: " + project.getRootFiles().toString()); + this.log(`remove project: ${project.getRootFiles().toString()}`); project.close(); @@ -345,14 +358,15 @@ namespace ts.server { return undefined; } - private addOpenFile(info: ScriptInfo) { + private addOpenFile(info: ScriptInfo): void { const externalProject = this.findContainingExternalProject(info.fileName); if (externalProject) { + // file is already included in some external project - do nothing return; } const configuredProject = this.findContainingConfiguredProject(info); if (configuredProject) { - // info.defaultProject = configuredProject; + // file is the part of configured project configuredProject.addOpenRef(); if (configuredProject.isRoot(info)) { this.openFileRootsConfigured.push(info); @@ -362,6 +376,7 @@ namespace ts.server { } return; } + // create new inferred project p with the newly opened file as root const inferredProject = this.createAndAddInferredProject(info); const openFileRoots: ScriptInfo[] = []; @@ -369,8 +384,11 @@ namespace ts.server { for (const rootFile of this.openFileRoots) { // if r referenced by the new project if (inferredProject.containsScriptInfo(rootFile)) { - // remove project rooted at r - this.removeProject(rootFile.getDefaultProject()); + // remove inferred project that was initially created for rootFile + const defaultProject = rootFile.getDefaultProject(); + Debug.assert(defaultProject.projectKind === ProjectKind.Inferred); + + this.removeProject(defaultProject); // put r in referenced open file list this.openFilesReferenced.push(rootFile); // set default project of r to the new project @@ -389,14 +407,14 @@ namespace ts.server { * Remove this file from the set of open, non-configured files. * @param info The file that has been closed or newly configured */ - private closeOpenFile(info: ScriptInfo) { + private closeOpenFile(info: ScriptInfo): void { // Closing file should trigger re-reading the file content from disk. This is // because the user may chose to discard the buffer content before saving // to the disk, and the server's version of the file can be out of sync. - info.reloadFromFile(info.fileName); + info.reloadFromFile(); - this.openFileRoots = copyListRemovingItem(info, this.openFileRoots); - this.openFileRootsConfigured = copyListRemovingItem(info, this.openFileRootsConfigured); + removeItemFromSet(this.openFileRoots, info); + removeItemFromSet(this.openFileRootsConfigured, info); // collect all projects that should be removed let projectsToRemove: Project[]; @@ -421,6 +439,10 @@ namespace ts.server { const orphanFiles: ScriptInfo[] = []; // for all open, referenced files f for (const f of this.openFilesReferenced) { + if (f === info) { + // skip closed file + continue; + } // collect orphanted files and try to re-add them as newly opened if (f.containingProjects.length === 0) { orphanFiles.push(f); @@ -430,17 +452,20 @@ namespace ts.server { openFilesReferenced.push(f); } } + this.openFilesReferenced = openFilesReferenced; // treat orphaned files as newly opened - for (let i = 0, len = orphanFiles.length; i < len; i++) { - this.addOpenFile(orphanFiles[i]); + for (const f of orphanFiles) { + this.addOpenFile(f); } } else { - this.openFilesReferenced = copyListRemovingItem(info, this.openFilesReferenced); + // just close file + removeItemFromSet(this.openFilesReferenced, info); } - this.releaseNonReferencedConfiguredProjects(); + // projectsToRemove should already cover it + // this.releaseNonReferencedConfiguredProjects(); info.isOpen = false; } @@ -450,36 +475,38 @@ namespace ts.server { * 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; otherwise we create a new one. */ - private openOrUpdateConfiguredProjectForFile(fileName: string): { configFileName?: string, configFileErrors?: Diagnostic[] } { - const searchPath = asNormalizedPath(getDirectoryPath(toNormalizedPath(fileName))); - this.log("Search path: " + searchPath, "Info"); + private openOrUpdateConfiguredProjectForFile(fileName: NormalizedPath): OpenConfiguredProjectResult { + const searchPath = getDirectoryPath(fileName); + this.log(`Search path: ${searchPath}`, "Info"); + // check if this file is already included in one of external projects - const configFileName = this.findConfigFile(searchPath); - if (configFileName) { - this.log("Config file name: " + configFileName, "Info"); - const project = this.findConfiguredProjectByProjectName(configFileName); - if (!project) { - const { success, errors } = this.openConfigFile(configFileName, fileName); - if (!success) { - return { configFileName, configFileErrors: errors }; - } - else { - // even if opening config file was successful, it could still - // contain errors that were tolerated. - this.log("Opened configuration file " + configFileName, "Info"); - if (errors && errors.length > 0) { - return { configFileName, configFileErrors: errors }; - } - } + const configFileName = this.findConfigFile(asNormalizedPath(searchPath)); + if (!configFileName) { + this.log("No config files found."); + return {}; + } + + this.log(`Config file name: ${configFileName}`, "Info"); + + const project = this.findConfiguredProjectByProjectName(configFileName); + if (!project) { + const { success, errors } = this.openConfigFile(configFileName, fileName); + if (!success) { + return { configFileName, configFileErrors: errors }; } - else { - this.updateConfiguredProject(project); + + // even if opening config file was successful, it could still + // contain errors that were tolerated. + this.log(`Opened configuration file ${configFileName}`, "Info"); + if (errors && errors.length > 0) { + return { configFileName, configFileErrors: errors }; } } else { - this.log("No config files found."); + this.updateConfiguredProject(project); } - return configFileName ? { configFileName } : {}; + + return { configFileName }; } // This is different from the method the compiler uses because @@ -509,38 +536,42 @@ namespace ts.server { } private printProjects() { - if (!this.psLogger.isVerbose()) { + if (!this.logger.isVerbose()) { return; } - this.psLogger.startGroup(); - for (let i = 0, len = this.inferredProjects.length; i < len; i++) { - const project = this.inferredProjects[i]; - project.updateGraph(); - this.psLogger.info("Project " + i.toString()); - this.psLogger.info(project.filesToString()); - this.psLogger.info("-----------------------------------------------"); + this.logger.startGroup(); + + let counter = 0; + counter = printProjects(this.externalProjects, counter); + counter = printProjects(this.configuredProjects, counter); + counter = printProjects(this.inferredProjects, counter); + + this.logger.info("Open file roots of inferred projects: "); + for (const rootFile of this.openFileRoots) { + this.logger.info(rootFile.fileName); } - for (let i = 0, len = this.configuredProjects.length; i < len; i++) { - const project = this.configuredProjects[i]; - project.updateGraph(); - this.psLogger.info("Project (configured) " + (i + this.inferredProjects.length).toString()); - this.psLogger.info(project.filesToString()); - this.psLogger.info("-----------------------------------------------"); - } - this.psLogger.info("Open file roots of inferred projects: "); - for (let i = 0, len = this.openFileRoots.length; i < len; i++) { - this.psLogger.info(this.openFileRoots[i].fileName); - } - this.psLogger.info("Open files referenced by inferred or configured projects: "); + this.logger.info("Open files referenced by inferred or configured projects: "); for (const referencedFile of this.openFilesReferenced) { const fileInfo = `${referencedFile.fileName} ${ProjectKind[referencedFile.getDefaultProject().projectKind]}`; - this.psLogger.info(fileInfo); + this.logger.info(fileInfo); } - this.psLogger.info("Open file roots of configured projects: "); - for (let i = 0, len = this.openFileRootsConfigured.length; i < len; i++) { - this.psLogger.info(this.openFileRootsConfigured[i].fileName); + this.logger.info("Open file roots of configured projects: "); + for (const configuredRoot of this.openFileRootsConfigured) { + this.logger.info(configuredRoot.fileName); + } + + this.logger.endGroup(); + + function printProjects(projects: Project[], counter: number) { + for (const project of projects) { + project.updateGraph(); + this.psLogger.info(`Project '${project.getProjectName()}' (${ProjectKind[project.projectKind]}) ${counter}`); + this.psLogger.info(project.filesToString()); + this.psLogger.info("-----------------------------------------------"); + counter++; + } + return counter; } - this.psLogger.endGroup(); } private findConfiguredProjectByProjectName(configFileName: NormalizedPath) { @@ -551,48 +582,46 @@ namespace ts.server { return findProjectByName(projectFileName, this.externalProjects); } - private configFileToProjectOptions(configFilename: string): { succeeded: boolean, projectOptions?: ProjectOptions, errors?: Diagnostic[] } { - configFilename = ts.normalizePath(configFilename); - // file references will be relative to dirPath (or absolute) - const dirPath = ts.getDirectoryPath(configFilename); - const contents = this.host.readFile(configFilename); - const rawConfig: { config?: ProjectOptions; error?: Diagnostic; } = ts.parseConfigFileTextToJson(configFilename, contents); - if (rawConfig.error) { - return { succeeded: false, errors: [rawConfig.error] }; - } - else { - const configHasFilesProperty = rawConfig.config["files"] !== undefined; - const parsedCommandLine = ts.parseJsonConfigFileContent(rawConfig.config, this.host, dirPath, /*existingOptions*/ {}, configFilename); - Debug.assert(!!parsedCommandLine.fileNames); + private convertConfigFileContentToProjectOptions(configFilename: string): ConfigFileConversionResult { + configFilename = normalizePath(configFilename); - if (parsedCommandLine.errors && (parsedCommandLine.errors.length > 0)) { - return { succeeded: false, errors: parsedCommandLine.errors }; - } - else if (parsedCommandLine.fileNames.length === 0) { - const error = createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename); - return { succeeded: false, errors: [error] }; - } - else { - const projectOptions: ProjectOptions = { - files: parsedCommandLine.fileNames, - compilerOptions: parsedCommandLine.options, - configHasFilesProperty, - wildcardDirectories: parsedCommandLine.wildcardDirectories, - }; - return { succeeded: true, projectOptions }; - } + const configObj = parseConfigFileTextToJson(configFilename, this.host.readFile(configFilename)); + if (configObj.error) { + return { success: false, errors: [configObj.error] }; } + + const parsedCommandLine = parseJsonConfigFileContent( + configObj.config, + this.host, + getDirectoryPath(configFilename), + /*existingOptions*/ {}, + configFilename); + + Debug.assert(!!parsedCommandLine.fileNames); + + if (parsedCommandLine.errors && (parsedCommandLine.errors.length > 0)) { + return { success: false, errors: parsedCommandLine.errors }; + } + + if (parsedCommandLine.fileNames.length === 0) { + const error = createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename); + return { success: false, errors: [error] }; + } + + const projectOptions: ProjectOptions = { + files: parsedCommandLine.fileNames, + compilerOptions: parsedCommandLine.options, + configHasFilesProperty: configObj.config["files"] !== undefined, + wildcardDirectories: parsedCommandLine.wildcardDirectories, + }; + return { success: true, projectOptions }; } - private exceedTotalNonTsFileSizeLimit(options: CompilerOptions, fileNames: string[]) { - if (options && options.disableSizeLimit) { + private exceededTotalSizeLimitForNonTsFiles(options: CompilerOptions, fileNames: string[]) { + if (options && options.disableSizeLimit || !this.host.getFileSize) { return false; } let totalNonTsFileSize = 0; - if (!this.host.getFileSize) { - return false; - } - for (const fileName of fileNames) { if (hasTypeScriptFileExtension(fileName)) { continue; @@ -605,16 +634,21 @@ namespace ts.server { return false; } - private createAndAddExternalProject(projectFileName: string, files: string[], compilerOptions: CompilerOptions, clientFileName?: string) { - const sizeLimitExceeded = this.exceedTotalNonTsFileSizeLimit(compilerOptions, files); - const project = new ExternalProject(projectFileName, this, this.documentRegistry, compilerOptions, !sizeLimitExceeded); - const errors = this.addFilesToProject(project, files, clientFileName); + private createAndAddExternalProject(projectFileName: string, files: string[], compilerOptions: CompilerOptions) { + const project = new ExternalProject( + projectFileName, + this, + this.documentRegistry, + compilerOptions, + /*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(compilerOptions, files)); + + const errors = this.addFilesToProjectAndUpdateGraph(project, files, /*clientFileName*/ undefined); this.externalProjects.push(project); return { project, errors }; } private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, clientFileName?: string) { - const sizeLimitExceeded = this.exceedTotalNonTsFileSizeLimit(projectOptions.compilerOptions, projectOptions.files); + const sizeLimitExceeded = !this.exceededTotalSizeLimitForNonTsFiles(projectOptions.compilerOptions, projectOptions.files); const project = new ConfiguredProject( configFileName, this, @@ -622,25 +656,27 @@ namespace ts.server { projectOptions.configHasFilesProperty, projectOptions.compilerOptions, projectOptions.wildcardDirectories, - !sizeLimitExceeded); + /*languageServiceEnabled*/ !sizeLimitExceeded); + + const errors = this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, clientFileName); - const errors = this.addFilesToProject(project, projectOptions.files, clientFileName); project.watchConfigFile(project => this.onConfigChangedForConfiguredProject(project)); if (!sizeLimitExceeded) { this.watchConfigDirectoryForProject(project, projectOptions); } project.watchWildcards((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path)); + this.configuredProjects.push(project); return { project, errors }; } - private watchConfigDirectoryForProject(project: ConfiguredProject, options: ProjectOptions) { + private watchConfigDirectoryForProject(project: ConfiguredProject, options: ProjectOptions): void { if (!options.configHasFilesProperty) { project.watchConfigDirectory((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path)); } } - private addFilesToProject(project: ConfiguredProject | ExternalProject, files: string[], clientFileName: string): Diagnostic[] { + private addFilesToProjectAndUpdateGraph(project: ConfiguredProject | ExternalProject, files: string[], clientFileName: string): Diagnostic[] { let errors: Diagnostic[]; for (const rootFilename of files) { if (this.host.fileExists(rootFilename)) { @@ -655,22 +691,23 @@ namespace ts.server { return errors; } - private openConfigFile(configFileName: NormalizedPath, clientFileName?: string): { success: boolean, project?: ConfiguredProject, errors?: Diagnostic[] } { - const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(configFileName); - if (!succeeded) { - return { success: false, errors }; - } - else { - const { project, errors } = this.createAndAddConfiguredProject(configFileName, projectOptions, clientFileName); - return { success: true, project, errors }; + private openConfigFile(configFileName: NormalizedPath, clientFileName?: string): OpenConfigFileResult { + const conversionResult = this.convertConfigFileContentToProjectOptions(configFileName); + if (!conversionResult.success) { + return { success: false, errors: conversionResult.errors }; } + const { project, errors } = this.createAndAddConfiguredProject(configFileName, conversionResult.projectOptions, clientFileName); + return { success: true, project, errors }; } private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newRootFiles: string[], newOptions: CompilerOptions) { const oldRootFiles = project.getRootFiles(); + + // TODO: verify that newRootFiles are always normalized + // TODO: avoid N^2 const newFileNames = asNormalizedPathArray(filter(newRootFiles, f => this.host.fileExists(f))); - const fileNamesToRemove = oldRootFiles.filter(f => !contains(newFileNames, f)); - const fileNamesToAdd = newFileNames.filter(f => !contains(oldRootFiles, f)); + const fileNamesToRemove = asNormalizedPathArray(oldRootFiles.filter(f => !contains(newFileNames, f))); + const fileNamesToAdd = asNormalizedPathArray(newFileNames.filter(f => !contains(oldRootFiles, f))); for (const fileName of fileNamesToRemove) { const info = this.getScriptInfoForNormalizedPath(fileName); @@ -680,7 +717,7 @@ namespace ts.server { } for (const fileName of fileNamesToAdd) { - let info = this.getScriptInfo(fileName); + let info = this.getScriptInfoForNormalizedPath(fileName); if (!info) { info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false); } @@ -689,7 +726,8 @@ namespace ts.server { // openFileRoots or openFileReferenced. if (info.isOpen) { if (contains(this.openFileRoots, info)) { - this.openFileRoots = copyListRemovingItem(info, this.openFileRoots); + removeItemFromSet(this.openFileRoots, info); + // delete inferred project let toRemove: Project[]; for (const p of info.containingProjects) { @@ -722,30 +760,29 @@ namespace ts.server { if (!this.host.fileExists(project.configFileName)) { this.log("Config file deleted"); this.removeProject(project); + return; + } + + const { success, projectOptions, errors } = this.convertConfigFileContentToProjectOptions(project.configFileName); + if (!success) { + return errors; + } + + if (this.exceededTotalSizeLimitForNonTsFiles(projectOptions.compilerOptions, projectOptions.files)) { + project.setCompilerOptions(projectOptions.compilerOptions); + if (!project.languageServiceEnabled) { + // language service is already disabled + return; + } + project.disableLanguageService(); + project.stopWatchingDirectory(); } else { - const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(project.configFileName); - if (!succeeded) { - return errors; - } - else { - if (this.exceedTotalNonTsFileSizeLimit(projectOptions.compilerOptions, projectOptions.files)) { - project.setCompilerOptions(projectOptions.compilerOptions); - if (!project.languageServiceEnabled) { - // language service is already disabled - return; - } - project.disableLanguageService(); - project.stopWatchingDirectory(); - } - else { - if (!project.languageServiceEnabled) { - project.enableLanguageService(); - } - this.watchConfigDirectoryForProject(project, projectOptions); - this.updateNonInferredProject(project, projectOptions.files, projectOptions.compilerOptions); - } + if (!project.languageServiceEnabled) { + project.enableLanguageService(); } + this.watchConfigDirectoryForProject(project, projectOptions); + this.updateNonInferredProject(project, projectOptions.files, projectOptions.compilerOptions); } } @@ -756,7 +793,7 @@ namespace ts.server { this.directoryWatchers.startWatchingContainingDirectoriesForFile( root.fileName, project, - fileName => this.onConfigChangeForInferredProject(fileName)); + fileName => this.onConfigFileAddedForInferredProject(fileName)); project.updateGraph(); this.inferredProjects.push(project); @@ -764,15 +801,20 @@ namespace ts.server { } /** - * @param filename is absolute pathname + * @param uncheckedFileName is absolute pathname * @param fileContent is a known version of the file content that is more up to date than the one on disk */ - getOrCreateScriptInfo(fileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { - return this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(fileName), openedByClient, fileContent, scriptKind); + getOrCreateScriptInfo(uncheckedFileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { + return this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName), openedByClient, fileContent, scriptKind); } + + getScriptInfo(uncheckedFileName: string) { + return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); + } + getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { - let info = this.filenameToScriptInfo.get(fileName); + let info = this.getScriptInfoForNormalizedPath(fileName); if (!info) { let content: string; if (this.host.fileExists(fileName)) { @@ -803,22 +845,26 @@ namespace ts.server { return info; } - log(msg: string, type = "Err") { - this.psLogger.msg(msg, type); + getScriptInfoForNormalizedPath(fileName: NormalizedPath) { + return this.filenameToScriptInfo.get(fileName); } - setHostConfiguration(args: ts.server.protocol.ConfigureRequestArguments) { + log(msg: string, type = "Err") { + this.logger.msg(msg, type); + } + + setHostConfiguration(args: protocol.ConfigureRequestArguments) { if (args.file) { - const info = this.filenameToScriptInfo.get(toNormalizedPath(args.file)); + const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(args.file)); if (info) { info.setFormatOptions(args.formatOptions); - this.log("Host configuration update for file " + args.file, "Info"); + this.log(`Host configuration update for file ${args.file}`, "Info"); } } else { if (args.hostInfo !== undefined) { this.hostConfiguration.hostInfo = args.hostInfo; - this.log("Host information " + args.hostInfo, "Info"); + this.log(`Host information ${args.hostInfo}`, "Info"); } if (args.formatOptions) { mergeMaps(this.hostConfiguration.formatCodeOptions, args.formatOptions); @@ -828,7 +874,7 @@ namespace ts.server { } closeLog() { - this.psLogger.close(); + this.logger.close(); } /** @@ -921,27 +967,33 @@ namespace ts.server { } } else { - // - openFileRoots.push(rootFile); - } - if (rootFile.containingProjects.some(p => p.projectKind !== ProjectKind.Inferred)) { - // file was included in non-inferred project - drop old inferred project - - } - else { - openFileRoots.push(rootFile); - } - let inferredProjectsToRemove: Project[]; - for (const p of rootFile.containingProjects) { - if (p.projectKind !== ProjectKind.Inferred) { - // file was included in non-inferred project - drop old inferred project + if (rootFile.containingProjects.length === 1) { + // file contained only in one project + openFileRoots.push(rootFile); + } + else { + // TODO: fixme + // file is contained in more than one inferred project - keep only ones where it is used as reference + const roots = rootFile.containingProjects.filter(p => p.isRoot(rootFile)); + for (const root of roots) { + this.removeProject(root); + } + Debug.assert(rootFile.containingProjects.length > 0); + this.openFilesReferenced.push(rootFile); } } - // if (inInferredProjectOnly) { - // openFileRoots.push(rootFile); + // if (rootFile.containingProjects.some(p => p.projectKind !== ProjectKind.Inferred)) { + // // file was included in non-inferred project - drop old inferred project + // } // else { - + // openFileRoots.push(rootFile); + // } + // let inferredProjectsToRemove: Project[]; + // for (const p of rootFile.containingProjects) { + // if (p.projectKind !== ProjectKind.Inferred) { + // // file was included in non-inferred project - drop old inferred project + // } // } // const rootedProject = rootFile.defaultProject; @@ -979,29 +1031,19 @@ namespace ts.server { this.printProjects(); } - getScriptInfo(uncheckedFileName: string) { - return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); - } - - getScriptInfoForNormalizedPath(fileName: NormalizedPath) { - return this.filenameToScriptInfo.get(fileName); - } /** * Open file whose contents is managed by the client * @param filename is absolute pathname * @param fileContent is a known version of the file content that is more up to date than the one on disk */ - openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind): { configFileName?: string, configFileErrors?: Diagnostic[] } { + openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind): OpenConfiguredProjectResult { return this.openClientFileWithNormalizedPath(toNormalizedPath(fileName), fileContent, scriptKind); } - openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind): { configFileName?: string, configFileErrors?: Diagnostic[] } { - let configFileName: string; - let configFileErrors: Diagnostic[]; - - if (!this.findContainingExternalProject(fileName)) { - ({ configFileName, configFileErrors } = this.openOrUpdateConfiguredProjectForFile(fileName)); - } + openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind): OpenConfiguredProjectResult { + const { configFileName = undefined, configFileErrors = undefined }: OpenConfiguredProjectResult = this.findContainingExternalProject(fileName) + ? {} + : this.openOrUpdateConfiguredProjectForFile(fileName); // at this point if file is the part of some configured/external project then this project should be created const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind); @@ -1015,7 +1057,7 @@ namespace ts.server { * @param filename is absolute pathname */ closeClientFile(uncheckedFileName: string) { - const info = this.filenameToScriptInfo.get(toNormalizedPath(uncheckedFileName)); + const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); if (info) { this.closeOpenFile(info); info.isOpen = false; @@ -1024,22 +1066,22 @@ namespace ts.server { } getDefaultProjectForFile(fileName: NormalizedPath) { - const scriptInfo = this.filenameToScriptInfo.get(fileName); + const scriptInfo = this.getScriptInfoForNormalizedPath(fileName); return scriptInfo && scriptInfo.getDefaultProject(); } - private syncExternalFilesList(knownProjects: protocol.ProjectVersionInfo[], projects: Project[], result: protocol.ProjectFiles[]): void { - for (const proj of projects) { - const knownProject = ts.forEach(knownProjects, p => p.projectName === proj.getProjectName() && p); + private collectChanges(lastKnownProjectVersions: protocol.ProjectVersionInfo[], currentProjects: Project[], result: protocol.ProjectFiles[]): void { + for (const proj of currentProjects) { + const knownProject = forEach(lastKnownProjectVersions, p => p.projectName === proj.getProjectName() && p); result.push(proj.getChangesSinceVersion(knownProject && knownProject.version)); } } synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[]): protocol.ProjectFiles[] { const files: protocol.ProjectFiles[] = []; - this.syncExternalFilesList(knownProjects, this.externalProjects, files); - this.syncExternalFilesList(knownProjects, this.configuredProjects, files); - this.syncExternalFilesList(knownProjects, this.inferredProjects, files); + this.collectChanges(knownProjects, this.externalProjects, files); + this.collectChanges(knownProjects, this.configuredProjects, files); + this.collectChanges(knownProjects, this.inferredProjects, files); return files; } @@ -1053,6 +1095,7 @@ namespace ts.server { for (const file of changedFiles) { const scriptInfo = this.getScriptInfo(file.fileName); Debug.assert(!!scriptInfo); + // apply changes in reverse order for (let i = file.changes.length - 1; i >= 0; i--) { const change = file.changes[i]; scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); @@ -1074,9 +1117,9 @@ namespace ts.server { const configuredProject = this.findConfiguredProjectByProjectName(configFile); if (configuredProject) { this.removeProject(configuredProject); - this.updateProjectStructure(); } } + this.updateProjectStructure(); } else { // close external project @@ -1092,33 +1135,33 @@ namespace ts.server { const externalProject = this.findExternalProjectByProjectName(proj.projectFileName); if (externalProject) { this.updateNonInferredProject(externalProject, proj.rootFiles, proj.options); + return; } - else { - let tsConfigFiles: NormalizedPath[]; - const rootFiles: string[] = []; - for (const file of proj.rootFiles) { - if (getBaseFileName(file) === "tsconfig.json") { - (tsConfigFiles || (tsConfigFiles = [])).push(toNormalizedPath(file)); - } - else { - rootFiles.push(file); - } - } - if (tsConfigFiles) { - // store the list of tsconfig files that belong to the external project - this.externalProjectToConfiguredProjectMap[proj.projectFileName] = tsConfigFiles; - for (const tsconfigFile of tsConfigFiles) { - const { success, project, errors } = this.openConfigFile(tsconfigFile); - if (success) { - // keep project alive - its lifetime is bound to the lifetime of containing external project - project.addOpenRef(); - } - } + + let tsConfigFiles: NormalizedPath[]; + const rootFiles: string[] = []; + for (const file of proj.rootFiles) { + if (getBaseFileName(file) === "tsconfig.json") { + (tsConfigFiles || (tsConfigFiles = [])).push(toNormalizedPath(file)); } else { - this.createAndAddExternalProject(proj.projectFileName, proj.rootFiles, proj.options); + rootFiles.push(file); } } + if (tsConfigFiles) { + // store the list of tsconfig files that belong to the external project + this.externalProjectToConfiguredProjectMap[proj.projectFileName] = tsConfigFiles; + for (const tsconfigFile of tsConfigFiles) { + const { success, project, errors } = this.openConfigFile(tsconfigFile); + if (success) { + // keep project alive - its lifetime is bound to the lifetime of containing external project + project.addOpenRef(); + } + } + } + else { + this.createAndAddExternalProject(proj.projectFileName, proj.rootFiles, proj.options); + } } } } diff --git a/src/server/lshost.ts b/src/server/lshost.ts index c5b44834e2f..b65c71d5e28 100644 --- a/src/server/lshost.ts +++ b/src/server/lshost.ts @@ -72,6 +72,10 @@ namespace ts.server { return this.project.getProjectVersion(); } + getCompilationSettings() { + return this.compilationSettings; + } + getCancellationToken() { return this.cancellationToken; } @@ -90,53 +94,30 @@ namespace ts.server { } getScriptSnapshot(filename: string): ts.IScriptSnapshot { - const scriptInfo = this.project.getScriptInfo(filename); + const scriptInfo = this.project.getScriptInfoLSHost(filename); if (scriptInfo) { return scriptInfo.snap(); } } - setCompilationSettings(opt: ts.CompilerOptions) { - this.compilationSettings = opt; - // conservatively assume that changing compiler options might affect module resolution strategy - this.resolvedModuleNames.clear(); - this.resolvedTypeReferenceDirectives.clear(); - } - - getCompilationSettings() { - // change this to return active project settings for file - return this.compilationSettings; - } - getScriptFileNames() { return this.project.getRootFiles(); } getScriptKind(fileName: string) { - const info = this.project.getScriptInfo(fileName); + const info = this.project.getScriptInfoLSHost(fileName); return info && info.scriptKind; } getScriptVersion(filename: string) { - return this.project.getScriptInfo(filename).getLatestVersion(); + const info = this.project.getScriptInfoLSHost(filename); + return info && info.getLatestVersion(); } getCurrentDirectory(): string { return ""; } - removeReferencedFile(info: ScriptInfo) { - if (!info.isOpen) { - this.resolvedModuleNames.remove(info.path); - this.resolvedTypeReferenceDirectives.remove(info.path); - } - } - - removeRoot(info: ScriptInfo) { - this.resolvedModuleNames.remove(info.path); - this.resolvedTypeReferenceDirectives.remove(info.path); - } - resolvePath(path: string): string { return this.host.resolvePath(path); } @@ -156,5 +137,17 @@ namespace ts.server { getDirectories(path: string): string[] { return this.host.getDirectories(path); } + + notifyFileRemoved(info: ScriptInfo) { + this.resolvedModuleNames.remove(info.path); + this.resolvedTypeReferenceDirectives.remove(info.path); + } + + setCompilationSettings(opt: ts.CompilerOptions) { + this.compilationSettings = opt; + // conservatively assume that changing compiler options might affect module resolution strategy + this.resolvedModuleNames.clear(); + this.resolvedTypeReferenceDirectives.clear(); + } } } \ No newline at end of file diff --git a/src/server/project.ts b/src/server/project.ts index 01c7939ca82..a3be7358b30 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -4,14 +4,22 @@ /// namespace ts.server { + export enum ProjectKind { Inferred, Configured, External } + function remove(items: T[], item: T) { + const index = items.indexOf(item); + if (index >= 0) { + items.splice(index, 1); + } + } + export abstract class Project { - private rootFiles: ScriptInfo[] = []; + private readonly rootFiles: ScriptInfo[] = []; private readonly rootFilesMap: FileMap = createFileMap(); private lsHost: ServerLanguageServiceHost; private program: ts.Program; @@ -130,10 +138,8 @@ namespace ts.server { containsFile(filename: NormalizedPath, requireOpen?: boolean) { const info = this.projectService.getScriptInfoForNormalizedPath(filename); - if (info) { - if ((!requireOpen) || info.isOpen) { - return this.containsScriptInfo(info); - } + if (info && (info.isOpen || !requireOpen)) { + return this.containsScriptInfo(info); } } @@ -153,12 +159,13 @@ namespace ts.server { } removeFile(info: ScriptInfo, detachFromProject: boolean = true) { - if (!this.removeRoot(info)) { - this.removeReferencedFile(info) - } + this.removeRootFileIfNecessary(info); + this.lsHost.notifyFileRemoved(info); + if (detachFromProject) { info.detachFromProject(this); } + this.markAsDirty(); } @@ -179,14 +186,31 @@ namespace ts.server { // - newProgram is different from the old program and structure of the old program was not reused. if (!oldProgram || (this.program !== oldProgram && !oldProgram.structureIsReused)) { this.projectStructureVersion++; + if (oldProgram) { + for (const f of oldProgram.getSourceFiles()) { + if (!this.program.getSourceFileByPath(f.path)) { + // new program does not contain this file - detach it from the project + const scriptInfoToDetach = this.projectService.getScriptInfo(f.fileName); + if (scriptInfoToDetach) { + scriptInfoToDetach.detachFromProject(this); + } + } + } + } } } + getScriptInfoLSHost(fileName: string) { + const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); + if (scriptInfo) { + scriptInfo.attachToProject(this); + } + return scriptInfo; + } + getScriptInfoForNormalizedPath(fileName: NormalizedPath) { const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false); - if (scriptInfo && scriptInfo.attachToProject(this)) { - this.markAsDirty(); - } + Debug.assert(!scriptInfo || scriptInfo.isAttached(this)); return scriptInfo; } @@ -215,19 +239,23 @@ namespace ts.server { } } - saveTo(filename: string, tmpfilename: string) { - const script = this.getScriptInfo(filename); + saveTo(filename: NormalizedPath, tmpfilename: NormalizedPath) { + const script = this.projectService.getScriptInfoForNormalizedPath(filename); if (script) { + Debug.assert(script.isAttached(this)); const snap = script.snap(); this.projectService.host.writeFile(tmpfilename, snap.getText(0, snap.getLength())); } } - reloadScript(filename: NormalizedPath, cb: () => void) { - const script = this.getScriptInfoForNormalizedPath(filename); + reloadScript(filename: NormalizedPath): boolean { + const script = this.projectService.getScriptInfoForNormalizedPath(filename); if (script) { - script.reloadFromFile(filename, cb); + Debug.assert(script.isAttached(this)); + script.reloadFromFile(); + return true; } + return false; } getChangesSinceVersion(lastKnownVersion?: number): protocol.ProjectFiles { @@ -274,18 +302,11 @@ namespace ts.server { } // remove a root file from project - private removeRoot(info: ScriptInfo): boolean { + private removeRootFileIfNecessary(info: ScriptInfo): void { if (this.isRoot(info)) { - this.rootFiles = copyListRemovingItem(info, this.rootFiles); + remove(this.rootFiles, info); this.rootFilesMap.remove(info.path); - this.lsHost.removeRoot(info); - return true; } - return false; - } - - private removeReferencedFile(info: ScriptInfo) { - this.lsHost.removeReferencedFile(info) } } diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 52e1baf36b4..95023a93b74 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -3,15 +3,15 @@ namespace ts.server { export class ScriptInfo { - private svc: ScriptVersionCache; /** * All projects that include this file */ readonly containingProjects: Project[] = []; + readonly formatCodeSettings: ts.FormatCodeSettings; + readonly path: Path; private fileWatcher: FileWatcher; - formatCodeSettings: ts.FormatCodeSettings; - readonly path: Path; + private svc: ScriptVersionCache; constructor( private readonly host: ServerHost, @@ -29,11 +29,15 @@ namespace ts.server { } attachToProject(project: Project): boolean { - if (!contains(this.containingProjects, project)) { + const isNew = !this.isAttached(project); + if (isNew) { this.containingProjects.push(project); - return true; } - return false; + return isNew; + } + + isAttached(project: Project) { + return contains(this.containingProjects, project); } detachFromProject(project: Project) { @@ -80,8 +84,8 @@ namespace ts.server { this.markContainingProjectsAsDirty(); } - reloadFromFile(fileName: string, cb?: () => void) { - this.svc.reloadFromFile(fileName, cb) + reloadFromFile() { + this.svc.reloadFromFile(this.fileName); this.markContainingProjectsAsDirty(); } diff --git a/src/server/scriptVersionCache.ts b/src/server/scriptVersionCache.ts index 8269896059f..09f5905bff6 100644 --- a/src/server/scriptVersionCache.ts +++ b/src/server/scriptVersionCache.ts @@ -297,7 +297,7 @@ namespace ts.server { return this.currentVersion; } - reloadFromFile(filename: string, cb?: () => void) { + reloadFromFile(filename: string) { let content = this.host.readFile(filename); // If the file doesn't exist or cannot be read, we should // wipe out its cached content on the server to avoid side effects. @@ -305,8 +305,6 @@ namespace ts.server { content = ""; } this.reload(content); - if (cb) - cb(); } // reload whole script, leaving no change history behind reload diff --git a/src/server/session.ts b/src/server/session.ts index 29c8dcd4abd..c866ec178cc 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1018,7 +1018,7 @@ namespace ts.server { scriptInfo.editContent(start, end, args.insertString); this.changeSeq++; } - this.updateProjectStructure(this.changeSeq, (n) => n === this.changeSeq); + this.updateProjectStructure(this.changeSeq, n => n === this.changeSeq); } } @@ -1028,15 +1028,15 @@ namespace ts.server { if (project) { this.changeSeq++; // make sure no changes happen before this one is finished - project.reloadScript(file, () => { + if (project.reloadScript(file)) { this.output(undefined, CommandNames.Reload, reqSeq); - }); + } } } private saveToTmp(fileName: string, tempFileName: string) { const file = toNormalizedPath(fileName); - const tmpfile = ts.normalizePath(tempFileName); + const tmpfile = toNormalizedPath(tempFileName); const project = this.projectService.getDefaultProjectForFile(file); if (project) { @@ -1267,6 +1267,7 @@ namespace ts.server { }, [CommandNames.ApplyChangedToOpenFiles]: (request: protocol.ApplyChangedToOpenFilesRequest) => { this.projectService.applyChangesInOpenFiles(request.arguments.openFiles, request.arguments.changedFiles, request.arguments.closedFiles); + this.changeSeq++; // TODO: report errors return this.requiredResponse(true); }, diff --git a/src/server/utilities.ts b/src/server/utilities.ts index c6da27505ac..ef877362281 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -140,17 +140,13 @@ namespace ts.server { }; export interface ServerLanguageServiceHost { - getCompilationSettings(): CompilerOptions; setCompilationSettings(options: CompilerOptions): void; - removeRoot(info: ScriptInfo): void; - removeReferencedFile(info: ScriptInfo): void; + notifyFileRemoved(info: ScriptInfo): void; } export const nullLanguageServiceHost: ServerLanguageServiceHost = { - getCompilationSettings: () => undefined, setCompilationSettings: () => undefined, - removeRoot: () => undefined, - removeReferencedFile: () => undefined + notifyFileRemoved: () => undefined }; export interface ProjectOptions { From 4157655215305eedfb2d409aa53b27c91e365a08 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 24 Jun 2016 14:55:12 -0700 Subject: [PATCH 063/307] [in progress] project system work - fixes in tests --- src/server/editorServices.ts | 53 ++++++++++++++++++------------------ src/server/project.ts | 11 ++++---- src/server/scriptInfo.ts | 4 +-- src/server/session.ts | 4 +-- src/server/utilities.ts | 9 +++--- 5 files changed, 41 insertions(+), 40 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index bc645cb9f8e..623fdddfacb 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -35,10 +35,10 @@ namespace ts.server { } interface OpenConfigFileResult { - success: boolean, - errors?: Diagnostic[] + success: boolean; + errors?: Diagnostic[]; - project?: ConfiguredProject, + project?: ConfiguredProject; } interface OpenConfiguredProjectResult { @@ -304,23 +304,23 @@ namespace ts.server { } // TODO: delete if unused - private releaseNonReferencedConfiguredProjects() { - if (this.configuredProjects.every(p => p.openRefCount > 0)) { - return; - } + // private releaseNonReferencedConfiguredProjects() { + // if (this.configuredProjects.every(p => p.openRefCount > 0)) { + // return; + // } - const configuredProjects: ConfiguredProject[] = []; - for (const proj of this.configuredProjects) { - if (proj.openRefCount > 0) { - configuredProjects.push(proj); - } - else { - proj.close(); - } - } + // const configuredProjects: ConfiguredProject[] = []; + // for (const proj of this.configuredProjects) { + // if (proj.openRefCount > 0) { + // configuredProjects.push(proj); + // } + // else { + // proj.close(); + // } + // } - this.configuredProjects = configuredProjects; - } + // this.configuredProjects = configuredProjects; + // } private removeProject(project: Project) { this.log(`remove project: ${project.getRootFiles().toString()}`); @@ -540,7 +540,7 @@ namespace ts.server { return; } this.logger.startGroup(); - + let counter = 0; counter = printProjects(this.externalProjects, counter); counter = printProjects(this.configuredProjects, counter); @@ -636,10 +636,10 @@ namespace ts.server { private createAndAddExternalProject(projectFileName: string, files: string[], compilerOptions: CompilerOptions) { const project = new ExternalProject( - projectFileName, - this, - this.documentRegistry, - compilerOptions, + projectFileName, + this, + this.documentRegistry, + compilerOptions, /*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(compilerOptions, files)); const errors = this.addFilesToProjectAndUpdateGraph(project, files, /*clientFileName*/ undefined); @@ -648,7 +648,7 @@ namespace ts.server { } private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, clientFileName?: string) { - const sizeLimitExceeded = !this.exceededTotalSizeLimitForNonTsFiles(projectOptions.compilerOptions, projectOptions.files); + const sizeLimitExceeded = this.exceededTotalSizeLimitForNonTsFiles(projectOptions.compilerOptions, projectOptions.files); const project = new ConfiguredProject( configFileName, this, @@ -665,7 +665,7 @@ namespace ts.server { this.watchConfigDirectoryForProject(project, projectOptions); } project.watchWildcards((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path)); - + this.configuredProjects.push(project); return { project, errors }; } @@ -902,7 +902,7 @@ namespace ts.server { const openFileRootsConfigured: ScriptInfo[] = []; // collect all orphanted script infos that used to be roots of configured projects for (const info of this.openFileRootsConfigured) { - if(info.containingProjects.length === 0) { + if (info.containingProjects.length === 0) { unattachedOpenFiles.push(info); } else { @@ -999,7 +999,6 @@ namespace ts.server { // const rootedProject = rootFile.defaultProject; // const referencingProjects = this.findReferencingProjects(rootFile, rootedProject); - // if (rootFile.defaultProject && rootFile.defaultProject.projectKind !== ProjectKind.Inferred) { // // If the root file has already been added into a configured project, // // meaning the original inferred project is gone already. diff --git a/src/server/project.ts b/src/server/project.ts index a3be7358b30..ff3ec6a0f45 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -11,13 +11,13 @@ namespace ts.server { External } - function remove(items: T[], item: T) { + function remove(items: T[], item: T) { const index = items.indexOf(item); if (index >= 0) { items.splice(index, 1); } } - + export abstract class Project { private readonly rootFiles: ScriptInfo[] = []; private readonly rootFilesMap: FileMap = createFileMap(); @@ -153,12 +153,12 @@ namespace ts.server { this.rootFiles.push(info); this.rootFilesMap.set(info.path, info); info.attachToProject(this); - + this.markAsDirty(); } } - removeFile(info: ScriptInfo, detachFromProject: boolean = true) { + removeFile(info: ScriptInfo, detachFromProject = true) { this.removeRootFileIfNecessary(info); this.lsHost.notifyFileRemoved(info); @@ -330,7 +330,8 @@ namespace ts.server { languageServiceEnabled, /*compilerOptions*/ undefined); - this.inferredProjectName = makeInferredProjectName(InferredProject.NextId++); + this.inferredProjectName = makeInferredProjectName(InferredProject.NextId); + InferredProject.NextId++; } getProjectName() { diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 95023a93b74..d5c01f609dd 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -15,9 +15,9 @@ namespace ts.server { constructor( private readonly host: ServerHost, - readonly fileName: NormalizedPath, + readonly fileName: NormalizedPath, content: string, - readonly scriptKind: ScriptKind, + readonly scriptKind: ScriptKind, public isOpen = false) { this.path = toPath(fileName, host.getCurrentDirectory(), createGetCanonicalFileName(host.useCaseSensitiveFileNames)); diff --git a/src/server/session.ts b/src/server/session.ts index c866ec178cc..10cf03e96da 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -436,7 +436,7 @@ namespace ts.server { } private getTypeDefinition(args: protocol.FileLocationRequestArgs): protocol.FileSpan[] { - const { file, project } = this.getFileAndProject(args) + const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); @@ -1443,7 +1443,7 @@ namespace ts.server { return this.requiredResponse(this.getNavigationBarItems(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.Occurrences]: (request: protocol.FileLocationRequest) => { - return this.requiredResponse(this.getOccurrences(request.arguments));; + return this.requiredResponse(this.getOccurrences(request.arguments)); }, [CommandNames.DocumentHighlights]: (request: protocol.DocumentHighlightsRequest) => { return this.requiredResponse(this.getDocumentHighlights(request.arguments, /*simplifiedResult*/ true)); diff --git a/src/server/utilities.ts b/src/server/utilities.ts index ef877362281..18970f578b3 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -1,5 +1,7 @@ /// +/* tslint:disable:no-null-keyword */ + namespace ts.server { export interface Logger { close(): void; @@ -55,7 +57,6 @@ namespace ts.server { items[index] = items.pop(); } } - export type NormalizedPath = string & { __normalizedPathTag: any }; @@ -91,9 +92,9 @@ namespace ts.server { return hasProperty(map, path); }, remove(path) { - delete map[path] + delete map[path]; } - } + }; } function throwLanguageServiceIsDisabledError() {; throw new Error("LanguageService is disabled"); @@ -169,5 +170,5 @@ namespace ts.server { export function makeInferredProjectName(counter: number) { return `/dev/null/inferredProject${counter}*`; - } + } } \ No newline at end of file From 8bcca681024f268a11e6ec2adf5826d0ce66b537 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 24 Jun 2016 15:49:29 -0700 Subject: [PATCH 064/307] [in progress] project system work - fixes in tests 2 --- src/server/editorServices.ts | 47 ++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 623fdddfacb..a322dd1630b 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -376,30 +376,35 @@ namespace ts.server { } return; } + if (info.containingProjects.length === 0) { + // create new inferred project p with the newly opened file as root + const inferredProject = this.createAndAddInferredProject(info); + const openFileRoots: ScriptInfo[] = []; + // for each inferred project root r + for (const rootFile of this.openFileRoots) { + // if r referenced by the new project + if (inferredProject.containsScriptInfo(rootFile)) { + // remove inferred project that was initially created for rootFile + const defaultProject = rootFile.getDefaultProject(); + if (defaultProject === inferredProject) { + continue; + } + Debug.assert(defaultProject.projectKind === ProjectKind.Inferred); - // create new inferred project p with the newly opened file as root - const inferredProject = this.createAndAddInferredProject(info); - const openFileRoots: ScriptInfo[] = []; - // for each inferred project root r - for (const rootFile of this.openFileRoots) { - // if r referenced by the new project - if (inferredProject.containsScriptInfo(rootFile)) { - // remove inferred project that was initially created for rootFile - const defaultProject = rootFile.getDefaultProject(); - Debug.assert(defaultProject.projectKind === ProjectKind.Inferred); - - this.removeProject(defaultProject); - // put r in referenced open file list - this.openFilesReferenced.push(rootFile); - // set default project of r to the new project - rootFile.attachToProject(inferredProject); - } - else { - // otherwise, keep r as root of inferred project - openFileRoots.push(rootFile); + this.removeProject(defaultProject); + // put r in referenced open file list + this.openFilesReferenced.push(rootFile); + // set default project of r to the new project + rootFile.attachToProject(inferredProject); + } + else { + // otherwise, keep r as root of inferred project + openFileRoots.push(rootFile); + } } + this.openFileRoots = openFileRoots; } - this.openFileRoots = openFileRoots; + this.openFileRoots.push(info); } From efa2b70ded48badf55b296de9f621ccb4287451e Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 24 Jun 2016 16:49:24 -0700 Subject: [PATCH 065/307] fix logger access issue --- src/server/editorServices.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index a322dd1630b..3f476ba9d19 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -547,9 +547,9 @@ namespace ts.server { this.logger.startGroup(); let counter = 0; - counter = printProjects(this.externalProjects, counter); - counter = printProjects(this.configuredProjects, counter); - counter = printProjects(this.inferredProjects, counter); + counter = printProjects(this.logger, this.externalProjects, counter); + counter = printProjects(this.logger, this.configuredProjects, counter); + counter = printProjects(this.logger, this.inferredProjects, counter); this.logger.info("Open file roots of inferred projects: "); for (const rootFile of this.openFileRoots) { @@ -567,12 +567,12 @@ namespace ts.server { this.logger.endGroup(); - function printProjects(projects: Project[], counter: number) { + function printProjects(logger: Logger, projects: Project[], counter: number) { for (const project of projects) { project.updateGraph(); - this.psLogger.info(`Project '${project.getProjectName()}' (${ProjectKind[project.projectKind]}) ${counter}`); - this.psLogger.info(project.filesToString()); - this.psLogger.info("-----------------------------------------------"); + logger.info(`Project '${project.getProjectName()}' (${ProjectKind[project.projectKind]}) ${counter}`); + logger.info(project.filesToString()); + logger.info("-----------------------------------------------"); counter++; } return counter; From f54ca3929a02f9ec5059a96d4ec5509bb40637b8 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 24 Jun 2016 17:15:36 -0700 Subject: [PATCH 066/307] added server bits to jake file --- Jakefile.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/Jakefile.js b/Jakefile.js index 933c58c7a39..b7bf34a5f3d 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -123,9 +123,16 @@ var cancellationTokenSources = [ var serverSources = serverCoreSources.concat(servicesSources); var languageServiceLibrarySources = [ + "protocol.d.ts", + "utilities.ts", + "scriptVersionCache.ts", + "scriptInfo.ts", + "lsHost.ts", + "project.ts", "editorServices.ts", "protocol.d.ts", - "session.ts" + "session.ts", + ].map(function (f) { return path.join(serverDirectory, f); }).concat(servicesSources); @@ -173,10 +180,14 @@ var harnessSources = harnessCoreSources.concat([ return path.join(unittestsDirectory, f); })).concat([ "protocol.d.ts", - "session.ts", - "client.ts", + "utilities.ts", "scriptVersionCache.ts", - "editorServices.ts" + "scriptInfo.ts", + "lsHost.ts", + "project.ts", + "editorServices.ts", + "protocol.d.ts", + "session.ts", ].map(function (f) { return path.join(serverDirectory, f); })); From 417c1254ebd5cd0dee3b3d2c6d6fa2b4f9793f0a Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 24 Jun 2016 17:27:18 -0700 Subject: [PATCH 067/307] fix casing --- src/server/editorServices.ts | 2 +- src/server/{lshost.ts => lsHost.ts} | 0 src/server/project.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/server/{lshost.ts => lsHost.ts} (100%) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 3f476ba9d19..adc038d1882 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -4,7 +4,7 @@ /// /// /// -/// +/// /// namespace ts.server { diff --git a/src/server/lshost.ts b/src/server/lsHost.ts similarity index 100% rename from src/server/lshost.ts rename to src/server/lsHost.ts diff --git a/src/server/project.ts b/src/server/project.ts index ff3ec6a0f45..2e735b01237 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1,7 +1,7 @@ /// /// /// -/// +/// namespace ts.server { From 96a867a52ef7ad9f5483a4475a903d93cf55ac33 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 27 Jun 2016 14:25:43 -0700 Subject: [PATCH 068/307] add support for single inferred project --- src/harness/harnessLanguageService.ts | 1 + src/server/editorServices.ts | 86 +++++++---- src/server/project.ts | 4 + src/server/protocol.d.ts | 5 + src/server/server.ts | 7 +- src/server/session.ts | 14 +- .../cases/unittests/cachingInServerLSHost.ts | 4 +- tests/cases/unittests/session.ts | 6 +- .../cases/unittests/tsserverProjectSystem.ts | 145 +++++++++++++----- 9 files changed, 183 insertions(+), 89 deletions(-) diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 7a7d0f6dcd8..553d59428ab 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -681,6 +681,7 @@ namespace Harness.LanguageService { const serverHost = new SessionServerHost(clientHost); const server = new ts.server.Session(serverHost, { isCancellationRequested: () => false }, + /*useOneInferredProject*/ false, Buffer ? Buffer.byteLength : (string: string, encoding?: string) => string.length, process.hrtime, serverHost); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f5a7aba2917..857d2c95dc5 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -137,26 +137,25 @@ namespace ts.server { private readonly directoryWatchers: DirectoryWatchers; - private hostConfiguration: HostConfiguration; + private readonly hostConfiguration: HostConfiguration; private timerForDetectingProjectFileListChanges: Map = {}; constructor(public readonly host: ServerHost, public readonly logger: Logger, public readonly cancellationToken: HostCancellationToken, + private readonly useOneInferredProject: boolean, private readonly eventHandler?: ProjectServiceEventHandler) { this.directoryWatchers = new DirectoryWatchers(this); // ts.disableIncrementalParsing = true; - this.setDefaultHostConfiguration(); - this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); - } - private setDefaultHostConfiguration() { this.hostConfiguration = { formatCodeOptions: getDefaultFormatCodeSettings(this.host), hostInfo: "Unknown host" }; + + this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); } stopWatchingDirectory(directory: string) { @@ -378,31 +377,36 @@ namespace ts.server { } if (info.containingProjects.length === 0) { // create new inferred project p with the newly opened file as root - const inferredProject = this.createAndAddInferredProject(info); - const openFileRoots: ScriptInfo[] = []; - // for each inferred project root r - for (const rootFile of this.openFileRoots) { - // if r referenced by the new project - if (inferredProject.containsScriptInfo(rootFile)) { - // remove inferred project that was initially created for rootFile - const defaultProject = rootFile.getDefaultProject(); - if (defaultProject === inferredProject) { - continue; - } - Debug.assert(defaultProject.projectKind === ProjectKind.Inferred); + // or add root to existing inferred project if 'useOneInferredProject' is true + const inferredProject = this.addFileToInferredProject(info); + if (!this.useOneInferredProject) { - this.removeProject(defaultProject); - // put r in referenced open file list - this.openFilesReferenced.push(rootFile); - // set default project of r to the new project - rootFile.attachToProject(inferredProject); - } - else { - // otherwise, keep r as root of inferred project - openFileRoots.push(rootFile); + // if useOneInferredProject is not set then try to fixup ownership of open files + const openFileRoots: ScriptInfo[] = []; + // for each inferred project root r + for (const rootFile of this.openFileRoots) { + // if r referenced by the new project + if (inferredProject.containsScriptInfo(rootFile)) { + // remove inferred project that was initially created for rootFile + const defaultProject = rootFile.getDefaultProject(); + if (defaultProject === inferredProject) { + continue; + } + Debug.assert(defaultProject.projectKind === ProjectKind.Inferred); + + this.removeProject(defaultProject); + // put r in referenced open file list + this.openFilesReferenced.push(rootFile); + // set default project of r to the new project + rootFile.attachToProject(inferredProject); + } + else { + // otherwise, keep r as root of inferred project + openFileRoots.push(rootFile); + } } + this.openFileRoots = openFileRoots; } - this.openFileRoots = openFileRoots; } this.openFileRoots.push(info); @@ -736,6 +740,8 @@ namespace ts.server { // delete inferred project let toRemove: Project[]; + + // TODO: unify logic for (const p of info.containingProjects) { if (p.projectKind === ProjectKind.Inferred && p.isRoot(info)) { (toRemove || (toRemove = [])).push(p); @@ -743,7 +749,10 @@ namespace ts.server { } if (toRemove) { for (const p of toRemove) { - this.removeProject(p); + p.removeFile(info); + if (!p.hasRoots()) { + this.removeProject(p); + } } } } @@ -792,8 +801,12 @@ namespace ts.server { } } - createAndAddInferredProject(root: ScriptInfo) { - const project = new InferredProject(this, this.documentRegistry, /*languageServiceEnabled*/ true); + addFileToInferredProject(root: ScriptInfo) { + const useExistingProject = this.useOneInferredProject && this.inferredProjects.length; + const project = useExistingProject + ? this.inferredProjects[0] + : new InferredProject(this, this.documentRegistry, /*languageServiceEnabled*/ true); + project.addRoot(root); this.directoryWatchers.startWatchingContainingDirectoriesForFile( @@ -802,7 +815,10 @@ namespace ts.server { fileName => this.onConfigFileAddedForInferredProject(fileName)); project.updateGraph(); - this.inferredProjects.push(project); + + if (!useExistingProject) { + this.inferredProjects.push(project); + } return project; } @@ -966,7 +982,10 @@ namespace ts.server { if (inConfiguredProject || inExternalProject) { const inferredProjects = rootFile.containingProjects.filter(p => p.projectKind === ProjectKind.Inferred); for (const p of inferredProjects) { - this.removeProject(p); + p.removeFile(rootFile, /*detachFromProject*/ true); + if (!p.hasRoots()) { + this.removeProject(p); + } } if (inConfiguredProject) { this.openFileRootsConfigured.push(rootFile); @@ -1033,6 +1052,9 @@ namespace ts.server { for (const f of unattachedOpenFiles) { this.addOpenFile(f); } + for (const p of this.inferredProjects) { + p.updateGraph(); + } this.printProjects(); } diff --git a/src/server/project.ts b/src/server/project.ts index 2e735b01237..00eeda9d8f0 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -108,6 +108,10 @@ namespace ts.server { return this.compilerOptions; } + hasRoots() { + return this.rootFiles.length > 0; + } + getRootFiles() { return this.rootFiles.map(info => info.fileName); } diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 54ce4acea41..e89b737b8c1 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -606,6 +606,11 @@ declare namespace ts.server.protocol { * The format options to use during formatting and other code editing features. */ formatOptions?: FormatOptions; + + /** + * If set to true - then all loose files will land into one inferred project + */ + useOneInferredProject?: boolean; } /** diff --git a/src/server/server.ts b/src/server/server.ts index 98762982d3f..79b40836f54 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -91,8 +91,8 @@ namespace ts.server { } class IOSession extends Session { - constructor(host: ServerHost, cancellationToken: HostCancellationToken, logger: ts.server.Logger) { - super(host, cancellationToken, Buffer.byteLength, process.hrtime, logger); + constructor(host: ServerHost, cancellationToken: HostCancellationToken, useOneInferredProject: boolean, logger: ts.server.Logger) { + super(host, cancellationToken, useOneInferredProject, Buffer.byteLength, process.hrtime, logger); } exit() { @@ -304,7 +304,8 @@ namespace ts.server { }; }; - const ioSession = new IOSession(sys, cancellationToken, logger); + const useOneInferredProject = sys.args.some(arg => arg === "--useOneInferredProject"); + const ioSession = new IOSession(sys, cancellationToken, useOneInferredProject, logger); process.on("uncaughtException", function(err: Error) { ioSession.logError(err, "unknown"); }); diff --git a/src/server/session.ts b/src/server/session.ts index d300cd13990..dfe532805cd 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -178,12 +178,13 @@ namespace ts.server { constructor( private host: ServerHost, - private cancellationToken: HostCancellationToken, + cancellationToken: HostCancellationToken, + useOneInferredProject: boolean, private byteLength: (buf: string, encoding?: string) => number, private hrtime: (start?: number[]) => number[], private logger: Logger) { this.projectService = - new ProjectService(host, logger, cancellationToken, (eventName, project, fileName) => { + new ProjectService(host, logger, cancellationToken, useOneInferredProject, (eventName, project, fileName) => { this.handleEvent(eventName, project, fileName); }); } @@ -1380,10 +1381,10 @@ namespace ts.server { this.cleanup(); return this.requiredResponse(true); }, - [CommandNames.SemanticDiagnosticsSync]: (request: protocol.FileRequest) => { + [CommandNames.SemanticDiagnosticsSync]: (request: protocol.SemanticDiagnosticsSyncRequest) => { return this.requiredResponse(this.getSemanticDiagnosticsSync(request.arguments)); }, - [CommandNames.SyntacticDiagnosticsSync]: (request: protocol.FileRequest) => { + [CommandNames.SyntacticDiagnosticsSync]: (request: protocol.SyntacticDiagnosticsSyncRequest) => { return this.requiredResponse(this.getSyntacticDiagnosticsSync(request.arguments)); }, [CommandNames.Geterr]: (request: protocol.Request) => { @@ -1398,9 +1399,8 @@ namespace ts.server { this.change(request.arguments); return this.notRequired(); }, - [CommandNames.Configure]: (request: protocol.Request) => { - const configureArgs = request.arguments; - this.projectService.setHostConfiguration(configureArgs); + [CommandNames.Configure]: (request: protocol.ConfigureRequest) => { + this.projectService.setHostConfiguration(request.arguments); this.output(undefined, CommandNames.Configure, request.seq); return this.notRequired(); }, diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index 2402102496d..0a96afa14f4 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -79,9 +79,9 @@ namespace ts { msg: (s: string, type?: string) => { } }; - const projectService = new server.ProjectService(serverHost, logger, { isCancellationRequested: () => false }); + const projectService = new server.ProjectService(serverHost, logger, { isCancellationRequested: () => false }, /*useOneInferredProject*/ false); const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */true, /*containingProject*/ undefined); - const project = projectService.createAndAddInferredProject(rootScriptInfo); + const project = projectService.addFileToInferredProject(rootScriptInfo); project.setCompilerOptions({ module: ts.ModuleKind.AMD } ); return { project, diff --git a/tests/cases/unittests/session.ts b/tests/cases/unittests/session.ts index a687d8cac93..c8a952da02e 100644 --- a/tests/cases/unittests/session.ts +++ b/tests/cases/unittests/session.ts @@ -40,7 +40,7 @@ namespace ts.server { let lastSent: protocol.Message; beforeEach(() => { - session = new Session(mockHost, nullCancellationToken, Utils.byteLength, process.hrtime, mockLogger); + session = new Session(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, process.hrtime, mockLogger); session.send = (msg: protocol.Message) => { lastSent = msg; }; @@ -265,7 +265,7 @@ namespace ts.server { lastSent: protocol.Message; customHandler = "testhandler"; constructor() { - super(mockHost, nullCancellationToken, Utils.byteLength, process.hrtime, mockLogger); + super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, process.hrtime, mockLogger); this.addProtocolHandler(this.customHandler, () => { return { response: undefined, responseRequired: true }; }); @@ -323,7 +323,7 @@ namespace ts.server { class InProcSession extends Session { private queue: protocol.Request[] = []; constructor(private client: InProcClient) { - super(mockHost, nullCancellationToken, Utils.byteLength, process.hrtime, mockLogger); + super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, process.hrtime, mockLogger); this.addProtocolHandler("echo", (req: protocol.Request) => ({ response: req.arguments, responseRequired: true diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index 78cb8a23716..9e003bc8049 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -26,6 +26,22 @@ namespace ts { return combinePaths(getDirectoryPath(libFile.path), "tsc.js"); } + interface TestServerHostCreationParameters { + fileOrFolderList: FileOrFolder[]; + useCaseSensitiveFileNames?: boolean; + executingFilePath?: string; + libFile?: FileOrFolder; + currentDirectory?: string; + } + + function createServerHost(params: TestServerHostCreationParameters): TestServerHost { + return new TestServerHost( + params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false, + params.executingFilePath || getExecutingFilePathFromLibFile(params.libFile), + params.currentDirectory || "/", + params.fileOrFolderList); + } + interface FileOrFolder { path: string; content?: string; @@ -112,12 +128,12 @@ namespace ts { checkMapKeys("watchedDirectories", host.watchedDirectories, expectedDirectories); } - function checkConfiguredProjectActualFiles(project: server.Project, expectedFiles: string[]) { - checkFileNames("configuredProjects project, actualFileNames", project.getFileNames(), expectedFiles); + function checkProjectActualFiles(project: server.Project, expectedFiles: string[]) { + checkFileNames(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles); } - function checkConfiguredProjectRootFiles(project: server.Project, expectedFiles: string[]) { - checkFileNames("configuredProjects project, rootFileNames", project.getRootFiles(), expectedFiles); + function checkProjectRootFiles(project: server.Project, expectedFiles: string[]) { + checkFileNames(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); } type TimeOutCallback = () => any; @@ -193,7 +209,7 @@ namespace ts { return ts.matchFiles(path, extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), (dir) => { const result: FileSystemEntries = { directories: [], - files : [] + files: [] }; const dirEntry = that.fs.get(that.toPath(dir)); if (isFolder(dirEntry)) { @@ -325,8 +341,8 @@ namespace ts { path: "/a/b/c/module.d.ts", content: `export let x: number` }; - const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [appFile, moduleFile, libFile]); - const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); + const host = createServerHost({ fileOrFolderList: [appFile, moduleFile, libFile], libFile }); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); const { configFileName } = projectService.openClientFile(appFile.path); assert(!configFileName, `should not find config, got: '${configFileName}`); @@ -363,8 +379,9 @@ namespace ts { content: "let z = 1" }; - const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [ configFile, libFile, file1, file2, file3 ]); - const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); + + const host = createServerHost({ fileOrFolderList: [configFile, libFile, file1, file2, file3], libFile }); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); assert(configFileName, "should find config file"); @@ -373,8 +390,8 @@ namespace ts { checkNumberOfConfiguredProjects(projectService, 1); const project = projectService.configuredProjects[0]; - checkConfiguredProjectActualFiles(project, [file1.path, libFile.path, file2.path]); - checkConfiguredProjectRootFiles(project, [file1.path, file2.path]); + checkProjectActualFiles(project, [file1.path, libFile.path, file2.path]); + checkProjectRootFiles(project, [file1.path, file2.path]); // watching all files except one that was open checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); checkWatchedDirectories(host, [getDirectoryPath(configFile.path)]); @@ -387,10 +404,11 @@ namespace ts { "files": ["commonFile1.ts"] }` }; - const filesWithoutConfig = [ libFile, commonFile1, commonFile2 ]; - const filesWithConfig = [ libFile, commonFile1, commonFile2, configFile ]; - const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", filesWithoutConfig); - const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); + const filesWithoutConfig = [libFile, commonFile1, commonFile2]; + const host = createServerHost({ fileOrFolderList: filesWithoutConfig, libFile }); + + const filesWithConfig = [libFile, commonFile1, commonFile2, configFile]; + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(commonFile1.path); projectService.openClientFile(commonFile2.path); @@ -420,21 +438,21 @@ namespace ts { path: "/a/b/tsconfig.json", content: `{}` }; - const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, libFile, configFile]); - const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); + const host = createServerHost({ fileOrFolderList: [commonFile1, libFile, configFile], libFile }); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(commonFile1.path); checkWatchedDirectories(host, ["/a/b"]); checkNumberOfConfiguredProjects(projectService, 1); const project = projectService.configuredProjects[0]; - checkConfiguredProjectRootFiles(project, [commonFile1.path]); + checkProjectRootFiles(project, [commonFile1.path]); // add a new ts file host.reloadFS([commonFile1, commonFile2, libFile, configFile]); host.triggerDirectoryWatcherCallback("/a/b", commonFile2.path); host.runQueuedTimeoutCallbacks(); // project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer. - checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); }); it("should ignore non-existing files specified in the config file", () => { @@ -448,14 +466,14 @@ namespace ts { ] }` }; - const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, commonFile2, configFile]); - const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); + const host = createServerHost({ fileOrFolderList: [commonFile1, commonFile2, configFile], libFile }); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(commonFile1.path); projectService.openClientFile(commonFile2.path); checkNumberOfConfiguredProjects(projectService, 1); const project = projectService.configuredProjects[0]; - checkConfiguredProjectRootFiles(project, [commonFile1.path]); + checkProjectRootFiles(project, [commonFile1.path]); checkNumberOfInferredProjects(projectService, 1); }); @@ -464,25 +482,25 @@ namespace ts { path: "/a/b/tsconfig.json", content: `{}` }; - const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, commonFile2, configFile]); - const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); + const host = createServerHost({ fileOrFolderList: [commonFile1, commonFile2, configFile], libFile }); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(commonFile1.path); checkNumberOfConfiguredProjects(projectService, 1); const project = projectService.configuredProjects[0]; - checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); // delete commonFile2 host.reloadFS([commonFile1, configFile]); host.triggerDirectoryWatcherCallback("/a/b", commonFile2.path); host.runQueuedTimeoutCallbacks(); - checkConfiguredProjectRootFiles(project, [commonFile1.path]); + checkProjectRootFiles(project, [commonFile1.path]); // re-add commonFile2 host.reloadFS([commonFile1, commonFile2, configFile]); host.triggerDirectoryWatcherCallback("/a/b", commonFile2.path); host.runQueuedTimeoutCallbacks(); - checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); }); it("should create new inferred projects for files excluded from a configured project", () => { @@ -494,12 +512,12 @@ namespace ts { }` }; const files = [commonFile1, commonFile2, configFile]; - const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", files); - const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); + const host = createServerHost({ fileOrFolderList: files, libFile }); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(commonFile1.path); const project = projectService.configuredProjects[0]; - checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); configFile.content = `{ "compilerOptions": {}, "files": ["${commonFile1.path}"] @@ -508,7 +526,7 @@ namespace ts { host.triggerFileWatcherCallback(configFile.path); checkNumberOfConfiguredProjects(projectService, 1); - checkConfiguredProjectRootFiles(project, [commonFile1.path]); + checkProjectRootFiles(project, [commonFile1.path]); projectService.openClientFile(commonFile2.path); checkNumberOfInferredProjects(projectService, 1); @@ -527,13 +545,13 @@ namespace ts { content: `let t = 1;` }; - const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, commonFile2, excludedFile1, configFile]); - const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); + const host = createServerHost({ fileOrFolderList: [commonFile1, commonFile2, excludedFile1, configFile], libFile }); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(commonFile1.path); checkNumberOfConfiguredProjects(projectService, 1); const project = projectService.configuredProjects[0]; - checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); projectService.openClientFile(excludedFile1.path); checkNumberOfInferredProjects(projectService, 1); }); @@ -561,15 +579,15 @@ namespace ts { }` }; const files = [file1, nodeModuleFile, classicModuleFile, configFile]; - const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", files); - const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); + const host = createServerHost({ fileOrFolderList: files, libFile }); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(file1.path); projectService.openClientFile(nodeModuleFile.path); projectService.openClientFile(classicModuleFile.path); checkNumberOfConfiguredProjects(projectService, 1); const project = projectService.configuredProjects[0]; - checkConfiguredProjectActualFiles(project, [file1.path, nodeModuleFile.path]); + checkProjectActualFiles(project, [file1.path, nodeModuleFile.path]); checkNumberOfInferredProjects(projectService, 1); configFile.content = `{ @@ -580,7 +598,7 @@ namespace ts { }`; host.reloadFS(files); host.triggerFileWatcherCallback(configFile.path); - checkConfiguredProjectActualFiles(project, [file1.path, classicModuleFile.path]); + checkProjectActualFiles(project, [file1.path, classicModuleFile.path]); checkNumberOfInferredProjects(projectService, 1); }); @@ -602,8 +620,8 @@ namespace ts { "files": [ "main.ts" ] }` }; - const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [file1, file2, configFile]); - const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); + const host = createServerHost({ fileOrFolderList: [file1, file2, configFile], libFile }); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(file1.path); projectService.closeClientFile(file1.path); projectService.openClientFile(file2.path); @@ -629,13 +647,56 @@ namespace ts { "files": [ "main.ts" ] }` }; - const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [file1, file2, configFile]); - const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken); + const host = createServerHost({ fileOrFolderList: [file1, file2, configFile], libFile }); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(file1.path); projectService.closeClientFile(file1.path); projectService.openClientFile(file2.path); checkNumberOfConfiguredProjects(projectService, 1); checkNumberOfInferredProjects(projectService, 0); }); + + it("should use only one inferred project if 'useOneInferredProject' is set", () => { + const file1 = { + path: "/a/b/main.ts", + content: "let x =1;" + }; + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6" + }, + "files": [ "main.ts" ] + }` + }; + const file2 = { + path: "/a/c/main.ts", + content: "let x =1;" + }; + + const file3 = { + path: "/a/d/main.ts", + content: "let x =1;" + }; + + const host = createServerHost({ fileOrFolderList: [file1, file2, file3, libFile], libFile }); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ true); + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + projectService.openClientFile(file3.path); + + checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 1); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path, libFile.path]); + + + host.reloadFS([file1, configFile, file2, file3, libFile]); + host.triggerDirectoryWatcherCallback(getDirectoryPath(configFile.path), configFile.path); + + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 1); + checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, libFile.path]); + }); }); } \ No newline at end of file From 24a6e50195d3befa2b80d0a6df04009be5cd53bc Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 27 Jun 2016 15:29:04 -0700 Subject: [PATCH 069/307] added tests --- src/server/editorServices.ts | 16 ++++++----- src/server/lsHost.ts | 4 +-- src/server/project.ts | 27 ++++++++++++++----- .../cases/unittests/tsserverProjectSystem.ts | 23 ++++++++++++++++ 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 857d2c95dc5..5dd6865f568 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -167,7 +167,7 @@ namespace ts.server { return undefined; } if (isInferredProjectName(projectName)) { - return forEach(this.inferredProjects, p => p.getProjectName() === projectName && p); + return findProjectByName(projectName, this.inferredProjects); } return this.findExternalProjectByProjectName(projectName) || this.findConfiguredProjectByProjectName(toNormalizedPath(projectName)); } @@ -209,17 +209,19 @@ namespace ts.server { info.detachAllProjects(); + if (!this.eventHandler) { + return; + } + for (const openFile of this.openFileRoots) { - if (this.eventHandler) { - this.eventHandler("context", openFile.getDefaultProject(), openFile.fileName); - } + this.eventHandler("context", openFile.getDefaultProject(), openFile.fileName); } for (const openFile of this.openFilesReferenced) { - if (this.eventHandler) { - this.eventHandler("context", openFile.getDefaultProject(), openFile.fileName); - } + this.eventHandler("context", openFile.getDefaultProject(), openFile.fileName); } + + // TODO: project system view is inconsistent } this.printProjects(); diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index b65c71d5e28..36bdc7a9afe 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -89,8 +89,8 @@ namespace ts.server { } getDefaultLibFileName() { - const nodeModuleBinDir = ts.getDirectoryPath(ts.normalizePath(this.host.getExecutingFilePath())); - return ts.combinePaths(nodeModuleBinDir, ts.getDefaultLibFileName(this.compilationSettings)); + const nodeModuleBinDir = getDirectoryPath(normalizePath(this.host.getExecutingFilePath())); + return combinePaths(nodeModuleBinDir, getDefaultLibFileName(this.compilationSettings)); } getScriptSnapshot(filename: string): ts.IScriptSnapshot { diff --git a/src/server/project.ts b/src/server/project.ts index 00eeda9d8f0..40bd98c980b 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -19,8 +19,8 @@ namespace ts.server { } export abstract class Project { - private readonly rootFiles: ScriptInfo[] = []; - private readonly rootFilesMap: FileMap = createFileMap(); + private rootFiles: ScriptInfo[] = []; + private rootFilesMap: FileMap = createFileMap(); private lsHost: ServerLanguageServiceHost; private program: ts.Program; @@ -96,11 +96,24 @@ namespace ts.server { abstract getProjectName(): string; close() { - for (const fileName of this.getFileNames()) { - const info = this.projectService.getScriptInfoForNormalizedPath(fileName); - info.detachFromProject(this); + if (this.program) { + // if we have a program - release all files that are enlisted in program + for (const f of this.program.getSourceFiles()) { + const info = this.projectService.getScriptInfo(f.fileName); + info.detachFromProject(this); + } } - // signal language service to release files acquired from document registry + else { + // release all root files + for (const root of this.rootFiles) { + root.detachFromProject(this); + } + } + this.rootFiles = undefined; + this.rootFilesMap = undefined; + this.program = undefined; + + // signal language service to release source files acquired from document registry this.languageService.dispose(); } @@ -137,7 +150,7 @@ namespace ts.server { } containsScriptInfo(info: ScriptInfo): boolean { - return this.program && this.program.getSourceFileByPath(info.path) !== undefined; + return this.isRoot(info) || (this.program && this.program.getSourceFileByPath(info.path) !== undefined); } containsFile(filename: NormalizedPath, requireOpen?: boolean) { diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index 9e003bc8049..bcdcdc09106 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -698,5 +698,28 @@ namespace ts { checkNumberOfInferredProjects(projectService, 1); checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, libFile.path]); }); + + it ("should close configured project after closing last open file", () => { + const file1 = { + path: "/a/b/main.ts", + content: "let x =1;" + }; + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6" + }, + "files": [ "main.ts" ] + }` + }; + const host = createServerHost({ fileOrFolderList: [file1, configFile, libFile], libFile }); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ true); + projectService.openClientFile(file1.path); + checkNumberOfConfiguredProjects(projectService, 1); + + projectService.closeClientFile(file1.path); + checkNumberOfConfiguredProjects(projectService, 0); + }) }); } \ No newline at end of file From 05801fc5db1bc4ec3b87c17b9921a0e1698abc90 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 27 Jun 2016 15:36:41 -0700 Subject: [PATCH 070/307] fix linter --- src/server/editorServices.ts | 2 +- tests/cases/unittests/tsserverProjectSystem.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 5dd6865f568..5c91bd61eab 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -220,7 +220,7 @@ namespace ts.server { for (const openFile of this.openFilesReferenced) { this.eventHandler("context", openFile.getDefaultProject(), openFile.fileName); } - + // TODO: project system view is inconsistent } diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index bcdcdc09106..0abd376ee5d 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -720,6 +720,6 @@ namespace ts { projectService.closeClientFile(file1.path); checkNumberOfConfiguredProjects(projectService, 0); - }) + }); }); } \ No newline at end of file From 9bec244469b6fb8b3f2dd65ae76cc01584d2ef28 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 27 Jun 2016 16:07:37 -0700 Subject: [PATCH 071/307] remove commented code, added optionality to properties --- src/server/editorServices.ts | 65 +++++++++++++++--------------------- src/server/protocol.d.ts | 6 ++-- 2 files changed, 30 insertions(+), 41 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 5c91bd61eab..43191dd4af3 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -304,26 +304,7 @@ namespace ts.server { return normalizePath(name); } - // TODO: delete if unused - // private releaseNonReferencedConfiguredProjects() { - // if (this.configuredProjects.every(p => p.openRefCount > 0)) { - // return; - // } - - // const configuredProjects: ConfiguredProject[] = []; - // for (const proj of this.configuredProjects) { - // if (proj.openRefCount > 0) { - // configuredProjects.push(proj); - // } - // else { - // proj.close(); - // } - // } - - // this.configuredProjects = configuredProjects; - // } - - private removeProject(project: Project) { + private removeProject(project: Project) { this.log(`remove project: ${project.getRootFiles().toString()}`); project.close(); @@ -430,7 +411,7 @@ namespace ts.server { // collect all projects that should be removed let projectsToRemove: Project[]; for (const p of info.containingProjects) { - if ( p.projectKind === ProjectKind.Configured) { + if (p.projectKind === ProjectKind.Configured) { // last open file in configured project - close it if ((p).deleteOpenRef() === 0) { (projectsToRemove || (projectsToRemove = [])).push(p); @@ -761,7 +742,7 @@ namespace ts.server { if (contains(this.openFilesReferenced, info)) { removeItemFromSet(this.openFilesReferenced, info); } - if (project.projectKind === ProjectKind.Configured) { + if (project.projectKind === ProjectKind.Configured) { this.openFileRootsConfigured.push(info); } } @@ -1115,27 +1096,35 @@ namespace ts.server { } applyChangesInOpenFiles(openFiles: protocol.NewOpenFile[], changedFiles: protocol.ChangedOpenFile[], closedFiles: string[]): void { - for (const file of openFiles) { - const scriptInfo = this.getScriptInfo(file.fileName); - Debug.assert(!scriptInfo || !scriptInfo.isOpen); - this.openClientFileWithNormalizedPath(toNormalizedPath(file.fileName), file.content); - } - - for (const file of changedFiles) { - const scriptInfo = this.getScriptInfo(file.fileName); - Debug.assert(!!scriptInfo); - // apply changes in reverse order - for (let i = file.changes.length - 1; i >= 0; i--) { - const change = file.changes[i]; - scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); + if (openFiles) { + for (const file of openFiles) { + const scriptInfo = this.getScriptInfo(file.fileName); + Debug.assert(!scriptInfo || !scriptInfo.isOpen); + this.openClientFileWithNormalizedPath(toNormalizedPath(file.fileName), file.content); } } - for (const file of closedFiles) { - this.closeClientFile(file); + if (changedFiles) { + for (const file of changedFiles) { + const scriptInfo = this.getScriptInfo(file.fileName); + Debug.assert(!!scriptInfo); + // apply changes in reverse order + for (let i = file.changes.length - 1; i >= 0; i--) { + const change = file.changes[i]; + scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); + } + } } - this.updateProjectStructure(); + if (closedFiles) { + for (const file of closedFiles) { + this.closeClientFile(file); + } + } + + if (openFiles || changedFiles || closedFiles) { + this.updateProjectStructure(); + } } closeExternalProject(uncheckedFileName: string): void { diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 2c6779dc419..1dc1cb59609 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -691,9 +691,9 @@ declare namespace ts.server.protocol { } export interface ApplyChangedToOpenFilesRequestArgs { - openFiles: NewOpenFile[]; - changedFiles: ChangedOpenFile[]; - closedFiles: string[]; + openFiles?: NewOpenFile[]; + changedFiles?: ChangedOpenFile[]; + closedFiles?: string[]; } /** From d7bf32270edabfa7e5d7f418d570f26359e804aa Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 28 Jun 2016 17:45:29 -0700 Subject: [PATCH 072/307] remove multiple collections for open files --- src/server/editorServices.ts | 410 +++++++++--------- src/server/project.ts | 25 +- src/server/scriptInfo.ts | 30 +- src/server/session.ts | 2 +- .../cases/unittests/cachingInServerLSHost.ts | 2 +- 5 files changed, 256 insertions(+), 213 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 43191dd4af3..8e4515534d3 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -54,6 +54,20 @@ namespace ts.server { } } + /** + * TODO: enforce invariants: + * - script info can be never migrate to state - root file in inferred project, this is only a starting point + * - if script info has more that one containing projects - it is not a root file in inferred project because: + * - references in inferred project supercede the root part + * - root/reference in non-inferred project beats root in inferred project + */ + function isRootFileInInferredProject(info: ScriptInfo): boolean { + if (info.containingProjects.length === 0) { + return false; + } + return info.containingProjects[0].projectKind === ProjectKind.Inferred && info.containingProjects[0].isRoot(info); + } + class DirectoryWatchers { /** * a path to directory watcher map that detects added tsconfig files @@ -121,19 +135,11 @@ namespace ts.server { /** * projects specified by a tsconfig.json file **/ - configuredProjects: ConfiguredProject[] = []; - /** - * open, non-configured root files - **/ - openFileRoots: ScriptInfo[] = []; + readonly configuredProjects: ConfiguredProject[] = []; /** - * open files referenced by some project - **/ - openFilesReferenced: ScriptInfo[] = []; - /** - * open files that are roots of a configured project - **/ - openFileRootsConfigured: ScriptInfo[] = []; + * list of open files + */ + openFiles: ScriptInfo[] = []; private readonly directoryWatchers: DirectoryWatchers; @@ -213,11 +219,7 @@ namespace ts.server { return; } - for (const openFile of this.openFileRoots) { - this.eventHandler("context", openFile.getDefaultProject(), openFile.fileName); - } - - for (const openFile of this.openFilesReferenced) { + for (const openFile of this.openFiles) { this.eventHandler("context", openFile.getDefaultProject(), openFile.fileName); } @@ -266,14 +268,14 @@ namespace ts.server { // Call updateProjectStructure to clean up inferred projects we may have // created for the new files - this.updateProjectStructure(); + this.refreshInferredProjects(); } } private onConfigChangedForConfiguredProject(project: ConfiguredProject) { this.log(`Config file changed: ${project.configFileName}`); this.updateConfiguredProject(project); - this.updateProjectStructure(); + this.refreshInferredProjects(); } /** @@ -287,16 +289,8 @@ namespace ts.server { } this.log(`Detected newly added tsconfig file: ${fileName}`); - const { projectOptions } = this.convertConfigFileContentToProjectOptions(fileName); - const rootFilesInTsconfig = projectOptions.files.map(f => this.getCanonicalFileName(f)); - // We should only care about the new tsconfig file if it contains any - // opened root files of existing inferred projects - for (const rootFile of this.openFileRoots) { - if (contains(rootFilesInTsconfig, this.getCanonicalFileName(rootFile.fileName))) { - this.reloadProjects(); - break; - } - } + // TODO: add tests to check correct migration of currently open file if it is referenced from the root file of configured project + this.reloadProjects(); } private getCanonicalFileName(fileName: string) { @@ -340,59 +334,50 @@ namespace ts.server { return undefined; } - private addOpenFile(info: ScriptInfo): void { + private assignScriptInfoToInferredProjectIfNecessary(info: ScriptInfo, addToListOfOpenFiles: boolean): void { const externalProject = this.findContainingExternalProject(info.fileName); if (externalProject) { // file is already included in some external project - do nothing + if (addToListOfOpenFiles) { + this.openFiles.push(info); + } return; } const configuredProject = this.findContainingConfiguredProject(info); if (configuredProject) { // file is the part of configured project - configuredProject.addOpenRef(); - if (configuredProject.isRoot(info)) { - this.openFileRootsConfigured.push(info); - } - else { - this.openFilesReferenced.push(info); + if (addToListOfOpenFiles) { + configuredProject.addOpenRef(); + this.openFiles.push(info); } return; } + if (info.containingProjects.length === 0) { // create new inferred project p with the newly opened file as root // or add root to existing inferred project if 'useOneInferredProject' is true - const inferredProject = this.addFileToInferredProject(info); + const inferredProject = this.createInferredProjectWithRootFileIfNecessary(info); if (!this.useOneInferredProject) { // if useOneInferredProject is not set then try to fixup ownership of open files - const openFileRoots: ScriptInfo[] = []; - // for each inferred project root r - for (const rootFile of this.openFileRoots) { - // if r referenced by the new project - if (inferredProject.containsScriptInfo(rootFile)) { - // remove inferred project that was initially created for rootFile - const defaultProject = rootFile.getDefaultProject(); - if (defaultProject === inferredProject) { - continue; - } - Debug.assert(defaultProject.projectKind === ProjectKind.Inferred); + for (const f of this.openFiles) { + const defaultProject = f.getDefaultProject(); + if (isRootFileInInferredProject(info) && defaultProject !== inferredProject && inferredProject.containsScriptInfo(f)) { + // open file used to be root in inferred project, + // this inferred project is different from the one we've just created for current file + // and new inferred project references this open file. + // We should delete old inferred project and attach open file to the new one this.removeProject(defaultProject); - // put r in referenced open file list - this.openFilesReferenced.push(rootFile); - // set default project of r to the new project - rootFile.attachToProject(inferredProject); - } - else { - // otherwise, keep r as root of inferred project - openFileRoots.push(rootFile); + f.attachToProject(inferredProject); } } - this.openFileRoots = openFileRoots; } } - this.openFileRoots.push(info); + if (addToListOfOpenFiles) { + this.openFiles.push(info); + } } /** @@ -405,8 +390,8 @@ namespace ts.server { // to the disk, and the server's version of the file can be out of sync. info.reloadFromFile(); - removeItemFromSet(this.openFileRoots, info); - removeItemFromSet(this.openFileRootsConfigured, info); + removeItemFromSet(this.openFiles, info); + info.isOpen = false; // collect all projects that should be removed let projectsToRemove: Project[]; @@ -427,39 +412,22 @@ namespace ts.server { this.removeProject(project); } - const openFilesReferenced: ScriptInfo[] = []; - const orphanFiles: ScriptInfo[] = []; - // for all open, referenced files f - for (const f of this.openFilesReferenced) { - if (f === info) { - // skip closed file - continue; - } + let orphanFiles: ScriptInfo[]; + // for all open files + for (const f of this.openFiles) { // collect orphanted files and try to re-add them as newly opened if (f.containingProjects.length === 0) { - orphanFiles.push(f); - } - else { - // otherwise add it back to the list of referenced files - openFilesReferenced.push(f); + (orphanFiles || (orphanFiles = [])).push(f); } } - this.openFilesReferenced = openFilesReferenced; // treat orphaned files as newly opened - for (const f of orphanFiles) { - this.addOpenFile(f); + if (orphanFiles) { + for (const f of orphanFiles) { + this.assignScriptInfoToInferredProjectIfNecessary(f, /*addToListOfOpenFiles*/ false); + } } } - else { - // just close file - removeItemFromSet(this.openFilesReferenced, info); - } - - // projectsToRemove should already cover it - // this.releaseNonReferencedConfiguredProjects(); - - info.isOpen = false; } /** @@ -539,19 +507,10 @@ namespace ts.server { counter = printProjects(this.logger, this.configuredProjects, counter); counter = printProjects(this.logger, this.inferredProjects, counter); - this.logger.info("Open file roots of inferred projects: "); - for (const rootFile of this.openFileRoots) { + this.logger.info("Open files: "); + for (const rootFile of this.openFiles) { this.logger.info(rootFile.fileName); } - this.logger.info("Open files referenced by inferred or configured projects: "); - for (const referencedFile of this.openFilesReferenced) { - const fileInfo = `${referencedFile.fileName} ${ProjectKind[referencedFile.getDefaultProject().projectKind]}`; - this.logger.info(fileInfo); - } - this.logger.info("Open file roots of configured projects: "); - for (const configuredRoot of this.openFileRootsConfigured) { - this.logger.info(configuredRoot.fileName); - } this.logger.endGroup(); @@ -718,33 +677,44 @@ namespace ts.server { // if the root file was opened by client, it would belong to either // openFileRoots or openFileReferenced. if (info.isOpen) { - if (contains(this.openFileRoots, info)) { - removeItemFromSet(this.openFileRoots, info); - - // delete inferred project - let toRemove: Project[]; - - // TODO: unify logic - for (const p of info.containingProjects) { - if (p.projectKind === ProjectKind.Inferred && p.isRoot(info)) { - (toRemove || (toRemove = [])).push(p); - } - } - if (toRemove) { - for (const p of toRemove) { - p.removeFile(info); - if (!p.hasRoots()) { - this.removeProject(p); - } - } + // delete inferred project + let toRemove: Project; + if (isRootFileInInferredProject(info)) { + toRemove = info.containingProjects[0]; + } + if (toRemove) { + toRemove.removeFile(info); + if (!toRemove.hasRoots()) { + this.removeProject(toRemove); } } - if (contains(this.openFilesReferenced, info)) { - removeItemFromSet(this.openFilesReferenced, info); - } - if (project.projectKind === ProjectKind.Configured) { - this.openFileRootsConfigured.push(info); - } + // if (contains(this.openFileRoots, info)) { + // removeItemFromSet(this.openFileRoots, info); + + // // delete inferred project + // let toRemove: Project[]; + + // // TODO: unify logic + // for (const p of info.containingProjects) { + // if (p.projectKind === ProjectKind.Inferred && p.isRoot(info)) { + // (toRemove || (toRemove = [])).push(p); + // } + // } + // if (toRemove) { + // for (const p of toRemove) { + // p.removeFile(info); + // if (!p.hasRoots()) { + // this.removeProject(p); + // } + // } + // } + // } + // if (contains(this.openFilesReferenced, info)) { + // removeItemFromSet(this.openFilesReferenced, info); + // } + // if (project.projectKind === ProjectKind.Configured) { + // this.openFileRootsConfigured.push(info); + // } } } project.addRoot(info); @@ -784,7 +754,7 @@ namespace ts.server { } } - addFileToInferredProject(root: ScriptInfo) { + createInferredProjectWithRootFileIfNecessary(root: ScriptInfo) { const useExistingProject = this.useOneInferredProject && this.inferredProjects.length; const project = useExistingProject ? this.inferredProjects[0] @@ -887,11 +857,11 @@ namespace ts.server { */ reloadProjects() { this.log("reload projects."); - // First check if there is new tsconfig file added for inferred project roots - for (const info of this.openFileRoots) { + // try to reload config file for all open files + for (const info of this.openFiles) { this.openOrUpdateConfiguredProjectForFile(info.fileName); } - this.updateProjectStructure(); + this.refreshInferredProjects(); } /** @@ -899,20 +869,60 @@ namespace ts.server { * It is called on the premise that all the configured projects are * up to date. */ - updateProjectStructure() { + refreshInferredProjects() { this.log("updating project structure from ...", "Info"); this.printProjects(); const unattachedOpenFiles: ScriptInfo[] = []; - const openFileRootsConfigured: ScriptInfo[] = []; - // collect all orphanted script infos that used to be roots of configured projects - for (const info of this.openFileRootsConfigured) { + // collect all orphanted script infos from open files + for (const info of this.openFiles) { if (info.containingProjects.length === 0) { unattachedOpenFiles.push(info); } else { - openFileRootsConfigured.push(info); + if (isRootFileInInferredProject(info) && info.containingProjects.length > 1) { + const inferredProject = info.containingProjects[0]; + Debug.assert(inferredProject.projectKind === ProjectKind.Inferred); + inferredProject.removeFile(info); + if (!inferredProject.hasRoots()) { + this.removeProject(inferredProject); + } + } + // let inConfiguredProject = false; + // let inExternalProject = false; + // for (const p of info.containingProjects) { + // inConfiguredProject = inConfiguredProject || p.projectKind === ProjectKind.Configured; + // inExternalProject = inExternalProject || p.projectKind === ProjectKind.External; + // } + // if (inConfiguredProject || inExternalProject) { + // const inferredProjects = rootFile.containingProjects.filter(p => p.projectKind === ProjectKind.Inferred); + // for (const p of inferredProjects) { + // p.removeFile(rootFile, /*detachFromProject*/ true); + // if (!p.hasRoots()) { + // this.removeProject(p); + // } + // } + // } + // else { + // if (rootFile.containingProjects.length > 1) { + // // TODO: fixme + // // file is contained in more than one inferred project - keep only ones where it is used as reference + // const roots = rootFile.containingProjects.filter(p => p.isRoot(rootFile)); + // for (const root of roots) { + // this.removeProject(root); + // } + // Debug.assert(info.containingProjects.length > 0); + // } + // } + } + + // if (info.containingProjects.length === 0) { + // unattachedOpenFiles.push(info); + // } + // else { + // openFileRootsConfigured.push(info); + // } // const project = info.defaultProject; // if (!project || !(project.containsScriptInfo(info))) { // info.defaultProject = undefined; @@ -922,30 +932,34 @@ namespace ts.server { // openFileRootsConfigured.push(info); // } } - this.openFileRootsConfigured = openFileRootsConfigured; - - // First loop through all open files that are referenced by projects but are not - // project roots. For each referenced file, see if the default project still - // references that file. If so, then just keep the file in the referenced list. - // If not, add the file to an unattached list, to be rechecked later. - const openFilesReferenced: ScriptInfo[] = []; - for (const referencedFile of this.openFilesReferenced) { - // check if any of projects that used to reference this file are still referencing it - if (referencedFile.containingProjects.length === 0) { - unattachedOpenFiles.push(referencedFile); - } - else { - openFilesReferenced.push(referencedFile); - } - // referencedFile.defaultProject.updateGraph(); - // if (referencedFile.defaultProject.containsScriptInfo(referencedFile)) { - // openFilesReferenced.push(referencedFile); - // } - // else { - // unattachedOpenFiles.push(referencedFile); - // } + for (const unattached of unattachedOpenFiles) { + this.assignScriptInfoToInferredProjectIfNecessary(unattached, /*addToListOfOpenFiles*/ false); } - this.openFilesReferenced = openFilesReferenced; + + // this.openFileRootsConfigured = openFileRootsConfigured; + + // // First loop through all open files that are referenced by projects but are not + // // project roots. For each referenced file, see if the default project still + // // references that file. If so, then just keep the file in the referenced list. + // // If not, add the file to an unattached list, to be rechecked later. + // const openFilesReferenced: ScriptInfo[] = []; + // for (const referencedFile of this.openFilesReferenced) { + // // check if any of projects that used to reference this file are still referencing it + // if (referencedFile.containingProjects.length === 0) { + // unattachedOpenFiles.push(referencedFile); + // } + // else { + // openFilesReferenced.push(referencedFile); + // } + // // referencedFile.defaultProject.updateGraph(); + // // if (referencedFile.defaultProject.containsScriptInfo(referencedFile)) { + // // openFilesReferenced.push(referencedFile); + // // } + // // else { + // // unattachedOpenFiles.push(referencedFile); + // // } + // } + // this.openFilesReferenced = openFilesReferenced; // Then, loop through all of the open files that are project roots. // For each root file, note the project that it roots. Then see if @@ -954,42 +968,42 @@ namespace ts.server { // projects newly references the file, remove its project from the // inferred projects list (since it is no longer a root) and add // the file to the open, referenced file list. - const openFileRoots: ScriptInfo[] = []; - for (const rootFile of this.openFileRoots) { - let inConfiguredProject = false; - let inExternalProject = false; - for (const p of rootFile.containingProjects) { - inConfiguredProject = inConfiguredProject || p.projectKind === ProjectKind.Configured; - inExternalProject = inExternalProject || p.projectKind === ProjectKind.External; - } - if (inConfiguredProject || inExternalProject) { - const inferredProjects = rootFile.containingProjects.filter(p => p.projectKind === ProjectKind.Inferred); - for (const p of inferredProjects) { - p.removeFile(rootFile, /*detachFromProject*/ true); - if (!p.hasRoots()) { - this.removeProject(p); - } - } - if (inConfiguredProject) { - this.openFileRootsConfigured.push(rootFile); - } - } - else { - if (rootFile.containingProjects.length === 1) { - // file contained only in one project - openFileRoots.push(rootFile); - } - else { - // TODO: fixme - // file is contained in more than one inferred project - keep only ones where it is used as reference - const roots = rootFile.containingProjects.filter(p => p.isRoot(rootFile)); - for (const root of roots) { - this.removeProject(root); - } - Debug.assert(rootFile.containingProjects.length > 0); - this.openFilesReferenced.push(rootFile); - } - } + // const openFileRoots: ScriptInfo[] = []; + // for (const rootFile of this.openFileRoots) { + // let inConfiguredProject = false; + // let inExternalProject = false; + // for (const p of rootFile.containingProjects) { + // inConfiguredProject = inConfiguredProject || p.projectKind === ProjectKind.Configured; + // inExternalProject = inExternalProject || p.projectKind === ProjectKind.External; + // } + // if (inConfiguredProject || inExternalProject) { + // const inferredProjects = rootFile.containingProjects.filter(p => p.projectKind === ProjectKind.Inferred); + // for (const p of inferredProjects) { + // p.removeFile(rootFile, /*detachFromProject*/ true); + // if (!p.hasRoots()) { + // this.removeProject(p); + // } + // } + // if (inConfiguredProject) { + // this.openFileRootsConfigured.push(rootFile); + // } + // } + // else { + // if (rootFile.containingProjects.length === 1) { + // // file contained only in one project + // openFileRoots.push(rootFile); + // } + // else { + // // TODO: fixme + // // file is contained in more than one inferred project - keep only ones where it is used as reference + // const roots = rootFile.containingProjects.filter(p => p.isRoot(rootFile)); + // for (const root of roots) { + // this.removeProject(root); + // } + // Debug.assert(rootFile.containingProjects.length > 0); + // this.openFilesReferenced.push(rootFile); + // } + // } // if (rootFile.containingProjects.some(p => p.projectKind !== ProjectKind.Inferred)) { // // file was included in non-inferred project - drop old inferred project @@ -1026,15 +1040,15 @@ namespace ts.server { // this.openFilesReferenced.push(rootFile); // } // } - } - this.openFileRoots = openFileRoots; + // } + // this.openFileRoots = openFileRoots; // Finally, if we found any open, referenced files that are no longer // referenced by their default project, treat them as newly opened // by the editor. - for (const f of unattachedOpenFiles) { - this.addOpenFile(f); - } + // for (const f of unattachedOpenFiles) { + // this.addOpenFile(f); + // } for (const p of this.inferredProjects) { p.updateGraph(); } @@ -1057,7 +1071,7 @@ namespace ts.server { // at this point if file is the part of some configured/external project then this project should be created const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind); - this.addOpenFile(info); + this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ true); this.printProjects(); return { configFileName, configFileErrors }; } @@ -1123,7 +1137,7 @@ namespace ts.server { } if (openFiles || changedFiles || closedFiles) { - this.updateProjectStructure(); + this.refreshInferredProjects(); } } @@ -1137,14 +1151,14 @@ namespace ts.server { this.removeProject(configuredProject); } } - this.updateProjectStructure(); + this.refreshInferredProjects(); } else { // close external project const externalProject = this.findExternalProjectByProjectName(uncheckedFileName); if (externalProject) { this.removeProject(externalProject); - this.updateProjectStructure(); + this.refreshInferredProjects(); } } } diff --git a/src/server/project.ts b/src/server/project.ts index 40bd98c980b..1d11a2ed984 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -122,11 +122,11 @@ namespace ts.server { } hasRoots() { - return this.rootFiles.length > 0; + return this.rootFiles && this.rootFiles.length > 0; } getRootFiles() { - return this.rootFiles.map(info => info.fileName); + return this.rootFiles && this.rootFiles.map(info => info.fileName); } getFileNames() { @@ -161,7 +161,7 @@ namespace ts.server { } isRoot(info: ScriptInfo) { - return this.rootFilesMap.contains(info.path); + return this.rootFilesMap && this.rootFilesMap.contains(info.path); } // add a root file to project @@ -190,14 +190,15 @@ namespace ts.server { this.projectStateVersion++; } - updateGraph() { + updateGraph(): boolean { if (!this.languageServiceEnabled) { - return; + return true; } const oldProgram = this.program; this.program = this.languageService.getProgram(); + const oldProjectStructureVersion = this.projectStructureVersion; // bump up the version if // - oldProgram is not set - this is a first time updateGraph is called // - newProgram is different from the old program and structure of the old program was not reused. @@ -205,16 +206,18 @@ namespace ts.server { this.projectStructureVersion++; if (oldProgram) { for (const f of oldProgram.getSourceFiles()) { - if (!this.program.getSourceFileByPath(f.path)) { - // new program does not contain this file - detach it from the project - const scriptInfoToDetach = this.projectService.getScriptInfo(f.fileName); - if (scriptInfoToDetach) { - scriptInfoToDetach.detachFromProject(this); - } + if (this.program.getSourceFileByPath(f.path)) { + continue; + } + // new program does not contain this file - detach it from the project + const scriptInfoToDetach = this.projectService.getScriptInfo(f.fileName); + if (scriptInfoToDetach) { + scriptInfoToDetach.detachFromProject(this); } } } } + return oldProjectStructureVersion === this.projectStructureVersion; } getScriptInfoLSHost(fileName: string) { diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index d5c01f609dd..9be0f2e590e 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -37,11 +37,37 @@ namespace ts.server { } isAttached(project: Project) { - return contains(this.containingProjects, project); + // unrolled for common cases + switch (this.containingProjects.length) { + case 0: return false; + case 1: return this.containingProjects[0] === project; + case 2: return this.containingProjects[0] === project || this.containingProjects[1] === project; + default: return contains(this.containingProjects, project); + } } detachFromProject(project: Project) { - removeItemFromSet(this.containingProjects, project); + // unrolled for common cases + switch (this.containingProjects.length) { + case 0: + return; + case 1: + if (this.containingProjects[0] === project) { + this.containingProjects.pop(); + } + break; + case 2: + if (this.containingProjects[0] === project) { + this.containingProjects[0] = this.containingProjects.pop(); + } + if (this.containingProjects[1] === project) { + this.containingProjects.pop(); + } + break; + default: + removeItemFromSet(this.containingProjects, project); + break; + } } detachAllProjects() { diff --git a/src/server/session.ts b/src/server/session.ts index dfe532805cd..c034e501403 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -307,7 +307,7 @@ namespace ts.server { private updateProjectStructure(seq: number, matchSeq: (seq: number) => boolean, ms = 1500) { this.host.setTimeout(() => { if (matchSeq(seq)) { - this.projectService.updateProjectStructure(); + this.projectService.refreshInferredProjects(); } }, ms); } diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index 0a96afa14f4..0d9590d5431 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -81,7 +81,7 @@ namespace ts { const projectService = new server.ProjectService(serverHost, logger, { isCancellationRequested: () => false }, /*useOneInferredProject*/ false); const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */true, /*containingProject*/ undefined); - const project = projectService.addFileToInferredProject(rootScriptInfo); + const project = projectService.createInferredProjectWithRootFileIfNecessary(rootScriptInfo); project.setCompilerOptions({ module: ts.ModuleKind.AMD } ); return { project, From 641c2ffd5e5f3f57b704757ee3371edb199a78c8 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 29 Jun 2016 12:23:25 -0700 Subject: [PATCH 073/307] introduce ThrottledOperations --- src/server/editorServices.ts | 91 ++++++++-------- src/server/server.ts | 8 +- src/server/session.ts | 4 +- src/server/utilities.ts | 103 +++++++++++------- .../cases/unittests/tsserverProjectSystem.ts | 24 ++-- 5 files changed, 126 insertions(+), 104 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index dd7fb3bbbcc..98cce0c3db4 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -108,7 +108,6 @@ namespace ts.server { currentPath = parentPath; parentPath = getDirectoryPath(parentPath); } - } } @@ -139,21 +138,21 @@ namespace ts.server { /** * list of open files */ - openFiles: ScriptInfo[] = []; + readonly openFiles: ScriptInfo[] = []; private readonly directoryWatchers: DirectoryWatchers; + private readonly throttledOperations: ThrottledOperations; private readonly hostConfiguration: HostConfiguration; - private timerForDetectingProjectFileListChanges: Map = {}; - constructor(public readonly host: ServerHost, public readonly logger: Logger, public readonly cancellationToken: HostCancellationToken, - private readonly useOneInferredProject: boolean, + private readonly useSingleInferredProject: boolean, private readonly eventHandler?: ProjectServiceEventHandler) { this.directoryWatchers = new DirectoryWatchers(this); + this.throttledOperations = new ThrottledOperations(host); // ts.disableIncrementalParsing = true; this.hostConfiguration = { @@ -243,14 +242,10 @@ namespace ts.server { } this.log(`Detected source file changes: ${fileName}`); - const timeoutId = this.timerForDetectingProjectFileListChanges[project.configFileName]; - if (timeoutId) { - this.host.clearTimeout(timeoutId); - } - this.timerForDetectingProjectFileListChanges[project.configFileName] = this.host.setTimeout( - () => this.handleChangeInSourceFileForConfiguredProject(project), - 250 - ); + this.throttledOperations.schedule( + project.configFileName, + 250, + () => this.handleChangeInSourceFileForConfiguredProject(project)); } private handleChangeInSourceFileForConfiguredProject(project: ConfiguredProject) { @@ -357,7 +352,7 @@ namespace ts.server { // create new inferred project p with the newly opened file as root // or add root to existing inferred project if 'useOneInferredProject' is true const inferredProject = this.createInferredProjectWithRootFileIfNecessary(info); - if (!this.useOneInferredProject) { + if (!this.useSingleInferredProject) { // if useOneInferredProject is not set then try to fixup ownership of open files for (const f of this.openFiles) { @@ -755,7 +750,7 @@ namespace ts.server { } createInferredProjectWithRootFileIfNecessary(root: ScriptInfo) { - const useExistingProject = this.useOneInferredProject && this.inferredProjects.length; + const useExistingProject = this.useSingleInferredProject && this.inferredProjects.length; const project = useExistingProject ? this.inferredProjects[0] : new InferredProject(this, this.documentRegistry, /*languageServiceEnabled*/ true); @@ -1004,42 +999,42 @@ namespace ts.server { // this.openFilesReferenced.push(rootFile); // } // } - // if (rootFile.containingProjects.some(p => p.projectKind !== ProjectKind.Inferred)) { - // // file was included in non-inferred project - drop old inferred project + // if (rootFile.containingProjects.some(p => p.projectKind !== ProjectKind.Inferred)) { + // // file was included in non-inferred project - drop old inferred project - // } - // else { - // openFileRoots.push(rootFile); - // } - // let inferredProjectsToRemove: Project[]; - // for (const p of rootFile.containingProjects) { - // if (p.projectKind !== ProjectKind.Inferred) { - // // file was included in non-inferred project - drop old inferred project - // } - // } + // } + // else { + // openFileRoots.push(rootFile); + // } + // let inferredProjectsToRemove: Project[]; + // for (const p of rootFile.containingProjects) { + // if (p.projectKind !== ProjectKind.Inferred) { + // // file was included in non-inferred project - drop old inferred project + // } + // } - // const rootedProject = rootFile.defaultProject; - // const referencingProjects = this.findReferencingProjects(rootFile, rootedProject); + // const rootedProject = rootFile.defaultProject; + // const referencingProjects = this.findReferencingProjects(rootFile, rootedProject); - // if (rootFile.defaultProject && rootFile.defaultProject.projectKind !== ProjectKind.Inferred) { - // // If the root file has already been added into a configured project, - // // meaning the original inferred project is gone already. - // if (rootedProject.projectKind === ProjectKind.Inferred) { - // this.removeProject(rootedProject); - // } - // this.openFileRootsConfigured.push(rootFile); - // } - // else { - // if (referencingProjects.length === 0) { - // rootFile.defaultProject = rootedProject; - // openFileRoots.push(rootFile); - // } - // else { - // // remove project from inferred projects list because root captured - // this.removeProject(rootedProject); - // this.openFilesReferenced.push(rootFile); - // } - // } + // if (rootFile.defaultProject && rootFile.defaultProject.projectKind !== ProjectKind.Inferred) { + // // If the root file has already been added into a configured project, + // // meaning the original inferred project is gone already. + // if (rootedProject.projectKind === ProjectKind.Inferred) { + // this.removeProject(rootedProject); + // } + // this.openFileRootsConfigured.push(rootFile); + // } + // else { + // if (referencingProjects.length === 0) { + // rootFile.defaultProject = rootedProject; + // openFileRoots.push(rootFile); + // } + // else { + // // remove project from inferred projects list because root captured + // this.removeProject(rootedProject); + // this.openFilesReferenced.push(rootFile); + // } + // } // } // this.openFileRoots = openFileRoots; diff --git a/src/server/server.ts b/src/server/server.ts index 79b40836f54..17d7e15c19d 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -91,8 +91,8 @@ namespace ts.server { } class IOSession extends Session { - constructor(host: ServerHost, cancellationToken: HostCancellationToken, useOneInferredProject: boolean, logger: ts.server.Logger) { - super(host, cancellationToken, useOneInferredProject, Buffer.byteLength, process.hrtime, logger); + constructor(host: ServerHost, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, logger: ts.server.Logger) { + super(host, cancellationToken, useSingleInferredProject, Buffer.byteLength, process.hrtime, logger); } exit() { @@ -304,8 +304,8 @@ namespace ts.server { }; }; - const useOneInferredProject = sys.args.some(arg => arg === "--useOneInferredProject"); - const ioSession = new IOSession(sys, cancellationToken, useOneInferredProject, logger); + const useSingleInferredProject = sys.args.some(arg => arg === "--useSingleInferredProject"); + const ioSession = new IOSession(sys, cancellationToken, useSingleInferredProject, logger); process.on("uncaughtException", function(err: Error) { ioSession.logError(err, "unknown"); }); diff --git a/src/server/session.ts b/src/server/session.ts index c034e501403..f3d439580f3 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -179,12 +179,12 @@ namespace ts.server { constructor( private host: ServerHost, cancellationToken: HostCancellationToken, - useOneInferredProject: boolean, + useSingleInferredProject: boolean, private byteLength: (buf: string, encoding?: string) => number, private hrtime: (start?: number[]) => number[], private logger: Logger) { this.projectService = - new ProjectService(host, logger, cancellationToken, useOneInferredProject, (eventName, project, fileName) => { + new ProjectService(host, logger, cancellationToken, useSingleInferredProject, (eventName, project, fileName) => { this.handleEvent(eventName, project, fileName); }); } diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 0dcc989342a..34953f9a15f 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -1,7 +1,5 @@ /// -/* tslint:disable:no-null-keyword */ - namespace ts.server { export interface Logger { close(): void; @@ -74,14 +72,16 @@ namespace ts.server { } export interface NormalizedPathMap { - get (path: NormalizedPath): T; - set (path: NormalizedPath, value: T): void; + get(path: NormalizedPath): T; + set(path: NormalizedPath, value: T): void; contains(path: NormalizedPath): boolean; remove(path: NormalizedPath): void; } export function createNormalizedPathMap(): NormalizedPathMap { +/* tslint:disable:no-null-keyword */ const map: Map = Object.create(null); +/* tslint:enable:no-null-keyword */ return { get(path) { return map[path]; @@ -97,48 +97,49 @@ namespace ts.server { } }; } - function throwLanguageServiceIsDisabledError() {; + function throwLanguageServiceIsDisabledError() { + ; throw new Error("LanguageService is disabled"); } export const nullLanguageService: LanguageService = { - cleanupSemanticCache: (): any => throwLanguageServiceIsDisabledError(), - getSyntacticDiagnostics: (): any => throwLanguageServiceIsDisabledError(), - getSemanticDiagnostics: (): any => throwLanguageServiceIsDisabledError(), - getCompilerOptionsDiagnostics: (): any => throwLanguageServiceIsDisabledError(), - getSyntacticClassifications: (): any => throwLanguageServiceIsDisabledError(), + cleanupSemanticCache: (): any => throwLanguageServiceIsDisabledError(), + getSyntacticDiagnostics: (): any => throwLanguageServiceIsDisabledError(), + getSemanticDiagnostics: (): any => throwLanguageServiceIsDisabledError(), + getCompilerOptionsDiagnostics: (): any => throwLanguageServiceIsDisabledError(), + getSyntacticClassifications: (): any => throwLanguageServiceIsDisabledError(), getEncodedSyntacticClassifications: (): any => throwLanguageServiceIsDisabledError(), - getSemanticClassifications: (): any => throwLanguageServiceIsDisabledError(), - getEncodedSemanticClassifications: (): any => throwLanguageServiceIsDisabledError(), - getCompletionsAtPosition: (): any => throwLanguageServiceIsDisabledError(), - findReferences: (): any => throwLanguageServiceIsDisabledError(), - getCompletionEntryDetails: (): any => throwLanguageServiceIsDisabledError(), - getQuickInfoAtPosition: (): any => throwLanguageServiceIsDisabledError(), - findRenameLocations: (): any => throwLanguageServiceIsDisabledError(), - getNameOrDottedNameSpan: (): any => throwLanguageServiceIsDisabledError(), - getBreakpointStatementAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getBraceMatchingAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getSignatureHelpItems: (): any => throwLanguageServiceIsDisabledError(), - getDefinitionAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getRenameInfo: (): any => throwLanguageServiceIsDisabledError(), - getTypeDefinitionAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getReferencesAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getDocumentHighlights: (): any => throwLanguageServiceIsDisabledError(), - getOccurrencesAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getNavigateToItems: (): any => throwLanguageServiceIsDisabledError(), - getNavigationBarItems: (): any => throwLanguageServiceIsDisabledError(), - getOutliningSpans: (): any => throwLanguageServiceIsDisabledError(), - getTodoComments: (): any => throwLanguageServiceIsDisabledError(), - getIndentationAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getFormattingEditsForRange: (): any => throwLanguageServiceIsDisabledError(), - getFormattingEditsForDocument: (): any => throwLanguageServiceIsDisabledError(), - getFormattingEditsAfterKeystroke: (): any => throwLanguageServiceIsDisabledError(), - getDocCommentTemplateAtPosition: (): any => throwLanguageServiceIsDisabledError(), - isValidBraceCompletionAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getEmitOutput: (): any => throwLanguageServiceIsDisabledError(), - getProgram: (): any => throwLanguageServiceIsDisabledError(), - getNonBoundSourceFile: (): any => throwLanguageServiceIsDisabledError(), - dispose: (): any => throwLanguageServiceIsDisabledError(), + getSemanticClassifications: (): any => throwLanguageServiceIsDisabledError(), + getEncodedSemanticClassifications: (): any => throwLanguageServiceIsDisabledError(), + getCompletionsAtPosition: (): any => throwLanguageServiceIsDisabledError(), + findReferences: (): any => throwLanguageServiceIsDisabledError(), + getCompletionEntryDetails: (): any => throwLanguageServiceIsDisabledError(), + getQuickInfoAtPosition: (): any => throwLanguageServiceIsDisabledError(), + findRenameLocations: (): any => throwLanguageServiceIsDisabledError(), + getNameOrDottedNameSpan: (): any => throwLanguageServiceIsDisabledError(), + getBreakpointStatementAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getBraceMatchingAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getSignatureHelpItems: (): any => throwLanguageServiceIsDisabledError(), + getDefinitionAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getRenameInfo: (): any => throwLanguageServiceIsDisabledError(), + getTypeDefinitionAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getReferencesAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getDocumentHighlights: (): any => throwLanguageServiceIsDisabledError(), + getOccurrencesAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getNavigateToItems: (): any => throwLanguageServiceIsDisabledError(), + getNavigationBarItems: (): any => throwLanguageServiceIsDisabledError(), + getOutliningSpans: (): any => throwLanguageServiceIsDisabledError(), + getTodoComments: (): any => throwLanguageServiceIsDisabledError(), + getIndentationAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getFormattingEditsForRange: (): any => throwLanguageServiceIsDisabledError(), + getFormattingEditsForDocument: (): any => throwLanguageServiceIsDisabledError(), + getFormattingEditsAfterKeystroke: (): any => throwLanguageServiceIsDisabledError(), + getDocCommentTemplateAtPosition: (): any => throwLanguageServiceIsDisabledError(), + isValidBraceCompletionAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getEmitOutput: (): any => throwLanguageServiceIsDisabledError(), + getProgram: (): any => throwLanguageServiceIsDisabledError(), + getNonBoundSourceFile: (): any => throwLanguageServiceIsDisabledError(), + dispose: (): any => throwLanguageServiceIsDisabledError(), }; export interface ServerLanguageServiceHost { @@ -172,4 +173,24 @@ namespace ts.server { export function makeInferredProjectName(counter: number) { return `/dev/null/inferredProject${counter}*`; } + + export class ThrottledOperations { + private pendingTimeouts: Map = {}; + constructor(private readonly host: ServerHost) { + } + + public schedule(operationId: string, delay: number, cb: () => void) { + if (hasProperty(this.pendingTimeouts, operationId)) { + // another operation was already scheduled for this id - cancel it + this.host.clearTimeout(this.pendingTimeouts[operationId]); + } + // schedule new operation, pass arguments + this.pendingTimeouts[operationId] = this.host.setTimeout(ThrottledOperations.run, delay, this, operationId, cb); + } + + private static run(self: ThrottledOperations, operationId: string, cb: () => void) { + delete self.pendingTimeouts[operationId]; + cb(); + } + } } \ No newline at end of file diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index 0abd376ee5d..fc6ef16e398 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -145,7 +145,10 @@ namespace ts { private fs: ts.FileMap; private getCanonicalFileName: (s: string) => string; private toPath: (f: string) => Path; - private callbackQueue: TimeOutCallback[] = []; + + private nextTimeoutId = 0; + private callbacks: { [n: number]: TimeOutCallback } = {}; + readonly watchedDirectories: Map<{ cb: DirectoryWatcherCallback, recursive: boolean }[]> = {}; readonly watchedFiles: Map = {}; @@ -283,25 +286,28 @@ namespace ts { } // TOOD: record and invoke callbacks to simulate timer events - readonly setTimeout = (callback: TimeOutCallback, time: number) => { - this.callbackQueue.push(callback); - return this.callbackQueue.length - 1; + readonly setTimeout = (callback: TimeOutCallback, time: number, ...args: any[]) => { + const timeoutId = this.nextTimeoutId; + this.nextTimeoutId++; + this.callbacks[timeoutId] = callback.bind(undefined, ...args); + return timeoutId; }; readonly clearTimeout = (timeoutId: any): void => { if (typeof timeoutId === "number") { - this.callbackQueue.splice(timeoutId, 1); + delete this.callbacks[timeoutId]; } }; checkTimeoutQueueLength(expected: number) { - assert.equal(this.callbackQueue.length, expected, `expected ${expected} timeout callbacks queued but found ${this.callbackQueue.length}.`); + const callbacksCount = sizeOfMap(this.callbacks); + assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`); } runQueuedTimeoutCallbacks() { - for (const callback of this.callbackQueue) { - callback(); + for (const id in this.callbacks) { + this.callbacks[id](); } - this.callbackQueue = []; + this.callbacks = []; } readonly readFile = (s: string) => (this.fs.get(this.toPath(s))).content; From 011b55fd5d536a9611c8d85096253f7f66626b81 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 29 Jun 2016 14:42:51 -0700 Subject: [PATCH 074/307] remove commented code --- src/server/editorServices.ts | 249 ++++++----------------------------- 1 file changed, 42 insertions(+), 207 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 98cce0c3db4..245fbf61304 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -11,7 +11,7 @@ namespace ts.server { export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; /** - * This helper funciton processes a list of projects and return the concatenated, sortd and deduplicated output of processing each project. + * This helper function processes a list of projects and return the concatenated, sortd and deduplicated output of processing each project. */ export function combineProjectOutput(projects: Project[], action: (project: Project) => T[], comparer?: (a: T, b: T) => number, areEqual?: (a: T, b: T) => boolean) { const result = projects.reduce((previous, current) => concatenate(previous, action(current)), []).sort(comparer); @@ -177,6 +177,24 @@ namespace ts.server { return this.findExternalProjectByProjectName(projectName) || this.findConfiguredProjectByProjectName(toNormalizedPath(projectName)); } + private findContainingConfiguredProject(info: ScriptInfo): ConfiguredProject { + for (const proj of this.configuredProjects) { + if (proj.containsScriptInfo(info)) { + return proj; + } + } + return undefined; + } + + private findContainingExternalProject(fileName: NormalizedPath): ExternalProject { + for (const proj of this.externalProjects) { + if (proj.containsFile(fileName)) { + return proj; + } + } + return undefined; + } + getFormatCodeOptions(file?: NormalizedPath) { if (file) { const info = this.getScriptInfoForNormalizedPath(file); @@ -187,6 +205,18 @@ namespace ts.server { return this.hostConfiguration.formatCodeOptions; } + private updateProjectGraphs(projects: Project[]) { + let shouldRefreshInferredProjects = false; + for (const p of projects) { + if (p.updateGraph()) { + shouldRefreshInferredProjects = true; + } + } + if (shouldRefreshInferredProjects) { + this.refreshInferredProjects(); + } + } + private onSourceFileChanged(fileName: NormalizedPath) { const info = this.getScriptInfoForNormalizedPath(fileName); if (!info) { @@ -199,7 +229,11 @@ namespace ts.server { } else { if (info && (!info.isOpen)) { + // file has been changed which might affect the set of referenced files in projects that include + // this file and set of inferred projects + // TODO: add tests info.reloadFromFile(); + this.updateProjectGraphs(info.containingProjects); } } } @@ -209,10 +243,16 @@ namespace ts.server { info.stopWatcher(); + // TODO: handle isOpen = true case + if (!info.isOpen) { this.filenameToScriptInfo.remove(info.fileName); + // capture list of projects + const containingProjects = info.containingProjects.slice(); info.detachAllProjects(); + // update projects to make sure that set of referenced files is correct + this.updateProjectGraphs(containingProjects); if (!this.eventHandler) { return; @@ -221,8 +261,6 @@ namespace ts.server { for (const openFile of this.openFiles) { this.eventHandler("context", openFile.getDefaultProject(), openFile.fileName); } - - // TODO: project system view is inconsistent } this.printProjects(); @@ -244,7 +282,7 @@ namespace ts.server { this.log(`Detected source file changes: ${fileName}`); this.throttledOperations.schedule( project.configFileName, - 250, + /*delay*/250, () => this.handleChangeInSourceFileForConfiguredProject(project)); } @@ -311,24 +349,6 @@ namespace ts.server { } } - private findContainingConfiguredProject(info: ScriptInfo): ConfiguredProject { - for (const proj of this.configuredProjects) { - if (proj.containsScriptInfo(info)) { - return proj; - } - } - return undefined; - } - - private findContainingExternalProject(fileName: NormalizedPath): ExternalProject { - for (const proj of this.externalProjects) { - if (proj.containsFile(fileName)) { - return proj; - } - } - return undefined; - } - private assignScriptInfoToInferredProjectIfNecessary(info: ScriptInfo, addToListOfOpenFiles: boolean): void { const externalProject = this.findContainingExternalProject(info.fileName); if (externalProject) { @@ -353,7 +373,6 @@ namespace ts.server { // or add root to existing inferred project if 'useOneInferredProject' is true const inferredProject = this.createInferredProjectWithRootFileIfNecessary(info); if (!this.useSingleInferredProject) { - // if useOneInferredProject is not set then try to fixup ownership of open files for (const f of this.openFiles) { const defaultProject = f.getDefaultProject(); @@ -669,8 +688,6 @@ namespace ts.server { info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false); } else { - // if the root file was opened by client, it would belong to either - // openFileRoots or openFileReferenced. if (info.isOpen) { // delete inferred project let toRemove: Project; @@ -683,33 +700,6 @@ namespace ts.server { this.removeProject(toRemove); } } - // if (contains(this.openFileRoots, info)) { - // removeItemFromSet(this.openFileRoots, info); - - // // delete inferred project - // let toRemove: Project[]; - - // // TODO: unify logic - // for (const p of info.containingProjects) { - // if (p.projectKind === ProjectKind.Inferred && p.isRoot(info)) { - // (toRemove || (toRemove = [])).push(p); - // } - // } - // if (toRemove) { - // for (const p of toRemove) { - // p.removeFile(info); - // if (!p.hasRoots()) { - // this.removeProject(p); - // } - // } - // } - // } - // if (contains(this.openFilesReferenced, info)) { - // removeItemFromSet(this.openFilesReferenced, info); - // } - // if (project.projectKind === ProjectKind.Configured) { - // this.openFileRootsConfigured.push(info); - // } } } project.addRoot(info); @@ -883,167 +873,12 @@ namespace ts.server { this.removeProject(inferredProject); } } - // let inConfiguredProject = false; - // let inExternalProject = false; - // for (const p of info.containingProjects) { - // inConfiguredProject = inConfiguredProject || p.projectKind === ProjectKind.Configured; - // inExternalProject = inExternalProject || p.projectKind === ProjectKind.External; - // } - // if (inConfiguredProject || inExternalProject) { - // const inferredProjects = rootFile.containingProjects.filter(p => p.projectKind === ProjectKind.Inferred); - // for (const p of inferredProjects) { - // p.removeFile(rootFile, /*detachFromProject*/ true); - // if (!p.hasRoots()) { - // this.removeProject(p); - // } - // } - // } - // else { - // if (rootFile.containingProjects.length > 1) { - // // TODO: fixme - // // file is contained in more than one inferred project - keep only ones where it is used as reference - // const roots = rootFile.containingProjects.filter(p => p.isRoot(rootFile)); - // for (const root of roots) { - // this.removeProject(root); - // } - // Debug.assert(info.containingProjects.length > 0); - // } - // } - } - - // if (info.containingProjects.length === 0) { - // unattachedOpenFiles.push(info); - // } - // else { - // openFileRootsConfigured.push(info); - // } - // const project = info.defaultProject; - // if (!project || !(project.containsScriptInfo(info))) { - // info.defaultProject = undefined; - // unattachedOpenFiles.push(info); - // } - // else { - // openFileRootsConfigured.push(info); - // } } for (const unattached of unattachedOpenFiles) { this.assignScriptInfoToInferredProjectIfNecessary(unattached, /*addToListOfOpenFiles*/ false); } - // this.openFileRootsConfigured = openFileRootsConfigured; - - // // First loop through all open files that are referenced by projects but are not - // // project roots. For each referenced file, see if the default project still - // // references that file. If so, then just keep the file in the referenced list. - // // If not, add the file to an unattached list, to be rechecked later. - // const openFilesReferenced: ScriptInfo[] = []; - // for (const referencedFile of this.openFilesReferenced) { - // // check if any of projects that used to reference this file are still referencing it - // if (referencedFile.containingProjects.length === 0) { - // unattachedOpenFiles.push(referencedFile); - // } - // else { - // openFilesReferenced.push(referencedFile); - // } - // // referencedFile.defaultProject.updateGraph(); - // // if (referencedFile.defaultProject.containsScriptInfo(referencedFile)) { - // // openFilesReferenced.push(referencedFile); - // // } - // // else { - // // unattachedOpenFiles.push(referencedFile); - // // } - // } - // this.openFilesReferenced = openFilesReferenced; - - // Then, loop through all of the open files that are project roots. - // For each root file, note the project that it roots. Then see if - // any other projects newly reference the file. If zero projects - // newly reference the file, keep it as a root. If one or more - // projects newly references the file, remove its project from the - // inferred projects list (since it is no longer a root) and add - // the file to the open, referenced file list. - // const openFileRoots: ScriptInfo[] = []; - // for (const rootFile of this.openFileRoots) { - // let inConfiguredProject = false; - // let inExternalProject = false; - // for (const p of rootFile.containingProjects) { - // inConfiguredProject = inConfiguredProject || p.projectKind === ProjectKind.Configured; - // inExternalProject = inExternalProject || p.projectKind === ProjectKind.External; - // } - // if (inConfiguredProject || inExternalProject) { - // const inferredProjects = rootFile.containingProjects.filter(p => p.projectKind === ProjectKind.Inferred); - // for (const p of inferredProjects) { - // p.removeFile(rootFile, /*detachFromProject*/ true); - // if (!p.hasRoots()) { - // this.removeProject(p); - // } - // } - // if (inConfiguredProject) { - // this.openFileRootsConfigured.push(rootFile); - // } - // } - // else { - // if (rootFile.containingProjects.length === 1) { - // // file contained only in one project - // openFileRoots.push(rootFile); - // } - // else { - // // TODO: fixme - // // file is contained in more than one inferred project - keep only ones where it is used as reference - // const roots = rootFile.containingProjects.filter(p => p.isRoot(rootFile)); - // for (const root of roots) { - // this.removeProject(root); - // } - // Debug.assert(rootFile.containingProjects.length > 0); - // this.openFilesReferenced.push(rootFile); - // } - // } - // if (rootFile.containingProjects.some(p => p.projectKind !== ProjectKind.Inferred)) { - // // file was included in non-inferred project - drop old inferred project - - // } - // else { - // openFileRoots.push(rootFile); - // } - // let inferredProjectsToRemove: Project[]; - // for (const p of rootFile.containingProjects) { - // if (p.projectKind !== ProjectKind.Inferred) { - // // file was included in non-inferred project - drop old inferred project - // } - // } - - // const rootedProject = rootFile.defaultProject; - // const referencingProjects = this.findReferencingProjects(rootFile, rootedProject); - - // if (rootFile.defaultProject && rootFile.defaultProject.projectKind !== ProjectKind.Inferred) { - // // If the root file has already been added into a configured project, - // // meaning the original inferred project is gone already. - // if (rootedProject.projectKind === ProjectKind.Inferred) { - // this.removeProject(rootedProject); - // } - // this.openFileRootsConfigured.push(rootFile); - // } - // else { - // if (referencingProjects.length === 0) { - // rootFile.defaultProject = rootedProject; - // openFileRoots.push(rootFile); - // } - // else { - // // remove project from inferred projects list because root captured - // this.removeProject(rootedProject); - // this.openFilesReferenced.push(rootFile); - // } - // } - // } - // this.openFileRoots = openFileRoots; - - // Finally, if we found any open, referenced files that are no longer - // referenced by their default project, treat them as newly opened - // by the editor. - // for (const f of unattachedOpenFiles) { - // this.addOpenFile(f); - // } for (const p of this.inferredProjects) { p.updateGraph(); } From 65e5a72c5cba42dab8961336a65c0c6e3e22c368 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 29 Jun 2016 16:49:45 -0700 Subject: [PATCH 075/307] fix path normalization --- src/server/editorServices.ts | 20 +++++++++++++------- src/server/scriptInfo.ts | 2 +- src/server/utilities.ts | 4 +++- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 245fbf61304..e133ca0eb98 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -442,6 +442,10 @@ namespace ts.server { } } } + if (info.containingProjects.length === 0) { + // if there are not projects that include this script info - delete it + this.filenameToScriptInfo.remove(info.fileName); + } } /** @@ -666,14 +670,16 @@ namespace ts.server { return { success: true, project, errors }; } - private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newRootFiles: string[], newOptions: CompilerOptions) { + private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedRootFiles: string[], newOptions: CompilerOptions) { const oldRootFiles = project.getRootFiles(); - - // TODO: verify that newRootFiles are always normalized - // TODO: avoid N^2 - const newFileNames = asNormalizedPathArray(filter(newRootFiles, f => this.host.fileExists(f))); - const fileNamesToRemove = asNormalizedPathArray(oldRootFiles.filter(f => !contains(newFileNames, f))); - const fileNamesToAdd = asNormalizedPathArray(newFileNames.filter(f => !contains(oldRootFiles, f))); + const newFileNames: NormalizedPath[] = []; + for (const f of newUncheckedRootFiles) { + if (this.host.fileExists(f)) { + newFileNames.push(toNormalizedPath(f)); + } + } + const fileNamesToRemove = oldRootFiles.filter(f => !contains(newFileNames, f)); + const fileNamesToAdd = newFileNames.filter(f => !contains(oldRootFiles, f)); for (const fileName of fileNamesToRemove) { const info = this.getScriptInfoForNormalizedPath(fileName); diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 9be0f2e590e..7ab6bc7e7ec 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -60,7 +60,7 @@ namespace ts.server { if (this.containingProjects[0] === project) { this.containingProjects[0] = this.containingProjects.pop(); } - if (this.containingProjects[1] === project) { + else if (this.containingProjects[1] === project) { this.containingProjects.pop(); } break; diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 34953f9a15f..03d608bfd3e 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -49,10 +49,12 @@ namespace ts.server { if (index < 0) { return; } - if (items.length === 1) { + if (index === items.length - 1) { + // last item - pop it items.pop(); } else { + // non-last item - replace it with the last one items[index] = items.pop(); } } From 74d8d656f133c9dde87c283bf40c7953ea917dbc Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 29 Jun 2016 18:00:07 -0700 Subject: [PATCH 076/307] move setImmediate to host --- src/harness/harnessLanguageService.ts | 8 +++ src/server/session.ts | 10 +-- src/server/utilities.ts | 4 -- .../cases/unittests/cachingInServerLSHost.ts | 4 +- tests/cases/unittests/session.ts | 4 +- .../cases/unittests/tsserverProjectSystem.ts | 64 ++++++++++++++----- 6 files changed, 68 insertions(+), 26 deletions(-) diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 553d59428ab..3b5918c3286 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -666,6 +666,14 @@ namespace Harness.LanguageService { clearTimeout(timeoutId: any): void { clearTimeout(timeoutId); } + + setImmediate(callback: (...args: any[]) => void, ms: number, ...args: any[]): any { + return setImmediate(callback, args); + } + + clearImmediate(timeoutId: any): void { + clearImmediate(timeoutId); + } } export class ServerLanguageServiceAdapter implements LanguageServiceAdapter { diff --git a/src/server/session.ts b/src/server/session.ts index f3d439580f3..b741c421859 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -165,9 +165,11 @@ namespace ts.server { export const ProjectLanguageServiceDisabled = new Error("The project's language service is disabled."); } - export interface ServerHost extends ts.System { + export interface ServerHost extends System { setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; clearTimeout(timeoutId: any): void; + setImmediate(callback: (...args: any[]) => void, ...args: any[]): any; + clearImmediate(timeoutId: any): void; } export class Session { @@ -318,10 +320,10 @@ namespace ts.server { followMs = ms; } if (this.errorTimer) { - clearTimeout(this.errorTimer); + this.host.clearTimeout(this.errorTimer); } if (this.immediateId) { - clearImmediate(this.immediateId); + this.host.clearImmediate(this.immediateId); this.immediateId = undefined; } let index = 0; @@ -331,7 +333,7 @@ namespace ts.server { index++; if (checkSpec.project.containsFile(checkSpec.fileName, requireOpen)) { this.syntacticCheck(checkSpec.fileName, checkSpec.project); - this.immediateId = setImmediate(() => { + this.immediateId = this.host.setImmediate(() => { this.semanticCheck(checkSpec.fileName, checkSpec.project); this.immediateId = undefined; if (checkList.length > index) { diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 03d608bfd3e..79a8edc0f8c 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -69,10 +69,6 @@ namespace ts.server { return fileName; } - export function asNormalizedPathArray(fileNames: string[]): NormalizedPath[] { - return fileNames; - } - export interface NormalizedPathMap { get(path: NormalizedPath): T; set(path: NormalizedPath, value: T): void; diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index 0d9590d5431..888618fcefb 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -63,7 +63,9 @@ namespace ts { }; }, setTimeout, - clearTimeout + clearTimeout, + setImmediate, + clearImmediate }; } diff --git a/tests/cases/unittests/session.ts b/tests/cases/unittests/session.ts index c8a952da02e..90343f4699d 100644 --- a/tests/cases/unittests/session.ts +++ b/tests/cases/unittests/session.ts @@ -21,7 +21,9 @@ namespace ts.server { readDirectory(): string[] { return []; }, exit(): void { }, setTimeout(callback, ms, ...args) { return 0; }, - clearTimeout(timeoutId) { } + clearTimeout(timeoutId) { }, + setImmediate: () => 0, + clearImmediate() {} }; const nullCancellationToken: HostCancellationToken = { isCancellationRequested: () => false }; const mockLogger: Logger = { diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index fc6ef16e398..a0ba0c37ce5 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -136,6 +136,36 @@ namespace ts { checkFileNames(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); } + class Callbacks { + private map: { [n: number]: TimeOutCallback } = {}; + private nextId = 1; + + register(cb: (...args: any[]) => void, args: any[]) { + const timeoutId = this.nextId; + this.nextId++; + this.map[timeoutId] = cb.bind(undefined, ...args); + return timeoutId; + } + unregister(id: any) { + if (typeof id === "number") { + delete this.map[id]; + } + } + + count() { + return sizeOfMap(this.map); + } + + invoke() { + for (const id in this.map) { + if (hasProperty(this.map, id)) { + this.map[id](); + } + } + this.map = {}; + } + } + type TimeOutCallback = () => any; class TestServerHost implements server.ServerHost { @@ -146,8 +176,8 @@ namespace ts { private getCanonicalFileName: (s: string) => string; private toPath: (f: string) => Path; - private nextTimeoutId = 0; - private callbacks: { [n: number]: TimeOutCallback } = {}; + private timeoutCallbacks = new Callbacks(); + private immediateCallbacks = new Callbacks(); readonly watchedDirectories: Map<{ cb: DirectoryWatcherCallback, recursive: boolean }[]> = {}; readonly watchedFiles: Map = {}; @@ -286,30 +316,32 @@ namespace ts { } // TOOD: record and invoke callbacks to simulate timer events - readonly setTimeout = (callback: TimeOutCallback, time: number, ...args: any[]) => { - const timeoutId = this.nextTimeoutId; - this.nextTimeoutId++; - this.callbacks[timeoutId] = callback.bind(undefined, ...args); - return timeoutId; + setTimeout (callback: TimeOutCallback, time: number, ...args: any[]) { + return this.timeoutCallbacks.register(callback, args); }; - readonly clearTimeout = (timeoutId: any): void => { - if (typeof timeoutId === "number") { - delete this.callbacks[timeoutId]; - } + + clearTimeout(timeoutId: any): void { + this.timeoutCallbacks.unregister(timeoutId); }; checkTimeoutQueueLength(expected: number) { - const callbacksCount = sizeOfMap(this.callbacks); + const callbacksCount = this.timeoutCallbacks.count(); assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`); } runQueuedTimeoutCallbacks() { - for (const id in this.callbacks) { - this.callbacks[id](); - } - this.callbacks = []; + this.timeoutCallbacks.invoke(); } + setImmediate (callback: TimeOutCallback, time: number, ...args: any[]) { + return this.immediateCallbacks.register(callback, args); + }; + + clearImmediate(timeoutId: any): void { + this.immediateCallbacks.unregister(timeoutId); + }; + + readonly readFile = (s: string) => (this.fs.get(this.toPath(s))).content; readonly resolvePath = (s: string) => s; readonly getExecutingFilePath = () => this.executingFilePath; From e72bb57b9ae807ab042ee19d5cd540dd21975b3b Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 29 Jun 2016 20:59:39 -0700 Subject: [PATCH 077/307] added tests --- src/server/editorServices.ts | 2 +- .../cases/unittests/tsserverProjectSystem.ts | 42 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index e133ca0eb98..65b34d817f8 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -121,7 +121,7 @@ namespace ts.server { /** * maps external project file name to list of config files that were the part of this project */ - private readonly externalProjectToConfiguredProjectMap: Map; + private readonly externalProjectToConfiguredProjectMap: Map = {}; /** * external projects (configuration and list of root files is not controlled by tsserver) diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index a0ba0c37ce5..c989d376c2a 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -116,6 +116,10 @@ namespace ts { assert.equal(projectService.configuredProjects.length, expected, `expected ${expected} configured project(s)`); } + function checkNumberOfExternalProjects(projectService: server.ProjectService, expected: number) { + assert.equal(projectService.externalProjects.length, expected, `expected ${expected} external project(s)`); + } + function checkNumberOfInferredProjects(projectService: server.ProjectService, expected: number) { assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`); } @@ -144,7 +148,7 @@ namespace ts { const timeoutId = this.nextId; this.nextId++; this.map[timeoutId] = cb.bind(undefined, ...args); - return timeoutId; + return timeoutId; } unregister(id: any) { if (typeof id === "number") { @@ -759,5 +763,41 @@ namespace ts { projectService.closeClientFile(file1.path); checkNumberOfConfiguredProjects(projectService, 0); }); + + it ("should not close external project with no open files", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y =1;" + }; + const externalProjectName = "externalproject"; + const host = createServerHost({ fileOrFolderList: [file1, file2], libFile }); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ true); + projectService.openExternalProject({ + rootFiles: [ file1.path, file2.path ], + options: {}, + projectFileName: externalProjectName + }); + + checkNumberOfExternalProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + // open client file - should not lead to creation of inferred project + projectService.openClientFile(file1.path, file1.content); + checkNumberOfExternalProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + // close client file - external project should still exists + projectService.closeClientFile(file1.path); + checkNumberOfExternalProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + projectService.closeExternalProject(externalProjectName); + checkNumberOfExternalProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 0); + }); }); } \ No newline at end of file From f6b9c0bfd30610aab9a31d6d73577d66fcbe8046 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 29 Jun 2016 21:23:23 -0700 Subject: [PATCH 078/307] more tests --- src/harness/harnessLanguageService.ts | 2 +- .../cases/unittests/tsserverProjectSystem.ts | 74 ++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 3b5918c3286..3e431c73f9e 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -673,7 +673,7 @@ namespace Harness.LanguageService { clearImmediate(timeoutId: any): void { clearImmediate(timeoutId); - } + } } export class ServerLanguageServiceAdapter implements LanguageServiceAdapter { diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index c989d376c2a..9495a4a4403 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -775,7 +775,7 @@ namespace ts { }; const externalProjectName = "externalproject"; const host = createServerHost({ fileOrFolderList: [file1, file2], libFile }); - const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ true); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openExternalProject({ rootFiles: [ file1.path, file2.path ], options: {}, @@ -799,5 +799,77 @@ namespace ts { checkNumberOfExternalProjects(projectService, 0); checkNumberOfInferredProjects(projectService, 0); }); + + it ("external project that included config files", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + const config1 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify( + { + compilerOptions: {}, + files: ["f1.ts"] + } + ) + }; + const file2 = { + path: "/a/c/f2.ts", + content: "let y =1;" + }; + const config2 = { + path: "/a/c/tsconfig.json", + content: JSON.stringify( + { + compilerOptions: {}, + files: ["f2.ts"] + } + ) + }; + const file3 = { + path: "/a/d/f3.ts", + content: "let z =1;" + }; + const externalProjectName = "externalproject"; + const host = createServerHost({ fileOrFolderList: [file1, file2, file3, config1, config2], libFile }); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); + projectService.openExternalProject({ + rootFiles: [ config1.path, config2.path, file3.path ], + options: {}, + projectFileName: externalProjectName + }); + + checkNumberOfExternalProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 2); + checkNumberOfInferredProjects(projectService, 0); + + // open client file - should not lead to creation of inferred project + projectService.openClientFile(file1.path, file1.content); + checkNumberOfExternalProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 2); + checkNumberOfInferredProjects(projectService, 0); + + projectService.openClientFile(file3.path, file3.content); + checkNumberOfExternalProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 2); + checkNumberOfInferredProjects(projectService, 1); + + projectService.closeExternalProject(externalProjectName); + checkNumberOfExternalProjects(projectService, 0); + // open file 'file1' from configured project is moved to its own inferred project + inferred project for file3 + checkNumberOfInferredProjects(projectService, 2); + + projectService.closeClientFile(file3.path); + checkNumberOfExternalProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 1); + + projectService.closeClientFile(file1.path); + checkNumberOfExternalProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 0); + }); + }); } \ No newline at end of file From dcf3cc248b825ee2c7fcd7ea16c8f60695f228c8 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 29 Jun 2016 22:35:54 -0700 Subject: [PATCH 079/307] more tests --- src/server/editorServices.ts | 14 ++- .../cases/unittests/tsserverProjectSystem.ts | 89 ++++++++++++++++++- 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 65b34d817f8..56a7dae4389 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -983,10 +983,11 @@ namespace ts.server { if (configFiles) { for (const configFile of configFiles) { const configuredProject = this.findConfiguredProjectByProjectName(configFile); - if (configuredProject) { + if (configuredProject && configuredProject.deleteOpenRef() === 0) { this.removeProject(configuredProject); } } + // TODO: do this only if ownership of files is changed this.refreshInferredProjects(); } else { @@ -1019,9 +1020,14 @@ namespace ts.server { // store the list of tsconfig files that belong to the external project this.externalProjectToConfiguredProjectMap[proj.projectFileName] = tsConfigFiles; for (const tsconfigFile of tsConfigFiles) { - const { success, project, errors } = this.openConfigFile(tsconfigFile); - if (success) { - // keep project alive - its lifetime is bound to the lifetime of containing external project + let project = this.findConfiguredProjectByProjectName(tsconfigFile); + if (!project) { + const result = this.openConfigFile(tsconfigFile); + // TODO: save errors + project = result.success && result.project; + } + if (project) { + // keep project alive even if no documents are opened - its lifetime is bound to the lifetime of containing external project project.addOpenRef(); } } diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index 9495a4a4403..8b4a9304efd 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -857,13 +857,14 @@ namespace ts { projectService.closeExternalProject(externalProjectName); checkNumberOfExternalProjects(projectService, 0); - // open file 'file1' from configured project is moved to its own inferred project + inferred project for file3 - checkNumberOfInferredProjects(projectService, 2); + // open file 'file1' from configured project keeps project alive + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 1); projectService.closeClientFile(file3.path); checkNumberOfExternalProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 0); - checkNumberOfInferredProjects(projectService, 1); + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); projectService.closeClientFile(file1.path); checkNumberOfExternalProjects(projectService, 0); @@ -871,5 +872,85 @@ namespace ts { checkNumberOfInferredProjects(projectService, 0); }); + it("external project with included config file opened after configured project", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const externalProjectName = "externalproject"; + const host = createServerHost({ fileOrFolderList: [file1, configFile], libFile }); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); + + projectService.openClientFile(file1.path); + checkNumberOfExternalProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + projectService.openExternalProject({ + rootFiles: [ configFile.path ], + options: {}, + projectFileName: externalProjectName + }); + + checkNumberOfExternalProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + projectService.closeClientFile(file1.path); + checkNumberOfExternalProjects(projectService, 0); + // configured project is alive since it is opened as part of external project + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + projectService.closeExternalProject(externalProjectName); + checkNumberOfExternalProjects(projectService, 0); + // configured project is alive since it is opened as part of external project + checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 0); + }); + it("external project with included config file opened after configured project and then closed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const externalProjectName = "externalproject"; + const host = createServerHost({ fileOrFolderList: [file1, configFile], libFile }); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); + + projectService.openClientFile(file1.path); + checkNumberOfExternalProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + projectService.openExternalProject({ + rootFiles: [ configFile.path ], + options: {}, + projectFileName: externalProjectName + }); + + checkNumberOfExternalProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + projectService.closeExternalProject(externalProjectName); + checkNumberOfExternalProjects(projectService, 0); + // configured project is alive since file is still open + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + projectService.closeClientFile(file1.path); + checkNumberOfExternalProjects(projectService, 0); + // configured project is alive since it is opened as part of external project + checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 0); + }); }); } \ No newline at end of file From a9cd516119675e281c19202defb1056a81da1b58 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 30 Jun 2016 09:58:05 -0700 Subject: [PATCH 080/307] initialize setImmediate\clearImmediate --- src/server/server.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/server.ts b/src/server/server.ts index 17d7e15c19d..c9f807778c1 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -292,6 +292,8 @@ namespace ts.server { sys.setTimeout = setTimeout; sys.clearTimeout = clearTimeout; + sys.setImmediate = setImmediate; + sys.clearImmediate = clearImmediate; let cancellationToken: HostCancellationToken; try { From bebc5711cb40c0a4e423534e2610983f54af5a8e Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 30 Jun 2016 13:23:55 -0700 Subject: [PATCH 081/307] more tests --- src/server/editorServices.ts | 7 +- src/server/project.ts | 4 + .../cases/unittests/tsserverProjectSystem.ts | 95 +++++++++++++------ 3 files changed, 75 insertions(+), 31 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 56a7dae4389..311292dfa38 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -72,12 +72,12 @@ namespace ts.server { /** * a path to directory watcher map that detects added tsconfig files **/ - private directoryWatchersForTsconfig: Map = {}; + private readonly directoryWatchersForTsconfig: Map = {}; /** * count of how many projects are using the directory watcher. * If the number becomes 0 for a watcher, then we should close it. **/ - private directoryWatchersRefCount: Map = {}; + private readonly directoryWatchersRefCount: Map = {}; constructor(private readonly projectService: ProjectService) { } @@ -208,7 +208,7 @@ namespace ts.server { private updateProjectGraphs(projects: Project[]) { let shouldRefreshInferredProjects = false; for (const p of projects) { - if (p.updateGraph()) { + if (!p.updateGraph()) { shouldRefreshInferredProjects = true; } } @@ -231,7 +231,6 @@ namespace ts.server { if (info && (!info.isOpen)) { // file has been changed which might affect the set of referenced files in projects that include // this file and set of inferred projects - // TODO: add tests info.reloadFromFile(); this.updateProjectGraphs(info.containingProjects); } diff --git a/src/server/project.ts b/src/server/project.ts index 1d11a2ed984..a7898e39574 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -190,6 +190,10 @@ namespace ts.server { this.projectStateVersion++; } + /** + * Updates set of files that contribute to this project + * @returns: true if set of files in the project stays the same and false - otherwise. + */ updateGraph(): boolean { if (!this.languageServiceEnabled) { return true; diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index 8b4a9304efd..f5432f5561e 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -21,25 +21,34 @@ namespace ts { }; const { content: libFileContent } = Harness.getDefaultLibraryFile(Harness.IO); + const libFile: FileOrFolder = { + path: "/a/lib/lib.d.ts", + content: libFileContent + }; - function getExecutingFilePathFromLibFile(libFile: FileOrFolder): string { + function getExecutingFilePathFromLibFile(libFilePath: string): string { return combinePaths(getDirectoryPath(libFile.path), "tsc.js"); } interface TestServerHostCreationParameters { - fileOrFolderList: FileOrFolder[]; useCaseSensitiveFileNames?: boolean; executingFilePath?: string; libFile?: FileOrFolder; currentDirectory?: string; } - function createServerHost(params: TestServerHostCreationParameters): TestServerHost { + function createServerHost(fileOrFolderList: FileOrFolder[], + params?: TestServerHostCreationParameters, + libFilePath: string = libFile.path): TestServerHost { + + if (!params) { + params = {}; + } return new TestServerHost( params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false, - params.executingFilePath || getExecutingFilePathFromLibFile(params.libFile), + params.executingFilePath || getExecutingFilePathFromLibFile(libFilePath), params.currentDirectory || "/", - params.fileOrFolderList); + fileOrFolderList); } interface FileOrFolder { @@ -365,10 +374,6 @@ namespace ts { path: "/a/b/commonFile2.ts", content: "let y = 1" }; - const libFile: FileOrFolder = { - path: "/a/lib/lib.d.ts", - content: libFileContent - }; it("create inferred project", () => { const appFile: FileOrFolder = { @@ -383,7 +388,7 @@ namespace ts { path: "/a/b/c/module.d.ts", content: `export let x: number` }; - const host = createServerHost({ fileOrFolderList: [appFile, moduleFile, libFile], libFile }); + const host = createServerHost([appFile, moduleFile, libFile]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); const { configFileName } = projectService.openClientFile(appFile.path); @@ -421,8 +426,7 @@ namespace ts { content: "let z = 1" }; - - const host = createServerHost({ fileOrFolderList: [configFile, libFile, file1, file2, file3], libFile }); + const host = createServerHost([configFile, libFile, file1, file2, file3]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); @@ -447,7 +451,7 @@ namespace ts { }` }; const filesWithoutConfig = [libFile, commonFile1, commonFile2]; - const host = createServerHost({ fileOrFolderList: filesWithoutConfig, libFile }); + const host = createServerHost(filesWithoutConfig); const filesWithConfig = [libFile, commonFile1, commonFile2, configFile]; const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); @@ -480,7 +484,7 @@ namespace ts { path: "/a/b/tsconfig.json", content: `{}` }; - const host = createServerHost({ fileOrFolderList: [commonFile1, libFile, configFile], libFile }); + const host = createServerHost([commonFile1, libFile, configFile]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(commonFile1.path); checkWatchedDirectories(host, ["/a/b"]); @@ -508,7 +512,7 @@ namespace ts { ] }` }; - const host = createServerHost({ fileOrFolderList: [commonFile1, commonFile2, configFile], libFile }); + const host = createServerHost([commonFile1, commonFile2, configFile]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(commonFile1.path); projectService.openClientFile(commonFile2.path); @@ -524,7 +528,7 @@ namespace ts { path: "/a/b/tsconfig.json", content: `{}` }; - const host = createServerHost({ fileOrFolderList: [commonFile1, commonFile2, configFile], libFile }); + const host = createServerHost([commonFile1, commonFile2, configFile]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(commonFile1.path); @@ -554,7 +558,7 @@ namespace ts { }` }; const files = [commonFile1, commonFile2, configFile]; - const host = createServerHost({ fileOrFolderList: files, libFile }); + const host = createServerHost(files); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(commonFile1.path); @@ -587,7 +591,7 @@ namespace ts { content: `let t = 1;` }; - const host = createServerHost({ fileOrFolderList: [commonFile1, commonFile2, excludedFile1, configFile], libFile }); + const host = createServerHost([commonFile1, commonFile2, excludedFile1, configFile]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(commonFile1.path); @@ -621,7 +625,7 @@ namespace ts { }` }; const files = [file1, nodeModuleFile, classicModuleFile, configFile]; - const host = createServerHost({ fileOrFolderList: files, libFile }); + const host = createServerHost(files); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(file1.path); projectService.openClientFile(nodeModuleFile.path); @@ -662,7 +666,7 @@ namespace ts { "files": [ "main.ts" ] }` }; - const host = createServerHost({ fileOrFolderList: [file1, file2, configFile], libFile }); + const host = createServerHost([file1, file2, configFile]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(file1.path); projectService.closeClientFile(file1.path); @@ -689,7 +693,7 @@ namespace ts { "files": [ "main.ts" ] }` }; - const host = createServerHost({ fileOrFolderList: [file1, file2, configFile], libFile }); + const host = createServerHost([file1, file2, configFile]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(file1.path); projectService.closeClientFile(file1.path); @@ -722,7 +726,7 @@ namespace ts { content: "let x =1;" }; - const host = createServerHost({ fileOrFolderList: [file1, file2, file3, libFile], libFile }); + const host = createServerHost([file1, file2, file3, libFile]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ true); projectService.openClientFile(file1.path); projectService.openClientFile(file2.path); @@ -755,7 +759,7 @@ namespace ts { "files": [ "main.ts" ] }` }; - const host = createServerHost({ fileOrFolderList: [file1, configFile, libFile], libFile }); + const host = createServerHost([file1, configFile, libFile]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ true); projectService.openClientFile(file1.path); checkNumberOfConfiguredProjects(projectService, 1); @@ -774,7 +778,7 @@ namespace ts { content: "let y =1;" }; const externalProjectName = "externalproject"; - const host = createServerHost({ fileOrFolderList: [file1, file2], libFile }); + const host = createServerHost([file1, file2]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openExternalProject({ rootFiles: [ file1.path, file2.path ], @@ -832,7 +836,7 @@ namespace ts { content: "let z =1;" }; const externalProjectName = "externalproject"; - const host = createServerHost({ fileOrFolderList: [file1, file2, file3, config1, config2], libFile }); + const host = createServerHost([file1, file2, file3, config1, config2]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openExternalProject({ rootFiles: [ config1.path, config2.path, file3.path ], @@ -882,7 +886,7 @@ namespace ts { content: JSON.stringify({ compilerOptions: {} }) }; const externalProjectName = "externalproject"; - const host = createServerHost({ fileOrFolderList: [file1, configFile], libFile }); + const host = createServerHost([file1, configFile]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(file1.path); @@ -922,7 +926,7 @@ namespace ts { content: JSON.stringify({ compilerOptions: {} }) }; const externalProjectName = "externalproject"; - const host = createServerHost({ fileOrFolderList: [file1, configFile], libFile }); + const host = createServerHost([file1, configFile]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(file1.path); @@ -952,5 +956,42 @@ namespace ts { checkNumberOfConfiguredProjects(projectService, 0); checkNumberOfInferredProjects(projectService, 0); }); + + it("changes in closed files are reflected in project structure", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export let x = 1` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createServerHost([file1, file2, file3]); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useSingleInferredProject*/ false); + + projectService.openClientFile(file1.path); + + checkNumberOfInferredProjects(projectService, 1); + checkProjectActualFiles(projectService.inferredProjects[0], [ file1.path, file2.path ]); + + projectService.openClientFile(file3.path); + checkNumberOfInferredProjects(projectService, 2); + checkProjectActualFiles(projectService.inferredProjects[1], [ file3.path ]); + + const modifiedFile2 = { + path: file2.path, + content: `export * from "../c/f3"` // now inferred project should inclule file3 + }; + + host.reloadFS([file1, modifiedFile2, file3]); + host.triggerFileWatcherCallback(modifiedFile2.path, /*removed*/ false); + + checkNumberOfInferredProjects(projectService, 1); + checkProjectActualFiles(projectService.inferredProjects[0], [ file1.path, modifiedFile2.path, file3.path ]); + }); }); } \ No newline at end of file From 9eb01bfe162fb139df561111e7232f638849d988 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 30 Jun 2016 13:50:10 -0700 Subject: [PATCH 082/307] more tests --- .../cases/unittests/tsserverProjectSystem.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index f5432f5561e..45314181b9a 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -993,5 +993,45 @@ namespace ts { checkNumberOfInferredProjects(projectService, 1); checkProjectActualFiles(projectService.inferredProjects[0], [ file1.path, modifiedFile2.path, file3.path ]); }); + + it("deleted files affect project structure", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export * from "../c/f3"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createServerHost([file1, file2, file3]); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useSingleInferredProject*/ false); + + projectService.openClientFile(file1.path); + + checkNumberOfInferredProjects(projectService, 1); + checkNumberOfExternalProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 0); + + checkProjectActualFiles(projectService.inferredProjects[0], [ file1.path, file2.path, file3.path ]); + + projectService.openClientFile(file3.path); + checkNumberOfInferredProjects(projectService, 1); + checkNumberOfExternalProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 0); + + host.reloadFS([file1, file3]); + host.triggerFileWatcherCallback(file2.path, /*removed*/ true); + + checkNumberOfInferredProjects(projectService, 2); + checkNumberOfExternalProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 0); + + checkProjectActualFiles(projectService.inferredProjects[0], [ file1.path ]); + checkProjectActualFiles(projectService.inferredProjects[1], [ file3.path ]); + }); }); } \ No newline at end of file From 07817fedb04b9f3a12f7390c6a7fe3d6ad32c922 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 5 Jul 2016 11:14:23 -0700 Subject: [PATCH 083/307] more tests --- src/server/editorServices.ts | 6 +- .../cases/unittests/tsserverProjectSystem.ts | 112 +++++++++--------- 2 files changed, 62 insertions(+), 56 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 311292dfa38..c91760c3977 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -247,9 +247,10 @@ namespace ts.server { if (!info.isOpen) { this.filenameToScriptInfo.remove(info.fileName); - // capture list of projects + // capture list of projects since detachAllProjects will wipe out original list const containingProjects = info.containingProjects.slice(); info.detachAllProjects(); + // update projects to make sure that set of referenced files is correct this.updateProjectGraphs(containingProjects); @@ -298,7 +299,7 @@ namespace ts.server { // just update the current project. this.updateConfiguredProject(project); - // Call updateProjectStructure to clean up inferred projects we may have + // Call refreshInferredProjects to clean up inferred projects we may have // created for the new files this.refreshInferredProjects(); } @@ -321,7 +322,6 @@ namespace ts.server { } this.log(`Detected newly added tsconfig file: ${fileName}`); - // TODO: add tests to check correct migration of currently open file if it is referenced from the root file of configured project this.reloadProjects(); } diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index 45314181b9a..5cf7500a196 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -133,6 +133,12 @@ namespace ts { assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`); } + function checkNumberOfProjects(projectService: server.ProjectService, count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) { + checkNumberOfConfiguredProjects(projectService, count.configuredProjects || 0); + checkNumberOfExternalProjects(projectService, count.externalProjects || 0); + checkNumberOfInferredProjects(projectService, count.inferredProjects || 0); + } + function checkWatchedFiles(host: TestServerHost, expectedFiles: string[]) { checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles); } @@ -844,36 +850,24 @@ namespace ts { projectFileName: externalProjectName }); - checkNumberOfExternalProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 2); - checkNumberOfInferredProjects(projectService, 0); + checkNumberOfProjects(projectService, { configuredProjects: 2 }); // open client file - should not lead to creation of inferred project projectService.openClientFile(file1.path, file1.content); - checkNumberOfExternalProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 2); - checkNumberOfInferredProjects(projectService, 0); + checkNumberOfProjects(projectService, { configuredProjects: 2 }); projectService.openClientFile(file3.path, file3.content); - checkNumberOfExternalProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 2); - checkNumberOfInferredProjects(projectService, 1); + checkNumberOfProjects(projectService, { configuredProjects: 2, inferredProjects: 1 }); projectService.closeExternalProject(externalProjectName); - checkNumberOfExternalProjects(projectService, 0); // open file 'file1' from configured project keeps project alive - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 1); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); projectService.closeClientFile(file3.path); - checkNumberOfExternalProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); projectService.closeClientFile(file1.path); - checkNumberOfExternalProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 0); - checkNumberOfInferredProjects(projectService, 0); + checkNumberOfProjects(projectService, {}); }); it("external project with included config file opened after configured project", () => { @@ -890,9 +884,7 @@ namespace ts { const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(file1.path); - checkNumberOfExternalProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); projectService.openExternalProject({ rootFiles: [ configFile.path ], @@ -900,21 +892,14 @@ namespace ts { projectFileName: externalProjectName }); - checkNumberOfExternalProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); projectService.closeClientFile(file1.path); - checkNumberOfExternalProjects(projectService, 0); // configured project is alive since it is opened as part of external project - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); projectService.closeExternalProject(externalProjectName); - checkNumberOfExternalProjects(projectService, 0); - // configured project is alive since it is opened as part of external project - checkNumberOfConfiguredProjects(projectService, 0); - checkNumberOfInferredProjects(projectService, 0); + checkNumberOfProjects(projectService, { configuredProjects: 0 }); }); it("external project with included config file opened after configured project and then closed", () => { const file1 = { @@ -930,9 +915,7 @@ namespace ts { const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openClientFile(file1.path); - checkNumberOfExternalProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); projectService.openExternalProject({ rootFiles: [ configFile.path ], @@ -940,21 +923,14 @@ namespace ts { projectFileName: externalProjectName }); - checkNumberOfExternalProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); projectService.closeExternalProject(externalProjectName); - checkNumberOfExternalProjects(projectService, 0); // configured project is alive since file is still open - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); projectService.closeClientFile(file1.path); - checkNumberOfExternalProjects(projectService, 0); - // configured project is alive since it is opened as part of external project - checkNumberOfConfiguredProjects(projectService, 0); - checkNumberOfInferredProjects(projectService, 0); + checkNumberOfProjects(projectService, {}); }); it("changes in closed files are reflected in project structure", () => { @@ -1012,26 +988,56 @@ namespace ts { projectService.openClientFile(file1.path); - checkNumberOfInferredProjects(projectService, 1); - checkNumberOfExternalProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); checkProjectActualFiles(projectService.inferredProjects[0], [ file1.path, file2.path, file3.path ]); projectService.openClientFile(file3.path); - checkNumberOfInferredProjects(projectService, 1); - checkNumberOfExternalProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); host.reloadFS([file1, file3]); host.triggerFileWatcherCallback(file2.path, /*removed*/ true); - checkNumberOfInferredProjects(projectService, 2); - checkNumberOfExternalProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); checkProjectActualFiles(projectService.inferredProjects[0], [ file1.path ]); checkProjectActualFiles(projectService.inferredProjects[1], [ file3.path ]); }); + + it("open file become a part of configured project if it is referenced from root file", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "export let x = 5" + }; + const file2 = { + path: "/a/c/f2.ts", + content: `import {x} from "../b/f1"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: "export let y = 1" + }; + const configFile = { + path: "/a/c/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: [ "f2.ts", "f3.ts" ] }) + }; + + const host = createServerHost([file1, file2, file3]); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useSingleInferredProject*/ false); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [ file1.path ]); + + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [ file1.path ]); + checkProjectActualFiles(projectService.inferredProjects[1], [ file3.path ]); + + host.reloadFS([file1, file2, file3, configFile]); + host.triggerDirectoryWatcherCallback(getDirectoryPath(configFile.path), configFile.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(projectService.configuredProjects[0], [ file1.path, file2.path, file3.path ]); + }); }); } \ No newline at end of file From 83afa3fb949cbd603788288b254887fe3f7ea9a8 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 5 Jul 2016 11:48:26 -0700 Subject: [PATCH 084/307] more tests --- src/server/editorServices.ts | 8 ++++- .../cases/unittests/tsserverProjectSystem.ts | 36 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index c91760c3977..292313208b4 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -373,10 +373,16 @@ namespace ts.server { const inferredProject = this.createInferredProjectWithRootFileIfNecessary(info); if (!this.useSingleInferredProject) { // if useOneInferredProject is not set then try to fixup ownership of open files + // check 'defaultProject !== inferredProject' is necessary to handle cases + // when creation inferred project for some file has added other open files into this project (i.e. as referenced files) + // we definitely don't want to delete the project that was just created for (const f of this.openFiles) { + if (f.containingProjects.length === 0) { + // this is orphaned file that we have not processed yet - skip it + continue; + } const defaultProject = f.getDefaultProject(); if (isRootFileInInferredProject(info) && defaultProject !== inferredProject && inferredProject.containsScriptInfo(f)) { - // open file used to be root in inferred project, // this inferred project is different from the one we've just created for current file // and new inferred project references this open file. diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index 5cf7500a196..f10b37bd41f 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -1039,5 +1039,41 @@ namespace ts { checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectActualFiles(projectService.configuredProjects[0], [ file1.path, file2.path, file3.path ]); }); + + it("correctly migrate files between projects", () => { + const file1 = { + path: "/a/b/f1.ts", + content: ` + export * from "../c/f2.ts"; + export * from "../d/f3.ts";` + }; + const file2 = { + path: "/a/c/f2.ts", + content: "export let x = 1;" + }; + const file3 = { + path: "/a/d/f3.ts", + content: "export let y = 1;" + }; + const host = createServerHost([file1, file2, file3]); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useSingleInferredProject*/ false); + + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); + + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectRootFiles(projectService.inferredProjects[0], [file1.path]); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); + + projectService.closeClientFile(file1.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + }); }); } \ No newline at end of file From 9b4823e5361117c9d4c76de23c31d9c6484da854 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 5 Jul 2016 15:51:39 -0700 Subject: [PATCH 085/307] more tests --- src/server/editorServices.ts | 73 +++++---- src/server/project.ts | 4 + .../cases/unittests/tsserverProjectSystem.ts | 146 ++++++++++++++++++ 3 files changed, 194 insertions(+), 29 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 292313208b4..90712e29a1a 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -676,44 +676,59 @@ namespace ts.server { } private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedRootFiles: string[], newOptions: CompilerOptions) { - const oldRootFiles = project.getRootFiles(); - const newFileNames: NormalizedPath[] = []; - for (const f of newUncheckedRootFiles) { - if (this.host.fileExists(f)) { - newFileNames.push(toNormalizedPath(f)); - } - } - const fileNamesToRemove = oldRootFiles.filter(f => !contains(newFileNames, f)); - const fileNamesToAdd = newFileNames.filter(f => !contains(oldRootFiles, f)); + const oldRootScriptInfos = project.getRootScriptInfos(); + const newRootScriptInfos: ScriptInfo[] = []; + const newRootScriptInfoMap: NormalizedPathMap = createNormalizedPathMap(); - for (const fileName of fileNamesToRemove) { - const info = this.getScriptInfoForNormalizedPath(fileName); - if (info) { - project.removeFile(info); + let rootFilesChanged = false; + for (const newRootFile of newUncheckedRootFiles) { + if (!this.host.fileExists(newRootFile)) { + continue; } + const normalizedPath = toNormalizedPath(newRootFile); + let scriptInfo = this.getScriptInfoForNormalizedPath(normalizedPath); + if (!scriptInfo || !project.isRoot(scriptInfo)) { + rootFilesChanged = true; + if (!scriptInfo) { + scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ false); + } + } + newRootScriptInfos.push(scriptInfo); + newRootScriptInfoMap.set(scriptInfo.fileName, scriptInfo); } - for (const fileName of fileNamesToAdd) { - let info = this.getScriptInfoForNormalizedPath(fileName); - if (!info) { - info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false); + if (rootFilesChanged || newRootScriptInfos.length !== oldRootScriptInfos.length) { + let toAdd: ScriptInfo[]; + let toRemove: ScriptInfo[]; + for (const oldFile of oldRootScriptInfos) { + if (!newRootScriptInfoMap.contains(oldFile.fileName)) { + (toRemove || (toRemove = [])).push(oldFile); + } + } + for (const newFile of newRootScriptInfos) { + if (!project.isRoot(newFile)) { + (toAdd || (toAdd = [])).push(newFile); + } } - else { - if (info.isOpen) { - // delete inferred project - let toRemove: Project; - if (isRootFileInInferredProject(info)) { - toRemove = info.containingProjects[0]; - } - if (toRemove) { - toRemove.removeFile(info); - if (!toRemove.hasRoots()) { - this.removeProject(toRemove); + if (toRemove) { + for (const f of toRemove) { + project.removeFile(f); + } + } + if (toAdd) { + for (const f of toAdd) { + if (f.isOpen && isRootFileInInferredProject(f)) { + // if file is already root in some inferred project + // - remove the file from that project and delete the project if necessary + const inferredProject = f.containingProjects[0]; + inferredProject.removeFile(f); + if (!inferredProject.hasRoots()) { + this.removeProject(inferredProject); } } + project.addRoot(f); } } - project.addRoot(info); } project.setCompilerOptions(newOptions); diff --git a/src/server/project.ts b/src/server/project.ts index a7898e39574..f53c3653e69 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -129,6 +129,10 @@ namespace ts.server { return this.rootFiles && this.rootFiles.map(info => info.fileName); } + getRootScriptInfos() { + return this.rootFiles; + } + getFileNames() { if (!this.program) { return []; diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index f10b37bd41f..fb8648649d1 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -1075,5 +1075,151 @@ namespace ts { projectService.closeClientFile(file1.path); checkNumberOfProjects(projectService, { inferredProjects: 2 }); }); + + it("can correctly update configured project when set of root files has changed (new file on disk)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + + const host = createServerHost([file1, configFile]); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useSingleInferredProject*/ false); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(projectService.configuredProjects[0], [ file1.path ]); + + host.reloadFS([file1, file2, configFile]); + + host.triggerDirectoryWatcherCallback(getDirectoryPath(file2.path), file2.path); + host.checkTimeoutQueueLength(1); + host.runQueuedTimeoutCallbacks(); // to execute throttled requests + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectRootFiles(projectService.configuredProjects[0], [ file1.path, file2.path ]); + }); + + it("can correctly update configured project when set of root files has changed (new file in list of files)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: [ "f1.ts" ] }) + }; + + const host = createServerHost([file1, file2, configFile]); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useSingleInferredProject*/ false); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(projectService.configuredProjects[0], [ file1.path ]); + + const modifiedConfigFile = { + path: configFile.path, + content: JSON.stringify({ compilerOptions: {}, files: [ "f1.ts", "f2.ts" ] }) + }; + + host.reloadFS([file1, file2, modifiedConfigFile]); + host.triggerFileWatcherCallback(configFile.path, /*removed*/ false); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectRootFiles(projectService.configuredProjects[0], [ file1.path, file2.path ]); + }); + + it("can update configured project when set of root files was not changed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: [ "f1.ts", "f2.ts" ] }) + }; + + const host = createServerHost([file1, file2, configFile]); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useSingleInferredProject*/ false); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(projectService.configuredProjects[0], [ file1.path, file2.path ]); + + const modifiedConfigFile = { + path: configFile.path, + content: JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: [ "f1.ts", "f2.ts" ] }) + }; + + host.reloadFS([file1, file2, modifiedConfigFile]); + host.triggerFileWatcherCallback(configFile.path, /*removed*/ false); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectRootFiles(projectService.configuredProjects[0], [ file1.path, file2.path ]); + }); + + it("can correctly update external project when set of root files has changed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const host = createServerHost([file1, file2]); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useSingleInferredProject*/ false); + + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: [file1.path] }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [ file1.path ]); + + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: [file1.path, file2.path] }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectRootFiles(projectService.externalProjects[0], [ file1.path, file2.path ]); + }); + + it("can update external project when set of root files was not changed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "m"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: "export let y = 1" + }; + const file3 = { + path: "/a/m.ts", + content: "export let y = 1" + }; + + const host = createServerHost([file1, file2, file3]); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useSingleInferredProject*/ false); + + projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.NodeJs }, rootFiles: [file1.path, file2.path] }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectRootFiles(projectService.externalProjects[0], [ file1.path, file2.path ]); + checkProjectActualFiles(projectService.externalProjects[0], [ file1.path, file2.path ]); + + projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.Classic }, rootFiles: [file1.path, file2.path] }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectRootFiles(projectService.externalProjects[0], [ file1.path, file2.path ]); + checkProjectActualFiles(projectService.externalProjects[0], [ file1.path, file2.path, file3.path ]); + }); }); } \ No newline at end of file From 99f70e443cb9aa3ee1cf78ab1444449769ac66d9 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 5 Jul 2016 15:58:31 -0700 Subject: [PATCH 086/307] more tests --- .../cases/unittests/tsserverProjectSystem.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index fb8648649d1..e883587d812 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -1221,5 +1221,37 @@ namespace ts { checkProjectRootFiles(projectService.externalProjects[0], [ file1.path, file2.path ]); checkProjectActualFiles(projectService.externalProjects[0], [ file1.path, file2.path, file3.path ]); }); + + it("config file is deleted", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1;" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 2;" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const host = createServerHost([file1, file2, config]); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useSingleInferredProject*/ false); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(projectService.configuredProjects[0], [ file1.path, file2.path ]); + + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(projectService.configuredProjects[0], [ file1.path, file2.path ]); + + host.reloadFS([file1, file2]); + host.triggerFileWatcherCallback(config.path, /*removed*/ true); + + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); + }); }); } \ No newline at end of file From cb1e6c0d044089ccb5c2f6a9184d63dfc760f15c Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 5 Jul 2016 16:17:10 -0700 Subject: [PATCH 087/307] renames --- src/server/editorServices.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 90712e29a1a..0568e72a938 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -609,7 +609,7 @@ namespace ts.server { return false; } - private createAndAddExternalProject(projectFileName: string, files: string[], compilerOptions: CompilerOptions) { + private createAndAddExternalProject(projectFileName: string, files: NormalizedPath[], compilerOptions: CompilerOptions) { const project = new ExternalProject( projectFileName, this, @@ -884,11 +884,11 @@ namespace ts.server { this.log("updating project structure from ...", "Info"); this.printProjects(); - const unattachedOpenFiles: ScriptInfo[] = []; + const orphantedFiles: ScriptInfo[] = []; // collect all orphanted script infos from open files for (const info of this.openFiles) { if (info.containingProjects.length === 0) { - unattachedOpenFiles.push(info); + orphantedFiles.push(info); } else { if (isRootFileInInferredProject(info) && info.containingProjects.length > 1) { @@ -901,8 +901,8 @@ namespace ts.server { } } } - for (const unattached of unattachedOpenFiles) { - this.assignScriptInfoToInferredProjectIfNecessary(unattached, /*addToListOfOpenFiles*/ false); + for (const f of orphantedFiles) { + this.assignScriptInfoToInferredProjectIfNecessary(f, /*addToListOfOpenFiles*/ false); } for (const p of this.inferredProjects) { @@ -1001,14 +1001,17 @@ namespace ts.server { const fileName = toNormalizedPath(uncheckedFileName); const configFiles = this.externalProjectToConfiguredProjectMap[fileName]; if (configFiles) { + let shouldRefreshInferredProjects = false; for (const configFile of configFiles) { const configuredProject = this.findConfiguredProjectByProjectName(configFile); if (configuredProject && configuredProject.deleteOpenRef() === 0) { this.removeProject(configuredProject); + shouldRefreshInferredProjects = true; } } - // TODO: do this only if ownership of files is changed - this.refreshInferredProjects(); + if (shouldRefreshInferredProjects) { + this.refreshInferredProjects(); + } } else { // close external project @@ -1026,14 +1029,16 @@ namespace ts.server { this.updateNonInferredProject(externalProject, proj.rootFiles, proj.options); return; } + let tsConfigFiles: NormalizedPath[]; - const rootFiles: string[] = []; + const rootFiles: NormalizedPath[] = []; for (const file of proj.rootFiles) { - if (getBaseFileName(file) === "tsconfig.json") { - (tsConfigFiles || (tsConfigFiles = [])).push(toNormalizedPath(file)); + const normalized = toNormalizedPath(file); + if (getBaseFileName(normalized) === "tsconfig.json") { + (tsConfigFiles || (tsConfigFiles = [])).push(normalized); } else { - rootFiles.push(file); + rootFiles.push(normalized); } } if (tsConfigFiles) { @@ -1053,7 +1058,7 @@ namespace ts.server { } } else { - this.createAndAddExternalProject(proj.projectFileName, proj.rootFiles, proj.options); + this.createAndAddExternalProject(proj.projectFileName, rootFiles, proj.options); } } } From 71a3d0a42f40fc11fd00d31fbbd714b09fe3e219 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 5 Jul 2016 23:14:24 -0700 Subject: [PATCH 088/307] use server side format code options matching arguments is omitted --- src/server/protocol.d.ts | 2 +- src/server/session.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 1dc1cb59609..10b67d38ffe 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -111,7 +111,7 @@ declare namespace ts.server.protocol { } export interface IndentationRequestArgs extends FileLocationRequestArgs { - options: EditorSettings; + options?: EditorSettings; } /** diff --git a/src/server/session.ts b/src/server/session.ts index b741c421859..7368eed1bc0 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -783,7 +783,8 @@ namespace ts.server { private getIndentation(args: protocol.IndentationRequestArgs) { const { file, project } = this.getFileAndProject(args); const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file)); - const indentation = project.languageService.getIndentationAtPosition(file, position, args.options); + const options = args.options || this.projectService.getFormatCodeOptions(file); + const indentation = project.languageService.getIndentationAtPosition(file, position, options); return { position, indentation }; } @@ -855,17 +856,20 @@ namespace ts.server { private getFormattingEditsForRangeFull(args: protocol.FormatRequestArgs) { const { file, project } = this.getFileAndProject(args); - return project.languageService.getFormattingEditsForRange(file, args.position, args.endPosition, args.options); + const options = args.options || this.projectService.getFormatCodeOptions(file); + return project.languageService.getFormattingEditsForRange(file, args.position, args.endPosition, options); } private getFormattingEditsForDocumentFull(args: protocol.FormatRequestArgs) { const { file, project } = this.getFileAndProject(args); - return project.languageService.getFormattingEditsForDocument(file, args.options); + const options = args.options || this.projectService.getFormatCodeOptions(file); + return project.languageService.getFormattingEditsForDocument(file, options); } private getFormattingEditsAfterKeystrokeFull(args: protocol.FormatOnKeyRequestArgs) { const { file, project } = this.getFileAndProject(args); - return project.languageService.getFormattingEditsAfterKeystroke(file, args.position, args.key, args.options); + const options = args.options || this.projectService.getFormatCodeOptions(file); + return project.languageService.getFormattingEditsAfterKeystroke(file, args.position, args.key, options); } private getFormattingEditsAfterKeystroke(args: protocol.FormatOnKeyRequestArgs): protocol.CodeEdit[] { From e14a7ca0bc4adac8ed16b978d6bd4f025c7849cd Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 6 Jul 2016 00:28:24 -0700 Subject: [PATCH 089/307] initial support for compressing responses --- src/harness/harness.ts | 4 ++ src/harness/harnessLanguageService.ts | 5 +- src/server/node.d.ts | 9 +++ src/server/protocol.d.ts | 2 + src/server/server.ts | 27 +++++++-- src/server/session.ts | 55 ++++++++++++++----- .../cases/unittests/cachingInServerLSHost.ts | 3 + tests/cases/unittests/session.ts | 19 ++++--- .../cases/unittests/tsserverProjectSystem.ts | 1 + 9 files changed, 94 insertions(+), 31 deletions(-) diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 6f895c37b19..8726bf69da5 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -70,6 +70,10 @@ namespace Utils { return Buffer ? Buffer.byteLength(s, encoding) : s.length; } + export function compress(s: string): any { + return Buffer ? new Buffer(s, "utf8") : { s, length: s.length }; + } + export function evalFile(fileContents: string, fileName: string, nodeContext?: any) { const environment = getExecutionEnvironment(); switch (environment) { diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 3e431c73f9e..7ca3c832377 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -572,6 +572,8 @@ namespace Harness.LanguageService { this.writeMessage(message); } + writeCompressedData() { + } readFile(fileName: string): string { if (fileName.indexOf(Harness.Compiler.defaultLibFileName) >= 0) { @@ -690,7 +692,8 @@ namespace Harness.LanguageService { const server = new ts.server.Session(serverHost, { isCancellationRequested: () => false }, /*useOneInferredProject*/ false, - Buffer ? Buffer.byteLength : (string: string, encoding?: string) => string.length, + Utils.byteLength, + Utils.compress, process.hrtime, serverHost); // Fake the connection between the client and the server diff --git a/src/server/node.d.ts b/src/server/node.d.ts index 0bde0bb6602..8ac1f1f19b5 100644 --- a/src/server/node.d.ts +++ b/src/server/node.d.ts @@ -397,6 +397,15 @@ declare namespace NodeJS { } } +declare namespace NodeJS { + namespace zlib { + export interface GZip { + gzipSync(buf: Buffer): Buffer; + } + export function createGZip(): GZip; + } +} + declare namespace NodeJS { namespace fs { interface Stats { diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 10b67d38ffe..3bef3e600eb 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -30,6 +30,8 @@ declare namespace ts.server.protocol { * Object containing arguments for the command */ arguments?: any; + + canCompressResponse?: boolean; } /** diff --git a/src/server/server.ts b/src/server/server.ts index c9f807778c1..1ac6a0c7cbb 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -4,8 +4,10 @@ /* tslint:disable:no-null-keyword */ namespace ts.server { + const readline: NodeJS.ReadLine = require("readline"); const fs: typeof NodeJS.fs = require("fs"); + const zlib: typeof NodeJS.zlib = require("zlib"); const rl = readline.createInterface({ input: process.stdin, @@ -13,6 +15,11 @@ namespace ts.server { terminal: false, }); + function compress(s: string): CompressedData { + const gzip = zlib.createGZip(); + return gzip.gzipSync(new Buffer(s, "utf8")); + } + class Logger implements ts.server.Logger { private fd = -1; private seq = 0; @@ -92,7 +99,7 @@ namespace ts.server { class IOSession extends Session { constructor(host: ServerHost, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, logger: ts.server.Logger) { - super(host, cancellationToken, useSingleInferredProject, Buffer.byteLength, process.hrtime, logger); + super(host, cancellationToken, useSingleInferredProject, Buffer.byteLength, compress, process.hrtime, logger); } exit() { @@ -260,15 +267,16 @@ namespace ts.server { const pollingWatchedFileSet = createPollingWatchedFileSet(); const logger = createLoggerFromEnv(); - const pending: string[] = []; + const pending: Buffer[] = []; let canWrite = true; - function writeMessage(s: string) { + + function writeMessage(buf: Buffer) { if (!canWrite) { - pending.push(s); + pending.push(buf); } else { canWrite = false; - process.stdout.write(new Buffer(s, "utf8"), setCanWriteFlagAndWriteMessageIfNecessary); + process.stdout.write(buf, setCanWriteFlagAndWriteMessageIfNecessary); } } @@ -279,10 +287,16 @@ namespace ts.server { } } + function writeCompressedData(prefix: string, compressed: CompressedData, suffix: string): void { + sys.write(prefix); + writeMessage(compressed); + sys.write(suffix); + } + const sys = ts.sys; // Override sys.write because fs.writeSync is not reliable on Node 4 - sys.write = (s: string) => writeMessage(s); + sys.write = (s: string) => writeMessage(new Buffer(s, "utf8")); sys.watchFile = (fileName, callback) => { const watchedFile = pollingWatchedFileSet.addFile(fileName, callback); return { @@ -294,6 +308,7 @@ namespace ts.server { sys.clearTimeout = clearTimeout; sys.setImmediate = setImmediate; sys.clearImmediate = clearImmediate; + sys.writeCompressedData = writeCompressedData; let cancellationToken: HostCancellationToken; try { diff --git a/src/server/session.ts b/src/server/session.ts index 7368eed1bc0..855097ef86b 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -10,6 +10,15 @@ namespace ts.server { stack?: string; } + export interface CompressedData { + __compressedDataTag: any; + length: number; + } + + export interface ServerHost { + writeCompressedData(prefix: string, data: CompressedData, suffix: string): void; + } + export function generateSpaces(n: number): string { if (!spaceCache[n]) { let strBuilder = ""; @@ -183,6 +192,7 @@ namespace ts.server { cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, private byteLength: (buf: string, encoding?: string) => number, + private compress: (s: string) => CompressedData, private hrtime: (start?: number[]) => number[], private logger: Logger) { this.projectService = @@ -215,13 +225,27 @@ namespace ts.server { this.host.write(line + this.host.newLine); } - public send(msg: protocol.Message) { + private sendCompressedDataToClient(prefix: string, data: CompressedData) { + this.host.writeCompressedData(prefix, data, this.host.newLine); + } + + public send(msg: protocol.Message, canCompressResponse: boolean) { const json = JSON.stringify(msg); if (this.logger.isVerbose()) { this.logger.info(msg.type + ": " + json); } - this.sendLineToClient("Content-Length: " + (1 + this.byteLength(json, "utf8")) + - "\r\n\r\n" + json); + const len = this.byteLength(json, "utf8"); + if (len < 84000 || !canCompressResponse) { + this.sendLineToClient("Content-Length: " + (1 + this.byteLength(json, "utf8")) + "\r\n\r\n" + json); + } + else { + // TODO: measure time + const compressed = this.compress(json); + if (this.logger.isVerbose()) { + this.logger.info(`compressed message to ${compressed.length}`); + } + this.sendCompressedDataToClient(`Content-Length: ${compressed.length + 1}`, compressed); + } } public configFileDiagnosticEvent(triggerFile: string, configFile: string, diagnostics: ts.Diagnostic[]) { @@ -236,7 +260,7 @@ namespace ts.server { diagnostics: bakedDiags } }; - this.send(ev); + this.send(ev, /*canCompressResponse*/ false); } public event(info: any, eventName: string) { @@ -246,10 +270,10 @@ namespace ts.server { event: eventName, body: info, }; - this.send(ev); + this.send(ev, /*canCompressResponse*/ false); } - private response(info: any, cmdName: string, reqSeq = 0, errorMsg?: string) { + private response(info: any, cmdName: string, canCompressResponse: boolean, reqSeq = 0, errorMsg?: string) { const res: protocol.Response = { seq: 0, type: "response", @@ -263,11 +287,11 @@ namespace ts.server { else { res.message = errorMsg; } - this.send(res); + this.send(res, canCompressResponse); } - public output(body: any, commandName: string, requestSequence = 0, errorMessage?: string) { - this.response(body, commandName, requestSequence, errorMessage); + public output(body: any, commandName: string, canCompressResponse: boolean, requestSequence = 0, errorMessage?: string) { + this.response(body, commandName, canCompressResponse, requestSequence, errorMessage); } private getLocation(position: number, scriptInfo: ScriptInfo): protocol.Location { @@ -1030,7 +1054,7 @@ namespace ts.server { this.changeSeq++; // make sure no changes happen before this one is finished if (project.reloadScript(file)) { - this.output(undefined, CommandNames.Reload, reqSeq); + this.output(undefined, CommandNames.Reload, /*canCompressResponse*/ false, reqSeq); } } } @@ -1407,7 +1431,7 @@ namespace ts.server { }, [CommandNames.Configure]: (request: protocol.ConfigureRequest) => { this.projectService.setHostConfiguration(request.arguments); - this.output(undefined, CommandNames.Configure, request.seq); + this.output(undefined, CommandNames.Configure, /*canCompressResponse*/ false, request.seq); return this.notRequired(); }, [CommandNames.Reload]: (request: protocol.ReloadRequest) => { @@ -1473,7 +1497,7 @@ namespace ts.server { } else { this.projectService.log("Unrecognized JSON command: " + JSON.stringify(request)); - this.output(undefined, CommandNames.Unknown, request.seq, "Unrecognized JSON command: " + request.command); + this.output(undefined, CommandNames.Unknown, /*canCompressResponse*/ false, request.seq, "Unrecognized JSON command: " + request.command); return { responseRequired: false }; } } @@ -1501,22 +1525,23 @@ namespace ts.server { this.logger.msg(leader + ": " + elapsedMs.toFixed(4).toString(), "Perf"); } if (response) { - this.output(response, request.command, request.seq); + this.output(response, request.command, request.canCompressResponse, request.seq); } else if (responseRequired) { - this.output(undefined, request.command, request.seq, "No content available."); + this.output(undefined, request.command, /*canCompressResponse*/ false, request.seq, "No content available."); } } catch (err) { if (err instanceof OperationCanceledException) { // Handle cancellation exceptions - this.output({ canceled: true }, request.command, request.seq); + this.output({ canceled: true }, request.command, /*canCompressResponse*/ false, request.seq); return; } this.logError(err, message); this.output( undefined, request ? request.command : CommandNames.Unknown, + /*canCompressResponse*/ false, request ? request.seq : 0, "Error processing request. " + (err).message + "\n" + (err).stack); } diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index 888618fcefb..f3fb83e6ffc 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -29,6 +29,9 @@ namespace ts { writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => { throw new Error("NYI"); }, + writeCompressedData() { + throw new Error("NYI"); + }, resolvePath: (path: string): string => { throw new Error("NYI"); }, diff --git a/tests/cases/unittests/session.ts b/tests/cases/unittests/session.ts index 90343f4699d..3eb928a0462 100644 --- a/tests/cases/unittests/session.ts +++ b/tests/cases/unittests/session.ts @@ -23,7 +23,8 @@ namespace ts.server { setTimeout(callback, ms, ...args) { return 0; }, clearTimeout(timeoutId) { }, setImmediate: () => 0, - clearImmediate() {} + clearImmediate() {}, + writeCompressedData() {} }; const nullCancellationToken: HostCancellationToken = { isCancellationRequested: () => false }; const mockLogger: Logger = { @@ -42,7 +43,7 @@ namespace ts.server { let lastSent: protocol.Message; beforeEach(() => { - session = new Session(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, process.hrtime, mockLogger); + session = new Session(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, Utils.compress, process.hrtime, mockLogger); session.send = (msg: protocol.Message) => { lastSent = msg; }; @@ -180,7 +181,7 @@ namespace ts.server { session.send = Session.prototype.send; assert(session.send); - expect(session.send(msg)).to.not.exist; + expect(session.send(msg, /*canCompressResponse*/ false)).to.not.exist; expect(lastWrittenToHost).to.equal(resultMsg); }); }); @@ -248,7 +249,7 @@ namespace ts.server { }; const command = "test"; - session.output(body, command); + session.output(body, command, /*canCompressResponse*/ false); expect(lastSent).to.deep.equal({ seq: 0, @@ -267,7 +268,7 @@ namespace ts.server { lastSent: protocol.Message; customHandler = "testhandler"; constructor() { - super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, process.hrtime, mockLogger); + super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, Utils.compress, process.hrtime, mockLogger); this.addProtocolHandler(this.customHandler, () => { return { response: undefined, responseRequired: true }; }); @@ -286,7 +287,7 @@ namespace ts.server { }; const command = "test"; - session.output(body, command); + session.output(body, command, /*canCompressResponse*/ false); expect(session.lastSent).to.deep.equal({ seq: 0, @@ -325,7 +326,7 @@ namespace ts.server { class InProcSession extends Session { private queue: protocol.Request[] = []; constructor(private client: InProcClient) { - super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, process.hrtime, mockLogger); + super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, Utils.compress, process.hrtime, mockLogger); this.addProtocolHandler("echo", (req: protocol.Request) => ({ response: req.arguments, responseRequired: true @@ -346,11 +347,11 @@ namespace ts.server { ({ response } = this.executeCommand(msg)); } catch (e) { - this.output(undefined, msg.command, msg.seq, e.toString()); + this.output(undefined, msg.command, /*canCompressResponse*/ false, msg.seq, e.toString()); return; } if (response) { - this.output(response, msg.command, msg.seq); + this.output(response, msg.command, /*canCompressResponse*/ false, msg.seq); } } diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index e883587d812..b5a5f3aef54 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -366,6 +366,7 @@ namespace ts { readonly getExecutingFilePath = () => this.executingFilePath; readonly getCurrentDirectory = () => this.currentDirectory; readonly writeFile = (path: string, content: string) => notImplemented(); + readonly writeCompressedData = () => notImplemented(); readonly write = (s: string) => notImplemented(); readonly createDirectory = (s: string) => notImplemented(); readonly exit = () => notImplemented(); From 1060b1e52e6d983771361b5b897b152eb0d8f38c Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 6 Jul 2016 14:10:39 -0700 Subject: [PATCH 090/307] make compressed data wrapper over the real data --- src/server/server.ts | 5 +++-- src/server/session.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index 1ac6a0c7cbb..fda747e6dec 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -17,7 +17,8 @@ namespace ts.server { function compress(s: string): CompressedData { const gzip = zlib.createGZip(); - return gzip.gzipSync(new Buffer(s, "utf8")); + const data = gzip.gzipSync(new Buffer(s, "utf8")); + return { data, length: data.length }; } class Logger implements ts.server.Logger { @@ -289,7 +290,7 @@ namespace ts.server { function writeCompressedData(prefix: string, compressed: CompressedData, suffix: string): void { sys.write(prefix); - writeMessage(compressed); + writeMessage(compressed.data); sys.write(suffix); } diff --git a/src/server/session.ts b/src/server/session.ts index 855097ef86b..ad226d19fb2 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -11,8 +11,8 @@ namespace ts.server { } export interface CompressedData { - __compressedDataTag: any; length: number; + data: any; } export interface ServerHost { From b330fbf03ca77de28a73040882a799f6f781321b Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 6 Jul 2016 17:22:55 -0700 Subject: [PATCH 091/307] report compression time --- src/server/node.d.ts | 5 +---- src/server/server.ts | 5 ++--- src/server/session.ts | 19 +++++++++++++------ 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/server/node.d.ts b/src/server/node.d.ts index 8ac1f1f19b5..8d5a345bd5e 100644 --- a/src/server/node.d.ts +++ b/src/server/node.d.ts @@ -399,10 +399,7 @@ declare namespace NodeJS { declare namespace NodeJS { namespace zlib { - export interface GZip { - gzipSync(buf: Buffer): Buffer; - } - export function createGZip(): GZip; + export function gzipSync(buffer: Buffer): Buffer; } } diff --git a/src/server/server.ts b/src/server/server.ts index fda747e6dec..6be185b148f 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -16,9 +16,8 @@ namespace ts.server { }); function compress(s: string): CompressedData { - const gzip = zlib.createGZip(); - const data = gzip.gzipSync(new Buffer(s, "utf8")); - return { data, length: data.length }; + const data = zlib.gzipSync(new Buffer(s, "utf8")); + return { data, length: data.length, compressionKind: "gzip" }; } class Logger implements ts.server.Logger { diff --git a/src/server/session.ts b/src/server/session.ts index ad226d19fb2..c204998faf6 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -12,9 +12,16 @@ namespace ts.server { export interface CompressedData { length: number; + compressionKind: string; data: any; } + function hrTimeToMilliseconds(time: number[]): number { + const seconds = time[0]; + const nanoseconds = time[1]; + return ((1e9 * seconds) + nanoseconds) / 1000000.0; + } + export interface ServerHost { writeCompressedData(prefix: string, data: CompressedData, suffix: string): void; } @@ -239,12 +246,14 @@ namespace ts.server { this.sendLineToClient("Content-Length: " + (1 + this.byteLength(json, "utf8")) + "\r\n\r\n" + json); } else { - // TODO: measure time + const start = this.hrtime(); const compressed = this.compress(json); + var elapsed = this.hrtime(start); + if (this.logger.isVerbose()) { - this.logger.info(`compressed message to ${compressed.length}`); + this.logger.info(`compressed message ${json.length} to ${compressed.length} in ${hrTimeToMilliseconds(elapsed)} ms using ${compressed.compressionKind}`); } - this.sendCompressedDataToClient(`Content-Length: ${compressed.length + 1}`, compressed); + this.sendCompressedDataToClient(`Content-Length: ${compressed.length + 1} ${compressed.compressionKind}\r\n\r\n`, compressed); } } @@ -1515,9 +1524,7 @@ namespace ts.server { if (this.logger.isVerbose()) { const elapsed = this.hrtime(start); - const seconds = elapsed[0]; - const nanoseconds = elapsed[1]; - const elapsedMs = ((1e9 * seconds) + nanoseconds) / 1000000.0; + const elapsedMs = hrTimeToMilliseconds(elapsed); let leader = "Elapsed time (in milliseconds)"; if (!responseRequired) { leader = "Async elapsed time (in milliseconds)"; From 2333c68eecb598e731f018e4a7d22921dcf34d83 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 8 Jul 2016 12:10:57 -0700 Subject: [PATCH 092/307] fix linter --- src/server/session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/session.ts b/src/server/session.ts index c204998faf6..878f501c783 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -248,7 +248,7 @@ namespace ts.server { else { const start = this.hrtime(); const compressed = this.compress(json); - var elapsed = this.hrtime(start); + const elapsed = this.hrtime(start); if (this.logger.isVerbose()) { this.logger.info(`compressed message ${json.length} to ${compressed.length} in ${hrTimeToMilliseconds(elapsed)} ms using ${compressed.compressionKind}`); From 6c775ebf97f714c856f2cabf14e63d4151b026b2 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 8 Jul 2016 16:47:51 -0700 Subject: [PATCH 093/307] move max uncompressed message size out of the session --- src/harness/harness.ts | 4 +++- src/harness/harnessLanguageService.ts | 1 + src/server/server.ts | 4 +++- src/server/session.ts | 1 + tests/cases/unittests/session.ts | 6 +++--- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 8726bf69da5..04bfbba4266 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -71,9 +71,11 @@ namespace Utils { } export function compress(s: string): any { - return Buffer ? new Buffer(s, "utf8") : { s, length: s.length }; + return Buffer ? new Buffer(s, "utf8") : { data: s, length: s.length }; } + export const maxUncompressedMessageSize = Number.MAX_VALUE; + export function evalFile(fileContents: string, fileName: string, nodeContext?: any) { const environment = getExecutionEnvironment(); switch (environment) { diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 7ca3c832377..e6e27582925 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -693,6 +693,7 @@ namespace Harness.LanguageService { { isCancellationRequested: () => false }, /*useOneInferredProject*/ false, Utils.byteLength, + Utils.maxUncompressedMessageSize, Utils.compress, process.hrtime, serverHost); diff --git a/src/server/server.ts b/src/server/server.ts index 6be185b148f..b072b2516a4 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -20,6 +20,8 @@ namespace ts.server { return { data, length: data.length, compressionKind: "gzip" }; } + const maxUncompressedMessageSize = 84000; + class Logger implements ts.server.Logger { private fd = -1; private seq = 0; @@ -99,7 +101,7 @@ namespace ts.server { class IOSession extends Session { constructor(host: ServerHost, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, logger: ts.server.Logger) { - super(host, cancellationToken, useSingleInferredProject, Buffer.byteLength, compress, process.hrtime, logger); + super(host, cancellationToken, useSingleInferredProject, Buffer.byteLength, maxUncompressedMessageSize, compress, process.hrtime, logger); } exit() { diff --git a/src/server/session.ts b/src/server/session.ts index 878f501c783..a71704c176c 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -199,6 +199,7 @@ namespace ts.server { cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, private byteLength: (buf: string, encoding?: string) => number, + private maxUncompressedMessageSize: number, private compress: (s: string) => CompressedData, private hrtime: (start?: number[]) => number[], private logger: Logger) { diff --git a/tests/cases/unittests/session.ts b/tests/cases/unittests/session.ts index 3eb928a0462..3aebd28df9a 100644 --- a/tests/cases/unittests/session.ts +++ b/tests/cases/unittests/session.ts @@ -43,7 +43,7 @@ namespace ts.server { let lastSent: protocol.Message; beforeEach(() => { - session = new Session(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, Utils.compress, process.hrtime, mockLogger); + session = new Session(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, Utils.maxUncompressedMessageSize, Utils.compress, process.hrtime, mockLogger); session.send = (msg: protocol.Message) => { lastSent = msg; }; @@ -268,7 +268,7 @@ namespace ts.server { lastSent: protocol.Message; customHandler = "testhandler"; constructor() { - super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, Utils.compress, process.hrtime, mockLogger); + super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, Utils.maxUncompressedMessageSize, Utils.compress, process.hrtime, mockLogger); this.addProtocolHandler(this.customHandler, () => { return { response: undefined, responseRequired: true }; }); @@ -326,7 +326,7 @@ namespace ts.server { class InProcSession extends Session { private queue: protocol.Request[] = []; constructor(private client: InProcClient) { - super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, Utils.compress, process.hrtime, mockLogger); + super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, Utils.maxUncompressedMessageSize, Utils.compress, process.hrtime, mockLogger); this.addProtocolHandler("echo", (req: protocol.Request) => ({ response: req.arguments, responseRequired: true From 9267b24f56a1da9c9426062016dcc501595c75f0 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 8 Jul 2016 17:49:50 -0700 Subject: [PATCH 094/307] set max size for uncompressed messages --- src/server/session.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index a71704c176c..2d37f139fea 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -243,15 +243,14 @@ namespace ts.server { this.logger.info(msg.type + ": " + json); } const len = this.byteLength(json, "utf8"); - if (len < 84000 || !canCompressResponse) { + if (len < this.maxUncompressedMessageSize || !canCompressResponse) { this.sendLineToClient("Content-Length: " + (1 + this.byteLength(json, "utf8")) + "\r\n\r\n" + json); } else { - const start = this.hrtime(); + const start = this.logger.isVerbose() && this.hrtime(); const compressed = this.compress(json); - const elapsed = this.hrtime(start); - if (this.logger.isVerbose()) { + const elapsed = this.hrtime(start); this.logger.info(`compressed message ${json.length} to ${compressed.length} in ${hrTimeToMilliseconds(elapsed)} ms using ${compressed.compressionKind}`); } this.sendCompressedDataToClient(`Content-Length: ${compressed.length + 1} ${compressed.compressionKind}\r\n\r\n`, compressed); From 09a978538cff30003ef7b284da0cc64929ef4e16 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 11 Jul 2016 13:42:17 -0700 Subject: [PATCH 095/307] use indentation from ts.formattting --- src/server/session.ts | 90 +++++++++---------------------------------- 1 file changed, 18 insertions(+), 72 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 2d37f139fea..a9648bd8d9e 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -4,8 +4,6 @@ /// namespace ts.server { - const spaceCache: string[] = []; - interface StackTraceError extends Error { stack?: string; } @@ -22,50 +20,21 @@ namespace ts.server { return ((1e9 * seconds) + nanoseconds) / 1000000.0; } - export interface ServerHost { + export interface ServerHost extends System { + setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; + clearTimeout(timeoutId: any): void; + setImmediate(callback: (...args: any[]) => void, ...args: any[]): any; + clearImmediate(timeoutId: any): void; writeCompressedData(prefix: string, data: CompressedData, suffix: string): void; } - export function generateSpaces(n: number): string { - if (!spaceCache[n]) { - let strBuilder = ""; - for (let i = 0; i < n; i++) { - strBuilder += " "; - } - spaceCache[n] = strBuilder; - } - return spaceCache[n]; - } - - export function generateIndentString(n: number, editorOptions: EditorSettings): string { - if (editorOptions.convertTabsToSpaces) { - return generateSpaces(n); - } - else { - let result = ""; - for (let i = 0; i < Math.floor(n / editorOptions.tabSize); i++) { - result += "\t"; - } - for (let i = 0; i < n % editorOptions.tabSize; i++) { - result += " "; - } - return result; - } - } - interface FileStart { file: string; start: ILineInfo; } function compareNumber(a: number, b: number) { - if (a < b) { - return -1; - } - else if (a === b) { - return 0; - } - else return 1; + return a - b; } function compareFileStart(a: FileStart, b: FileStart) { @@ -107,8 +76,8 @@ namespace ts.server { } function allEditsBeforePos(edits: ts.TextChange[], pos: number) { - for (let i = 0, len = edits.length; i < len; i++) { - if (ts.textSpanEnd(edits[i].span) >= pos) { + for (const edit of edits) { + if (textSpanEnd(edit.span) >= pos) { return false; } } @@ -181,13 +150,6 @@ namespace ts.server { export const ProjectLanguageServiceDisabled = new Error("The project's language service is disabled."); } - export interface ServerHost extends System { - setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; - clearTimeout(timeoutId: any): void; - setImmediate(callback: (...args: any[]) => void, ...args: any[]): any; - clearImmediate(timeoutId: any): void; - } - export class Session { protected projectService: ProjectService; private errorTimer: any; /*NodeJS.Timer | number*/ @@ -218,33 +180,25 @@ namespace ts.server { } public logError(err: Error, cmd: string) { - const typedErr = err; let msg = "Exception on executing command " + cmd; - if (typedErr.message) { - msg += ":\n" + typedErr.message; - if (typedErr.stack) { - msg += "\n" + typedErr.stack; + if (err.message) { + msg += ":\n" + err.message; + if ((err).stack) { + msg += "\n" + (err).stack; } } this.projectService.log(msg); } - private sendLineToClient(line: string) { - this.host.write(line + this.host.newLine); - } - - private sendCompressedDataToClient(prefix: string, data: CompressedData) { - this.host.writeCompressedData(prefix, data, this.host.newLine); - } - public send(msg: protocol.Message, canCompressResponse: boolean) { const json = JSON.stringify(msg); if (this.logger.isVerbose()) { this.logger.info(msg.type + ": " + json); } + const len = this.byteLength(json, "utf8"); if (len < this.maxUncompressedMessageSize || !canCompressResponse) { - this.sendLineToClient("Content-Length: " + (1 + this.byteLength(json, "utf8")) + "\r\n\r\n" + json); + this.host.write(`Content-Length: ${1 + this.byteLength(json, "utf8")}\r\n\r\n${json}${this.host.newLine}`); } else { const start = this.logger.isVerbose() && this.hrtime(); @@ -253,7 +207,7 @@ namespace ts.server { const elapsed = this.hrtime(start); this.logger.info(`compressed message ${json.length} to ${compressed.length} in ${hrTimeToMilliseconds(elapsed)} ms using ${compressed.compressionKind}`); } - this.sendCompressedDataToClient(`Content-Length: ${compressed.length + 1} ${compressed.compressionKind}\r\n\r\n`, compressed); + this.host.writeCompressedData(`Content-Length: ${compressed.length + 1} ${compressed.compressionKind}\r\n\r\n`, compressed, this.host.newLine); } } @@ -282,7 +236,7 @@ namespace ts.server { this.send(ev, /*canCompressResponse*/ false); } - private response(info: any, cmdName: string, canCompressResponse: boolean, reqSeq = 0, errorMsg?: string) { + public output(info: any, cmdName: string, canCompressResponse: boolean, reqSeq = 0, errorMsg?: string) { const res: protocol.Response = { seq: 0, type: "response", @@ -299,10 +253,6 @@ namespace ts.server { this.send(res, canCompressResponse); } - public output(body: any, commandName: string, canCompressResponse: boolean, requestSequence = 0, errorMessage?: string) { - this.response(body, commandName, canCompressResponse, requestSequence, errorMessage); - } - private getLocation(position: number, scriptInfo: ScriptInfo): protocol.Location { const { line, offset } = scriptInfo.positionToLineOffset(position); return { line, offset: offset + 1 }; @@ -335,10 +285,6 @@ namespace ts.server { } } - private reloadProjects() { - this.projectService.reloadProjects(); - } - private updateProjectStructure(seq: number, matchSeq: (seq: number) => boolean, ms = 1500) { this.host.setTimeout(() => { if (matchSeq(seq)) { @@ -942,7 +888,7 @@ namespace ts.server { const firstNoWhiteSpacePosition = lineInfo.offset + i; edits.push({ span: ts.createTextSpanFromBounds(lineInfo.offset, firstNoWhiteSpacePosition), - newText: generateIndentString(preferredIndent, formatOptions) + newText: formatting.getIndentationString(preferredIndent, formatOptions) }); } } @@ -1488,7 +1434,7 @@ namespace ts.server { return this.requiredResponse(this.getProjectInfo(request.arguments)); }, [CommandNames.ReloadProjects]: (request: protocol.ReloadProjectsRequest) => { - this.reloadProjects(); + this.projectService.reloadProjects(); return this.notRequired(); } }; From ac9717dc3d628f082a0f8193f96e45c44465f8d2 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 11 Jul 2016 19:44:23 -0700 Subject: [PATCH 096/307] use Logger directly --- src/server/editorServices.ts | 38 ++++++++++++++++-------------------- src/server/project.ts | 4 ++-- src/server/server.ts | 8 ++++---- src/server/session.ts | 14 ++++++------- src/server/utilities.ts | 12 +++++++++++- 5 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 0568e72a938..44c28dbb5a1 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -86,7 +86,7 @@ namespace ts.server { // if the ref count for this directory watcher drops to 0, it's time to close it this.directoryWatchersRefCount[directory]--; if (this.directoryWatchersRefCount[directory] === 0) { - this.projectService.log(`Close directory watcher for: ${directory}`); + this.projectService.logger.info(`Close directory watcher for: ${directory}`); this.directoryWatchersForTsconfig[directory].close(); delete this.directoryWatchersForTsconfig[directory]; } @@ -97,7 +97,7 @@ namespace ts.server { let parentPath = getDirectoryPath(currentPath); while (currentPath != parentPath) { if (!this.directoryWatchersForTsconfig[currentPath]) { - this.projectService.log(`Add watcher for: ${currentPath}`); + this.projectService.logger.info(`Add watcher for: ${currentPath}`); this.directoryWatchersForTsconfig[currentPath] = this.projectService.host.watchDirectory(currentPath, callback); this.directoryWatchersRefCount[currentPath] = 1; } @@ -279,7 +279,7 @@ namespace ts.server { return; } - this.log(`Detected source file changes: ${fileName}`); + this.logger.info(`Detected source file changes: ${fileName}`); this.throttledOperations.schedule( project.configFileName, /*delay*/250, @@ -306,7 +306,7 @@ namespace ts.server { } private onConfigChangedForConfiguredProject(project: ConfiguredProject) { - this.log(`Config file changed: ${project.configFileName}`); + this.logger.info(`Config file changed: ${project.configFileName}`); this.updateConfiguredProject(project); this.refreshInferredProjects(); } @@ -317,11 +317,11 @@ namespace ts.server { private onConfigFileAddedForInferredProject(fileName: string) { // TODO: check directory separators if (getBaseFileName(fileName) != "tsconfig.json") { - this.log(`${fileName} is not tsconfig.json`); + this.logger.info(`${fileName} is not tsconfig.json`); return; } - this.log(`Detected newly added tsconfig file: ${fileName}`); + this.logger.info(`Detected newly added tsconfig file: ${fileName}`); this.reloadProjects(); } @@ -331,7 +331,7 @@ namespace ts.server { } private removeProject(project: Project) { - this.log(`remove project: ${project.getRootFiles().toString()}`); + this.logger.info(`remove project: ${project.getRootFiles().toString()}`); project.close(); @@ -460,16 +460,16 @@ namespace ts.server { */ private openOrUpdateConfiguredProjectForFile(fileName: NormalizedPath): OpenConfiguredProjectResult { const searchPath = getDirectoryPath(fileName); - this.log(`Search path: ${searchPath}`, "Info"); + this.logger.info(`Search path: ${searchPath}`); // check if this file is already included in one of external projects const configFileName = this.findConfigFile(asNormalizedPath(searchPath)); if (!configFileName) { - this.log("No config files found."); + this.logger.info("No config files found."); return {}; } - this.log(`Config file name: ${configFileName}`, "Info"); + this.logger.info(`Config file name: ${configFileName}`); const project = this.findConfiguredProjectByProjectName(configFileName); if (!project) { @@ -480,7 +480,7 @@ namespace ts.server { // even if opening config file was successful, it could still // contain errors that were tolerated. - this.log(`Opened configuration file ${configFileName}`, "Info"); + this.logger.info(`Opened configuration file ${configFileName}`); if (errors && errors.length > 0) { return { configFileName, configFileErrors: errors }; } @@ -737,7 +737,7 @@ namespace ts.server { private updateConfiguredProject(project: ConfiguredProject) { if (!this.host.fileExists(project.configFileName)) { - this.log("Config file deleted"); + this.logger.info("Config file deleted"); this.removeProject(project); return; } @@ -835,26 +835,22 @@ namespace ts.server { return this.filenameToScriptInfo.get(fileName); } - log(msg: string, type = "Err") { - this.logger.msg(msg, type); - } - setHostConfiguration(args: protocol.ConfigureRequestArguments) { if (args.file) { const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(args.file)); if (info) { info.setFormatOptions(args.formatOptions); - this.log(`Host configuration update for file ${args.file}`, "Info"); + this.logger.info(`Host configuration update for file ${args.file}`); } } else { if (args.hostInfo !== undefined) { this.hostConfiguration.hostInfo = args.hostInfo; - this.log(`Host information ${args.hostInfo}`, "Info"); + this.logger.info(`Host information ${args.hostInfo}`); } if (args.formatOptions) { mergeMaps(this.hostConfiguration.formatCodeOptions, args.formatOptions); - this.log("Format host information updated", "Info"); + this.logger.info("Format host information updated"); } } } @@ -867,7 +863,7 @@ namespace ts.server { * This function rebuilds the project for every file opened by the client */ reloadProjects() { - this.log("reload projects."); + this.logger.info("reload projects."); // try to reload config file for all open files for (const info of this.openFiles) { this.openOrUpdateConfiguredProjectForFile(info.fileName); @@ -881,7 +877,7 @@ namespace ts.server { * up to date. */ refreshInferredProjects() { - this.log("updating project structure from ...", "Info"); + this.logger.info("updating project structure from ..."); this.printProjects(); const orphantedFiles: ScriptInfo[] = []; diff --git a/src/server/project.ts b/src/server/project.ts index f53c3653e69..92f1d097b36 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -406,7 +406,7 @@ namespace ts.server { } const directoryToWatch = getDirectoryPath(this.configFileName); - this.projectService.log(`Add recursive watcher for: ${directoryToWatch}`); + this.projectService.logger.info(`Add recursive watcher for: ${directoryToWatch}`); this.directoryWatcher = this.projectService.host.watchDirectory(directoryToWatch, path => callback(this, path), /*recursive*/ true); } @@ -418,7 +418,7 @@ namespace ts.server { this.directoriesWatchedForWildcards = reduceProperties(this.wildcardDirectories, (watchers, flag, directory) => { if (comparePaths(configDirectoryPath, directory, ".", !this.projectService.host.useCaseSensitiveFileNames) !== Comparison.EqualTo) { const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; - this.projectService.log(`Add ${recursive ? "recursive " : ""}watcher for: ${directory}`); + this.projectService.logger.info(`Add ${recursive ? "recursive " : ""}watcher for: ${directory}`); watchers[directory] = this.projectService.host.watchDirectory( directory, path => callback(this, path), diff --git a/src/server/server.ts b/src/server/server.ts index b072b2516a4..1a101f9d030 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -44,11 +44,11 @@ namespace ts.server { } perftrc(s: string) { - this.msg(s, "Perf"); + this.msg(s, Msg.Perf); } info(s: string) { - this.msg(s, "Info"); + this.msg(s, Msg.Info); } startGroup() { @@ -71,7 +71,7 @@ namespace ts.server { } - msg(s: string, type = "Err") { + msg(s: string, type: Msg.Types = Msg.Err) { if (this.fd < 0) { if (this.logFilename) { this.fd = fs.openSync(this.logFilename, "w"); @@ -105,7 +105,7 @@ namespace ts.server { } exit() { - this.projectService.log("Exiting...", "Info"); + this.logger.info("Exiting..."); this.projectService.closeLog(); process.exit(0); } diff --git a/src/server/session.ts b/src/server/session.ts index a9648bd8d9e..88676aae6a1 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -164,7 +164,7 @@ namespace ts.server { private maxUncompressedMessageSize: number, private compress: (s: string) => CompressedData, private hrtime: (start?: number[]) => number[], - private logger: Logger) { + protected logger: Logger) { this.projectService = new ProjectService(host, logger, cancellationToken, useSingleInferredProject, (eventName, project, fileName) => { this.handleEvent(eventName, project, fileName); @@ -173,7 +173,7 @@ namespace ts.server { private handleEvent(eventName: string, project: Project, fileName: NormalizedPath) { if (eventName == "context") { - this.projectService.log("got context event, updating diagnostics for" + fileName, "Info"); + this.logger.info("got context event, updating diagnostics for" + fileName); this.updateErrorCheck([{ fileName, project }], this.changeSeq, (n) => n === this.changeSeq, 100); } @@ -187,7 +187,7 @@ namespace ts.server { msg += "\n" + (err).stack; } } - this.projectService.log(msg); + this.logger.msg(msg, Msg.Err); } public send(msg: protocol.Message, canCompressResponse: boolean) { @@ -334,7 +334,7 @@ namespace ts.server { if (!projects) { return; } - this.projectService.log(`cleaning ${caption}`); + this.logger.info(`cleaning ${caption}`); for (const p of projects) { p.languageService.cleanupSemanticCache(); } @@ -345,7 +345,7 @@ namespace ts.server { this.cleanProjects("configured projects", this.projectService.configuredProjects); this.cleanProjects("external projects", this.projectService.externalProjects); if (typeof global !== "undefined" && global.gc) { - this.projectService.log(`global.gc()`); + this.logger.info(`global.gc()`); global.gc(); global.gc(); global.gc(); @@ -1451,8 +1451,8 @@ namespace ts.server { return handler(request); } else { - this.projectService.log("Unrecognized JSON command: " + JSON.stringify(request)); - this.output(undefined, CommandNames.Unknown, /*canCompressResponse*/ false, request.seq, "Unrecognized JSON command: " + request.command); + this.logger.msg(`Unrecognized JSON command: ${JSON.stringify(request)}`, Msg.Err); + this.output(undefined, CommandNames.Unknown, /*canCompressResponse*/ false, request.seq, `Unrecognized JSON command: ${request.command}`); return { responseRequired: false }; } } diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 79a8edc0f8c..cf38324343c 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -9,7 +9,17 @@ namespace ts.server { info(s: string): void; startGroup(): void; endGroup(): void; - msg(s: string, type?: string): void; + msg(s: string, type?: Msg.Types): void; + } + + export namespace Msg { + export type Err = "Err"; + export const Err: Err = "Err"; + export type Info = "Info"; + export const Info: Info = "Info"; + export type Perf = "Perf"; + export const Perf: Perf = "Perf"; + export type Types = Err | Info | Perf; } export function getDefaultFormatCodeSettings(host: ServerHost): FormatCodeSettings { From aea1534704743dabbf63a1d7fe390a97c3ce1055 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 11 Jul 2016 19:51:14 -0700 Subject: [PATCH 097/307] move saveTo to ScriptInfo --- src/server/project.ts | 9 --------- src/server/scriptInfo.ts | 5 +++++ src/server/session.ts | 9 +++------ 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/server/project.ts b/src/server/project.ts index 92f1d097b36..9a4e7ada961 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -267,15 +267,6 @@ namespace ts.server { } } - saveTo(filename: NormalizedPath, tmpfilename: NormalizedPath) { - const script = this.projectService.getScriptInfoForNormalizedPath(filename); - if (script) { - Debug.assert(script.isAttached(this)); - const snap = script.snap(); - this.projectService.host.writeFile(tmpfilename, snap.getText(0, snap.getLength())); - } - } - reloadScript(filename: NormalizedPath): boolean { const script = this.projectService.getScriptInfoForNormalizedPath(filename); if (script) { diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 7ab6bc7e7ec..901e247df6c 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -110,6 +110,11 @@ namespace ts.server { this.markContainingProjectsAsDirty(); } + saveTo(fileName: string) { + const snap = this.snap(); + this.host.writeFile(fileName, snap.getText(0, snap.getLength())); + } + reloadFromFile() { this.svc.reloadFromFile(this.fileName); this.markContainingProjectsAsDirty(); diff --git a/src/server/session.ts b/src/server/session.ts index 88676aae6a1..739446b18c8 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1015,12 +1015,9 @@ namespace ts.server { } private saveToTmp(fileName: string, tempFileName: string) { - const file = toNormalizedPath(fileName); - const tmpfile = toNormalizedPath(tempFileName); - - const project = this.projectService.getDefaultProjectForFile(file); - if (project) { - project.saveTo(file, tmpfile); + const scriptInfo = this.projectService.getScriptInfo(fileName); + if (scriptInfo) { + scriptInfo.saveTo(tempFileName); } } From bcdb06c4f76c34f7d2b4ba335f4e9c131c6be102 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 11 Jul 2016 20:59:22 -0700 Subject: [PATCH 098/307] introduce methods that allow to get project without refreshing inferred projects --- src/server/editorServices.ts | 18 +++++++++++----- src/server/session.ts | 40 ++++++++++++++++++++---------------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 44c28dbb5a1..3a2d27f4647 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -172,11 +172,24 @@ namespace ts.server { return undefined; } if (isInferredProjectName(projectName)) { + this.ensureInferredProjectsUpToDate(); return findProjectByName(projectName, this.inferredProjects); } return this.findExternalProjectByProjectName(projectName) || this.findConfiguredProjectByProjectName(toNormalizedPath(projectName)); } + getDefaultProjectForFile(fileName: NormalizedPath, refreshInferredProjects: boolean) { + if (refreshInferredProjects) { + this.ensureInferredProjectsUpToDate(); + } + const scriptInfo = this.getScriptInfoForNormalizedPath(fileName); + return scriptInfo && scriptInfo.getDefaultProject(); + } + + private ensureInferredProjectsUpToDate() { + + } + private findContainingConfiguredProject(info: ScriptInfo): ConfiguredProject { for (const proj of this.configuredProjects) { if (proj.containsScriptInfo(info)) { @@ -941,11 +954,6 @@ namespace ts.server { this.printProjects(); } - getDefaultProjectForFile(fileName: NormalizedPath) { - const scriptInfo = this.getScriptInfoForNormalizedPath(fileName); - return scriptInfo && scriptInfo.getDefaultProject(); - } - private collectChanges(lastKnownProjectVersions: protocol.ProjectVersionInfo[], currentProjects: Project[], result: protocol.ProjectFiles[]): void { for (const proj of currentProjects) { const knownProject = forEach(lastKnownProjectVersions, p => p.projectName === proj.getProjectName() && p); diff --git a/src/server/session.ts b/src/server/session.ts index 739446b18c8..e0a727403eb 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -505,7 +505,7 @@ namespace ts.server { } private getProjectInfoWorker(uncheckedFileName: string, projectFileName: string, needFileNameList: boolean) { - const { file, project } = this.getFileAndProjectWorker(uncheckedFileName, projectFileName, /*errorOnMissingProject*/ true); + const { file, project } = this.getFileAndProjectWorker(uncheckedFileName, projectFileName, /*refreshInferredProjects*/ true, /*errorOnMissingProject*/ true); const projectInfo = { configFileName: project.getProjectName(), languageServiceDisabled: !project.languageServiceEnabled, @@ -730,12 +730,16 @@ namespace ts.server { } private getFileAndProject(args: protocol.FileRequestArgs, errorOnMissingProject = true) { - return this.getFileAndProjectWorker(args.file, args.projectFileName, errorOnMissingProject); + return this.getFileAndProjectWorker(args.file, args.projectFileName, /*refreshInferredProjects*/ true, errorOnMissingProject); } - private getFileAndProjectWorker(uncheckedFileName: string, projectFileName: string, errorOnMissingProject: boolean) { + private getFileAndProjectWithoutRefreshingInferredProjects(args: protocol.FileRequestArgs, errorOnMissingProject = true) { + return this.getFileAndProjectWorker(args.file, args.projectFileName, /*refreshInferredProjects*/ false, errorOnMissingProject); + } + + private getFileAndProjectWorker(uncheckedFileName: string, projectFileName: string, refreshInferredProjects: boolean, errorOnMissingProject: boolean) { const file = toNormalizedPath(uncheckedFileName); - const project: Project = this.getProject(projectFileName) || this.projectService.getDefaultProjectForFile(file); + const project: Project = this.getProject(projectFileName) || this.projectService.getDefaultProjectForFile(file, refreshInferredProjects); if (!project && errorOnMissingProject) { throw Errors.NoProject; } @@ -743,7 +747,7 @@ namespace ts.server { } private getOutliningSpans(args: protocol.FileRequestArgs) { - const { file, project } = this.getFileAndProject(args); + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); return project.languageService.getOutliningSpans(file); } @@ -753,14 +757,14 @@ namespace ts.server { } private getDocCommentTemplate(args: protocol.FileLocationRequestArgs) { - const { file, project } = this.getFileAndProject(args); + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); return project.languageService.getDocCommentTemplateAtPosition(file, position); } private getIndentation(args: protocol.IndentationRequestArgs) { - const { file, project } = this.getFileAndProject(args); + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file)); const options = args.options || this.projectService.getFormatCodeOptions(file); const indentation = project.languageService.getIndentationAtPosition(file, position, options); @@ -768,19 +772,19 @@ namespace ts.server { } private getBreakpointStatement(args: protocol.FileLocationRequestArgs) { - const { file, project } = this.getFileAndProject(args); + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file)); return project.languageService.getBreakpointStatementAtPosition(file, position); } private getNameOrDottedNameSpan(args: protocol.FileLocationRequestArgs) { - const { file, project } = this.getFileAndProject(args); + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file)); return project.languageService.getNameOrDottedNameSpan(file, position, position); } private isValidBraceCompletion(args: protocol.BraceCompletionRequestArgs) { - const { file, project } = this.getFileAndProject(args); + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file)); return project.languageService.isValidBraceCompletionAtPosition(file, position, args.openingBrace.charCodeAt(0)); } @@ -811,7 +815,7 @@ namespace ts.server { } private getFormattingEditsForRange(args: protocol.FormatRequestArgs): protocol.CodeEdit[] { - const { file, project } = this.getFileAndProject(args); + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file); const startPosition = scriptInfo.lineOffsetToPosition(args.line, args.offset); @@ -834,25 +838,25 @@ namespace ts.server { } private getFormattingEditsForRangeFull(args: protocol.FormatRequestArgs) { - const { file, project } = this.getFileAndProject(args); + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); const options = args.options || this.projectService.getFormatCodeOptions(file); return project.languageService.getFormattingEditsForRange(file, args.position, args.endPosition, options); } private getFormattingEditsForDocumentFull(args: protocol.FormatRequestArgs) { - const { file, project } = this.getFileAndProject(args); + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); const options = args.options || this.projectService.getFormatCodeOptions(file); return project.languageService.getFormattingEditsForDocument(file, options); } private getFormattingEditsAfterKeystrokeFull(args: protocol.FormatOnKeyRequestArgs) { - const { file, project } = this.getFileAndProject(args); + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); const options = args.options || this.projectService.getFormatCodeOptions(file); return project.languageService.getFormattingEditsAfterKeystroke(file, args.position, args.key, options); } private getFormattingEditsAfterKeystroke(args: protocol.FormatOnKeyRequestArgs): protocol.CodeEdit[] { - const { file, project } = this.getFileAndProject(args); + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file); const position = scriptInfo.lineOffsetToPosition(args.line, args.offset); const formatOptions = this.projectService.getFormatCodeOptions(file); @@ -976,7 +980,7 @@ namespace ts.server { private getDiagnostics(delay: number, fileNames: string[]) { const checkList = fileNames.reduce((accum: PendingErrorCheck[], uncheckedFileName: string) => { const fileName = toNormalizedPath(uncheckedFileName); - const project = this.projectService.getDefaultProjectForFile(fileName); + const project = this.projectService.getDefaultProjectForFile(fileName, /*refreshInferredProjects*/ true); if (project) { accum.push({ fileName, project }); } @@ -1004,7 +1008,7 @@ namespace ts.server { private reload(args: protocol.ReloadRequestArgs, reqSeq: number) { const file = toNormalizedPath(args.file); - const project = this.projectService.getDefaultProjectForFile(file); + const project = this.projectService.getDefaultProjectForFile(file, /*refreshInferredProjects*/ true); if (project) { this.changeSeq++; // make sure no changes happen before this one is finished @@ -1177,7 +1181,7 @@ namespace ts.server { const lowPriorityFiles: NormalizedPath[] = []; const veryLowPriorityFiles: NormalizedPath[] = []; const normalizedFileName = toNormalizedPath(fileName); - const project = this.projectService.getDefaultProjectForFile(normalizedFileName); + const project = this.projectService.getDefaultProjectForFile(normalizedFileName, /*refreshInferredProjects*/ true); for (const fileNameInProject of fileNamesInProject) { if (this.getCanonicalFileName(fileNameInProject) == this.getCanonicalFileName(fileName)) highPriorityFiles.push(fileNameInProject); From 78974efe1879d622196a0bce5e1384c4658f0a04 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 12 Jul 2016 18:02:54 -0700 Subject: [PATCH 099/307] defer updates in project structure after file is edited --- src/harness/harnessLanguageService.ts | 2 +- src/server/editorServices.ts | 44 +++++++++++++++++-- src/server/server.ts | 34 ++++++++------ src/server/session.ts | 30 ++++++++----- src/server/utilities.ts | 9 +++- .../cases/unittests/cachingInServerLSHost.ts | 2 +- tests/cases/unittests/session.ts | 2 +- .../cases/unittests/tsserverProjectSystem.ts | 31 ++++++++++++- 8 files changed, 119 insertions(+), 35 deletions(-) diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 86630d9083d..527705d8b4f 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -646,7 +646,7 @@ namespace Harness.LanguageService { return true; } - isVerbose() { + hasLevel() { return false; } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 3a2d27f4647..8139decfd05 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -145,6 +145,8 @@ namespace ts.server { private readonly hostConfiguration: HostConfiguration; + private changedFiles: ScriptInfo[]; + constructor(public readonly host: ServerHost, public readonly logger: Logger, public readonly cancellationToken: HostCancellationToken, @@ -163,6 +165,14 @@ namespace ts.server { this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); } + getChangedFiles_TestOnly() { + return this.changedFiles; + } + + ensureInferredProjectsUpToDate_TestOnly() { + this.ensureInferredProjectsUpToDate(); + } + stopWatchingDirectory(directory: string) { this.directoryWatchers.stopWatchingDirectory(directory); } @@ -187,7 +197,21 @@ namespace ts.server { } private ensureInferredProjectsUpToDate() { - + if (this.changedFiles) { + let projectsToUpdate: Project[]; + if (this.changedFiles.length === 1) { + // simpliest case - no allocations + projectsToUpdate = this.changedFiles[0].containingProjects; + } + else { + projectsToUpdate = []; + for (const f of this.changedFiles) { + projectsToUpdate = projectsToUpdate.concat(f.containingProjects); + } + } + this.updateProjectGraphs(projectsToUpdate); + this.changedFiles = undefined; + } } private findContainingConfiguredProject(info: ScriptInfo): ConfiguredProject { @@ -532,7 +556,7 @@ namespace ts.server { } private printProjects() { - if (!this.logger.isVerbose()) { + if (!this.logger.hasLevel(LogLevel.verbose)) { return; } @@ -970,11 +994,13 @@ namespace ts.server { } applyChangesInOpenFiles(openFiles: protocol.NewOpenFile[], changedFiles: protocol.ChangedOpenFile[], closedFiles: string[]): void { + const recordChangedFiles = changedFiles && !openFiles && !closedFiles; if (openFiles) { for (const file of openFiles) { const scriptInfo = this.getScriptInfo(file.fileName); Debug.assert(!scriptInfo || !scriptInfo.isOpen); - this.openClientFileWithNormalizedPath(toNormalizedPath(file.fileName), file.content); + const normalizedPath = scriptInfo ? scriptInfo.fileName : toNormalizedPath(file.fileName); + this.openClientFileWithNormalizedPath(normalizedPath, file.content); } } @@ -987,6 +1013,14 @@ namespace ts.server { const change = file.changes[i]; scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); } + if (recordChangedFiles) { + if (!this.changedFiles) { + this.changedFiles = [scriptInfo]; + } + else if (this.changedFiles.indexOf(scriptInfo) < 0) { + this.changedFiles.push(scriptInfo); + } + } } } @@ -996,7 +1030,9 @@ namespace ts.server { } } - if (openFiles || changedFiles || closedFiles) { + // if files were open or closed then explicitly refresh list of inferred projects + // otherwise if there were only changes in files - record changed files in `changedFiles` and defer the update + if (openFiles || closedFiles) { this.refreshInferredProjects(); } } diff --git a/src/server/server.ts b/src/server/server.ts index 1a101f9d030..953302821bc 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -30,7 +30,7 @@ namespace ts.server { constructor(private readonly logFilename: string, private readonly traceToConsole: boolean, - private readonly level: string) { + private readonly level: LogLevel) { } static padStringRight(str: string, padding: string) { @@ -66,11 +66,10 @@ namespace ts.server { return !!this.logFilename || this.traceToConsole; } - isVerbose() { - return this.loggingEnabled() && (this.level == "verbose"); + hasLevel(level: LogLevel) { + return this.loggingEnabled() && this.level >= level; } - msg(s: string, type: Msg.Types = Msg.Err) { if (this.fd < 0) { if (this.logFilename) { @@ -88,8 +87,8 @@ namespace ts.server { this.seq++; this.firstInGroup = true; } - const buf = new Buffer(s); if (this.fd >= 0) { + const buf = new Buffer(s); fs.writeSync(this.fd, buf, 0, buf.length, null); } if (this.traceToConsole) { @@ -124,12 +123,13 @@ namespace ts.server { interface LogOptions { file?: string; - detailLevel?: string; + detailLevel?: LogLevel; traceToConsole?: boolean; + logToFile?: boolean; } function parseLoggingEnvironmentString(logEnvStr: string): LogOptions { - const logEnv: LogOptions = {}; + const logEnv: LogOptions = { logToFile: true }; const args = logEnvStr.split(" "); for (let i = 0, len = args.length; i < (len - 1); i += 2) { const option = args[i]; @@ -140,11 +140,15 @@ namespace ts.server { logEnv.file = value; break; case "-level": - logEnv.detailLevel = value; + const level: LogLevel = (LogLevel)[value]; + logEnv.detailLevel = typeof level === "number" ? level : LogLevel.normal; break; case "-traceToConsole": logEnv.traceToConsole = value.toLowerCase() === "true"; break; + case "-logToFile": + logEnv.logToFile = value.toLowerCase() === "true"; + break; } } } @@ -154,16 +158,18 @@ namespace ts.server { // TSS_LOG "{ level: "normal | verbose | terse", file?: string}" function createLoggerFromEnv() { let fileName: string = undefined; - let detailLevel = "normal"; + let detailLevel = LogLevel.normal; let traceToConsole = false; const logEnvStr = process.env["TSS_LOG"]; if (logEnvStr) { const logEnv = parseLoggingEnvironmentString(logEnvStr); - if (logEnv.file) { - fileName = logEnv.file; - } - else { - fileName = __dirname + "/.log" + process.pid.toString(); + if (logEnv.logToFile) { + if (logEnv.file) { + fileName = logEnv.file; + } + else { + fileName = __dirname + "/.log" + process.pid.toString(); + } } if (logEnv.detailLevel) { detailLevel = logEnv.detailLevel; diff --git a/src/server/session.ts b/src/server/session.ts index e0a727403eb..96c6a23837d 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -191,8 +191,10 @@ namespace ts.server { } public send(msg: protocol.Message, canCompressResponse: boolean) { + const verboseLogging = this.logger.hasLevel(LogLevel.verbose); + const json = JSON.stringify(msg); - if (this.logger.isVerbose()) { + if (verboseLogging) { this.logger.info(msg.type + ": " + json); } @@ -201,9 +203,9 @@ namespace ts.server { this.host.write(`Content-Length: ${1 + this.byteLength(json, "utf8")}\r\n\r\n${json}${this.host.newLine}`); } else { - const start = this.logger.isVerbose() && this.hrtime(); + const start = verboseLogging && this.hrtime(); const compressed = this.compress(json); - if (this.logger.isVerbose()) { + if (verboseLogging) { const elapsed = this.hrtime(start); this.logger.info(`compressed message ${json.length} to ${compressed.length} in ${hrTimeToMilliseconds(elapsed)} ms using ${compressed.compressionKind}`); } @@ -1460,24 +1462,28 @@ namespace ts.server { public onMessage(message: string) { let start: number[]; - if (this.logger.isVerbose()) { - this.logger.info("request: " + message); + if (this.logger.hasLevel(LogLevel.requestTime)) { start = this.hrtime(); + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`request: ${message}`); + } } + let request: protocol.Request; try { request = JSON.parse(message); const {response, responseRequired} = this.executeCommand(request); - if (this.logger.isVerbose()) { - const elapsed = this.hrtime(start); - const elapsedMs = hrTimeToMilliseconds(elapsed); - let leader = "Elapsed time (in milliseconds)"; - if (!responseRequired) { - leader = "Async elapsed time (in milliseconds)"; + if (this.logger.hasLevel(LogLevel.requestTime)) { + const elapsedTime = hrTimeToMilliseconds(this.hrtime(start)).toFixed(4); + if (responseRequired) { + this.logger.perftrc(`${request.seq}::${request.command}: elapsed time (in milliseconds) ${elapsedTime}`); + } + else { + this.logger.perftrc(`${request.seq}::${request.command}: async elapsed time (in milliseconds) ${elapsedTime}`); } - this.logger.msg(leader + ": " + elapsedMs.toFixed(4).toString(), "Perf"); } + if (response) { this.output(response, request.command, request.canCompressResponse, request.seq); } diff --git a/src/server/utilities.ts b/src/server/utilities.ts index cf38324343c..ccbf380cfcb 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -1,9 +1,16 @@ /// namespace ts.server { + export enum LogLevel { + terse, + normal, + requestTime, + verbose + } + export interface Logger { close(): void; - isVerbose(): boolean; + hasLevel(level: LogLevel): boolean; loggingEnabled(): boolean; perftrc(s: string): void; info(s: string): void; diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index cbf8ff38b1e..cd5c2a37540 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -75,7 +75,7 @@ namespace ts { function createProject(rootFile: string, serverHost: server.ServerHost): { project: server.Project, rootScriptInfo: server.ScriptInfo } { const logger: server.Logger = { close() { }, - isVerbose: () => false, + hasLevel: () => false, loggingEnabled: () => false, perftrc: (s: string) => { }, info: (s: string) => { }, diff --git a/tests/cases/unittests/session.ts b/tests/cases/unittests/session.ts index 3aebd28df9a..123762c9687 100644 --- a/tests/cases/unittests/session.ts +++ b/tests/cases/unittests/session.ts @@ -29,7 +29,7 @@ namespace ts.server { const nullCancellationToken: HostCancellationToken = { isCancellationRequested: () => false }; const mockLogger: Logger = { close(): void {}, - isVerbose(): boolean { return false; }, + hasLevel(): boolean { return false; }, loggingEnabled(): boolean { return false; }, perftrc(s: string): void {}, info(s: string): void {}, diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index b5a5f3aef54..d906558985f 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -7,7 +7,7 @@ namespace ts { const nullLogger: server.Logger = { close: () => void 0, - isVerbose: () => void 0, + hasLevel: () => void 0, loggingEnabled: () => false, perftrc: () => void 0, info: () => void 0, @@ -1254,5 +1254,34 @@ namespace ts { checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); }); + + it("project structure update is deferred if files are not added\removed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `import {x} from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: "export let x = 1" + }; + const host = createServerHost([file1, file2]); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useSingleInferredProject*/ false); + + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + projectService.applyChangesInOpenFiles( + /*openFiles*/ undefined, + /*changedFiles*/ [{ fileName: file1.path, changes: [ { span: createTextSpan(0, file1.path.length), newText: "let y = 1" } ] }], + /*closedFiles*/ undefined); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const changedFiles = projectService.getChangedFiles_TestOnly(); + assert(changedFiles && changedFiles.length === 1, `expected 1 changed file, got ${JSON.stringify(changedFiles && changedFiles.length || 0)}`); + + projectService.ensureInferredProjectsUpToDate_TestOnly(); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + }); }); } \ No newline at end of file From a9ba0a5585092a8b45fd2a458bcb233fe8f49d29 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 12 Jul 2016 18:48:41 -0700 Subject: [PATCH 100/307] use circular buffer instead of unbounded array --- src/server/scriptVersionCache.ts | 46 ++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/src/server/scriptVersionCache.ts b/src/server/scriptVersionCache.ts index 09f5905bff6..2e8584be66a 100644 --- a/src/server/scriptVersionCache.ts +++ b/src/server/scriptVersionCache.ts @@ -267,15 +267,27 @@ namespace ts.server { export class ScriptVersionCache { changes: TextChange[] = []; - versions: LineIndexSnapshot[] = []; + versions: LineIndexSnapshot[] = new Array(ScriptVersionCache.maxVersions); minVersion = 0; // no versions earlier than min version will maintain change history - private currentVersion = 0; + private host: ServerHost; + private currentVersion = 0; static changeNumberThreshold = 8; static changeLengthThreshold = 256; static maxVersions = 8; + private versionToIndex(version: number) { + if (version < this.minVersion || version > this.currentVersion) { + return undefined; + } + return version % ScriptVersionCache.maxVersions; + } + + private currentVersionToIndex() { + return this.currentVersion % ScriptVersionCache.maxVersions; + } + // REVIEW: can optimize by coalescing simple edits edit(pos: number, deleteLen: number, insertedText?: string) { this.changes[this.changes.length] = new TextChange(pos, deleteLen, insertedText); @@ -287,7 +299,7 @@ namespace ts.server { } latest() { - return this.versions[this.currentVersion]; + return this.versions[this.currentVersionToIndex()]; } latestVersion() { @@ -312,22 +324,24 @@ namespace ts.server { this.currentVersion++; this.changes = []; // history wiped out by reload const snap = new LineIndexSnapshot(this.currentVersion, this); - this.versions[this.currentVersion] = snap; + + // delete all versions + for (let i = 0; i < this.versions.length; i++) { + this.versions[i] = undefined; + } + + this.versions[this.currentVersionToIndex()] = snap; snap.index = new LineIndex(); const lm = LineIndex.linesFromText(script); snap.index.load(lm.lines); - // REVIEW: could use linked list - for (let i = this.minVersion; i < this.currentVersion; i++) { - this.versions[i] = undefined; - } - this.minVersion = this.currentVersion; + this.minVersion = this.currentVersion; } getSnapshot() { - let snap = this.versions[this.currentVersion]; + let snap = this.versions[this.currentVersionToIndex()]; if (this.changes.length > 0) { - let snapIndex = this.latest().index; + let snapIndex = snap.index; for (let i = 0, len = this.changes.length; i < len; i++) { const change = this.changes[i]; snapIndex = snapIndex.edit(change.pos, change.deleteLen, change.insertedText); @@ -335,15 +349,13 @@ namespace ts.server { snap = new LineIndexSnapshot(this.currentVersion + 1, this); snap.index = snapIndex; snap.changesSincePreviousVersion = this.changes; + this.currentVersion = snap.version; - this.versions[snap.version] = snap; + this.versions[this.currentVersionToIndex()] = snap; this.changes = []; + if ((this.currentVersion - this.minVersion) >= ScriptVersionCache.maxVersions) { - const oldMin = this.minVersion; this.minVersion = (this.currentVersion - ScriptVersionCache.maxVersions) + 1; - for (let j = oldMin; j < this.minVersion; j++) { - this.versions[j] = undefined; - } } } return snap; @@ -354,7 +366,7 @@ namespace ts.server { if (oldVersion >= this.minVersion) { const textChangeRanges: ts.TextChangeRange[] = []; for (let i = oldVersion + 1; i <= newVersion; i++) { - const snap = this.versions[i]; + const snap = this.versions[this.versionToIndex(i)]; for (let j = 0, len = snap.changesSincePreviousVersion.length; j < len; j++) { const textChange = snap.changesSincePreviousVersion[j]; textChangeRanges[textChangeRanges.length] = textChange.getTextChangeRange(); From dec09ec51d7f00d30c62f8424b2499039925e087 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 18 Jul 2016 14:36:43 -0700 Subject: [PATCH 101/307] Port #9798 --- src/compiler/tsc.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index e32a3816d64..15c52c508cd 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -380,7 +380,8 @@ namespace ts { sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); return; } - const configParseResult = parseJsonConfigFileContent(configObject, sys, getNormalizedAbsolutePath(getDirectoryPath(configFileName), sys.getCurrentDirectory()), commandLine.options, configFileName); + const cwd = sys.getCurrentDirectory(); + const configParseResult = parseJsonConfigFileContent(configObject, sys, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), commandLine.options, getNormalizedAbsolutePath(configFileName, cwd)); if (configParseResult.errors.length > 0) { reportDiagnostics(configParseResult.errors, /* compilerHost */ undefined); sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); From e9086d1811745cb65a7be5f8c3d0c13fba7c6cb1 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 18 Jul 2016 16:56:25 -0700 Subject: [PATCH 102/307] remove node.d.ts --- src/server/cancellationToken.ts | 4 +- src/server/node.d.ts | 688 -------------------------------- 2 files changed, 2 insertions(+), 690 deletions(-) delete mode 100644 src/server/node.d.ts diff --git a/src/server/cancellationToken.ts b/src/server/cancellationToken.ts index ac5adeab6d7..39b74b8bc1a 100644 --- a/src/server/cancellationToken.ts +++ b/src/server/cancellationToken.ts @@ -1,4 +1,4 @@ -/// +/// // TODO: extract services types @@ -6,7 +6,7 @@ interface HostCancellationToken { isCancellationRequested(): boolean; } -const fs: typeof NodeJS.fs = require("fs"); +import fs = require("fs"); function createCancellationToken(args: string[]): HostCancellationToken { let cancellationPipeName: string; diff --git a/src/server/node.d.ts b/src/server/node.d.ts deleted file mode 100644 index 8d5a345bd5e..00000000000 --- a/src/server/node.d.ts +++ /dev/null @@ -1,688 +0,0 @@ -// Type definitions for Node.js v0.10.1 -// Project: http://nodejs.org/ -// Definitions by: Microsoft TypeScript , DefinitelyTyped -// Definitions: https://github.com/borisyankov/DefinitelyTyped - -/************************************************ -* * -* Node.js v0.10.1 API * -* * -************************************************/ - -/************************************************ -* * -* GLOBAL * -* * -************************************************/ -declare var process: NodeJS.Process; -declare var global: any; - -declare var __filename: string; -declare var __dirname: string; - -declare function setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): NodeJS.Timer; -declare function clearTimeout(timeoutId: NodeJS.Timer): void; -declare function setInterval(callback: (...args: any[]) => void, ms: number, ...args: any[]): NodeJS.Timer; -declare function clearInterval(intervalId: NodeJS.Timer): void; -declare function setImmediate(callback: (...args: any[]) => void, ...args: any[]): any; -declare function clearImmediate(immediateId: any): void; - -declare var require: { - (id: string): any; - resolve(id: string): string; - cache: any; - extensions: any; - main: any; -}; - -declare var module: { - exports: any; - require(id: string): any; - id: string; - filename: string; - loaded: boolean; - parent: any; - children: any[]; -}; - -// Same as module.exports -declare var exports: any; -declare var SlowBuffer: { - new (str: string, encoding?: string): Buffer; - new (size: number): Buffer; - new (size: Uint8Array): Buffer; - new (array: any[]): Buffer; - prototype: Buffer; - isBuffer(obj: any): boolean; - byteLength(string: string, encoding?: string): number; - concat(list: Buffer[], totalLength?: number): Buffer; -}; - - -// Buffer class -interface Buffer extends NodeBuffer { } -interface BufferConstructor { - new (str: string, encoding?: string): Buffer; - new (size: number): Buffer; - new (size: Uint8Array): Buffer; - new (array: any[]): Buffer; - prototype: Buffer; - isBuffer(obj: any): boolean; - byteLength(string: string, encoding?: string): number; - concat(list: Buffer[], totalLength?: number): Buffer; -} -declare var Buffer: BufferConstructor; - -/************************************************ -* * -* GLOBAL INTERFACES * -* * -************************************************/ -declare namespace NodeJS { - export interface ErrnoException extends Error { - errno?: any; - code?: string; - path?: string; - syscall?: string; - } - - export interface EventEmitter { - addListener(event: string, listener: Function): EventEmitter; - on(event: string, listener: Function): EventEmitter; - once(event: string, listener: Function): EventEmitter; - removeListener(event: string, listener: Function): EventEmitter; - removeAllListeners(event?: string): EventEmitter; - setMaxListeners(n: number): void; - listeners(event: string): Function[]; - emit(event: string, ...args: any[]): boolean; - } - - export interface ReadableStream extends EventEmitter { - readable: boolean; - read(size?: number): any; - setEncoding(encoding: string): void; - pause(): void; - resume(): void; - pipe(destination: T, options?: { end?: boolean; }): T; - unpipe(destination?: T): void; - unshift(chunk: string): void; - unshift(chunk: Buffer): void; - wrap(oldStream: ReadableStream): ReadableStream; - } - - export interface WritableStream extends EventEmitter { - writable: boolean; - write(buffer: Buffer, cb?: Function): boolean; - write(str: string, cb?: Function): boolean; - write(str: string, encoding?: string, cb?: Function): boolean; - end(): void; - end(buffer: Buffer, cb?: Function): void; - end(str: string, cb?: Function): void; - end(str: string, encoding?: string, cb?: Function): void; - } - - export interface ReadWriteStream extends ReadableStream, WritableStream { } - - interface WindowSize { - columns: number; - rows: number; - } - - export interface Process extends EventEmitter { - stdout: WritableStream & WindowSize; - stderr: WritableStream & WindowSize; - stdin: ReadableStream; - argv: string[]; - execPath: string; - abort(): void; - chdir(directory: string): void; - cwd(): string; - env: any; - exit(code?: number): void; - getgid(): number; - setgid(id: number): void; - setgid(id: string): void; - getuid(): number; - setuid(id: number): void; - setuid(id: string): void; - version: string; - versions: { - http_parser: string; - node: string; - v8: string; - ares: string; - uv: string; - zlib: string; - openssl: string; - }; - config: { - target_defaults: { - cflags: any[]; - default_configuration: string; - defines: string[]; - include_dirs: string[]; - libraries: string[]; - }; - variables: { - clang: number; - host_arch: string; - node_install_npm: boolean; - node_install_waf: boolean; - node_prefix: string; - node_shared_openssl: boolean; - node_shared_v8: boolean; - node_shared_zlib: boolean; - node_use_dtrace: boolean; - node_use_etw: boolean; - node_use_openssl: boolean; - target_arch: string; - v8_no_strict_aliasing: number; - v8_use_snapshot: boolean; - visibility: string; - }; - }; - kill(pid: number, signal?: string): void; - pid: number; - title: string; - arch: string; - platform: string; - memoryUsage(): { rss: number; heapTotal: number; heapUsed: number; }; - nextTick(callback: Function): void; - umask(mask?: number): number; - uptime(): number; - hrtime(time?: number[]): number[]; - - // Worker - send? (message: any, sendHandle?: any): void; - } - - export interface Timer { - ref(): void; - unref(): void; - } -} - - -/** - * @deprecated - */ -interface NodeBuffer { - [index: number]: number; - write(string: string, offset?: number, length?: number, encoding?: string): number; - toString(encoding?: string, start?: number, end?: number): string; - toJSON(): any; - length: number; - copy(targetBuffer: Buffer, targetStart?: number, sourceStart?: number, sourceEnd?: number): number; - slice(start?: number, end?: number): Buffer; - readUInt8(offset: number, noAsset?: boolean): number; - readUInt16LE(offset: number, noAssert?: boolean): number; - readUInt16BE(offset: number, noAssert?: boolean): number; - readUInt32LE(offset: number, noAssert?: boolean): number; - readUInt32BE(offset: number, noAssert?: boolean): number; - readInt8(offset: number, noAssert?: boolean): number; - readInt16LE(offset: number, noAssert?: boolean): number; - readInt16BE(offset: number, noAssert?: boolean): number; - readInt32LE(offset: number, noAssert?: boolean): number; - readInt32BE(offset: number, noAssert?: boolean): number; - readFloatLE(offset: number, noAssert?: boolean): number; - readFloatBE(offset: number, noAssert?: boolean): number; - readDoubleLE(offset: number, noAssert?: boolean): number; - readDoubleBE(offset: number, noAssert?: boolean): number; - writeUInt8(value: number, offset: number, noAssert?: boolean): void; - writeUInt16LE(value: number, offset: number, noAssert?: boolean): void; - writeUInt16BE(value: number, offset: number, noAssert?: boolean): void; - writeUInt32LE(value: number, offset: number, noAssert?: boolean): void; - writeUInt32BE(value: number, offset: number, noAssert?: boolean): void; - writeInt8(value: number, offset: number, noAssert?: boolean): void; - writeInt16LE(value: number, offset: number, noAssert?: boolean): void; - writeInt16BE(value: number, offset: number, noAssert?: boolean): void; - writeInt32LE(value: number, offset: number, noAssert?: boolean): void; - writeInt32BE(value: number, offset: number, noAssert?: boolean): void; - writeFloatLE(value: number, offset: number, noAssert?: boolean): void; - writeFloatBE(value: number, offset: number, noAssert?: boolean): void; - writeDoubleLE(value: number, offset: number, noAssert?: boolean): void; - writeDoubleBE(value: number, offset: number, noAssert?: boolean): void; - fill(value: any, offset?: number, end?: number): void; -} - -declare namespace NodeJS { - export interface Path { - normalize(p: string): string; - join(...paths: any[]): string; - resolve(...pathSegments: any[]): string; - relative(from: string, to: string): string; - dirname(p: string): string; - basename(p: string, ext?: string): string; - extname(p: string): string; - sep: string; - } -} - -declare namespace NodeJS { - export interface ReadLineInstance extends EventEmitter { - setPrompt(prompt: string, length: number): void; - prompt(preserveCursor?: boolean): void; - question(query: string, callback: Function): void; - pause(): void; - resume(): void; - close(): void; - write(data: any, key?: any): void; - } - export interface ReadLineOptions { - input: NodeJS.ReadableStream; - output: NodeJS.WritableStream; - completer?: Function; - terminal?: boolean; - } - - export interface ReadLine { - createInterface(options: ReadLineOptions): ReadLineInstance; - } -} - -declare namespace NodeJS { - namespace events { - export class EventEmitter implements NodeJS.EventEmitter { - static listenerCount(emitter: EventEmitter, event: string): number; - - addListener(event: string, listener: Function): EventEmitter; - on(event: string, listener: Function): EventEmitter; - once(event: string, listener: Function): EventEmitter; - removeListener(event: string, listener: Function): EventEmitter; - removeAllListeners(event?: string): EventEmitter; - setMaxListeners(n: number): void; - listeners(event: string): Function[]; - emit(event: string, ...args: any[]): boolean; - } - } -} - -declare namespace NodeJS { - namespace stream { - - export interface Stream extends events.EventEmitter { - pipe(destination: T, options?: { end?: boolean; }): T; - } - - export interface ReadableOptions { - highWaterMark?: number; - encoding?: string; - objectMode?: boolean; - } - - export class Readable extends events.EventEmitter implements NodeJS.ReadableStream { - readable: boolean; - constructor(opts?: ReadableOptions); - _read(size: number): void; - read(size?: number): any; - setEncoding(encoding: string): void; - pause(): void; - resume(): void; - pipe(destination: T, options?: { end?: boolean; }): T; - unpipe(destination?: T): void; - unshift(chunk: string): void; - unshift(chunk: Buffer): void; - wrap(oldStream: NodeJS.ReadableStream): NodeJS.ReadableStream; - push(chunk: any, encoding?: string): boolean; - } - - export interface WritableOptions { - highWaterMark?: number; - decodeStrings?: boolean; - } - - export class Writable extends events.EventEmitter implements NodeJS.WritableStream { - writable: boolean; - constructor(opts?: WritableOptions); - _write(data: Buffer, encoding: string, callback: Function): void; - _write(data: string, encoding: string, callback: Function): void; - write(buffer: Buffer, cb?: Function): boolean; - write(str: string, cb?: Function): boolean; - write(str: string, encoding?: string, cb?: Function): boolean; - end(): void; - end(buffer: Buffer, cb?: Function): void; - end(str: string, cb?: Function): void; - end(str: string, encoding?: string, cb?: Function): void; - } - - export interface DuplexOptions extends ReadableOptions, WritableOptions { - allowHalfOpen?: boolean; - } - - // Note: Duplex extends both Readable and Writable. - export class Duplex extends Readable implements NodeJS.ReadWriteStream { - writable: boolean; - constructor(opts?: DuplexOptions); - _write(data: Buffer, encoding: string, callback: Function): void; - _write(data: string, encoding: string, callback: Function): void; - write(buffer: Buffer, cb?: Function): boolean; - write(str: string, cb?: Function): boolean; - write(str: string, encoding?: string, cb?: Function): boolean; - end(): void; - end(buffer: Buffer, cb?: Function): void; - end(str: string, cb?: Function): void; - end(str: string, encoding?: string, cb?: Function): void; - } - - export interface TransformOptions extends ReadableOptions, WritableOptions { } - - // Note: Transform lacks the _read and _write methods of Readable/Writable. - export class Transform extends events.EventEmitter implements NodeJS.ReadWriteStream { - readable: boolean; - writable: boolean; - constructor(opts?: TransformOptions); - _transform(chunk: Buffer, encoding: string, callback: Function): void; - _transform(chunk: string, encoding: string, callback: Function): void; - _flush(callback: Function): void; - read(size?: number): any; - setEncoding(encoding: string): void; - pause(): void; - resume(): void; - pipe(destination: T, options?: { end?: boolean; }): T; - unpipe(destination?: T): void; - unshift(chunk: string): void; - unshift(chunk: Buffer): void; - wrap(oldStream: NodeJS.ReadableStream): NodeJS.ReadableStream; - push(chunk: any, encoding?: string): boolean; - write(buffer: Buffer, cb?: Function): boolean; - write(str: string, cb?: Function): boolean; - write(str: string, encoding?: string, cb?: Function): boolean; - end(): void; - end(buffer: Buffer, cb?: Function): void; - end(str: string, cb?: Function): void; - end(str: string, encoding?: string, cb?: Function): void; - } - - export class PassThrough extends Transform { } - } -} - -declare namespace NodeJS { - namespace zlib { - export function gzipSync(buffer: Buffer): Buffer; - } -} - -declare namespace NodeJS { - namespace fs { - interface Stats { - isFile(): boolean; - isDirectory(): boolean; - isBlockDevice(): boolean; - isCharacterDevice(): boolean; - isSymbolicLink(): boolean; - isFIFO(): boolean; - isSocket(): boolean; - dev: number; - ino: number; - mode: number; - nlink: number; - uid: number; - gid: number; - rdev: number; - size: number; - blksize: number; - blocks: number; - atime: Date; - mtime: Date; - ctime: Date; - } - interface FSWatcher extends events.EventEmitter { - close(): void; - } - - export interface ReadStream extends stream.Readable { } - export interface WriteStream extends stream.Writable { } - - export function rename(oldPath: string, newPath: string, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function renameSync(oldPath: string, newPath: string): void; - export function truncate(path: string, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function truncate(path: string, len: number, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function truncateSync(path: string, len?: number): void; - export function ftruncate(fd: number, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function ftruncate(fd: number, len: number, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function ftruncateSync(fd: number, len?: number): void; - export function chown(path: string, uid: number, gid: number, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function chownSync(path: string, uid: number, gid: number): void; - export function fchown(fd: number, uid: number, gid: number, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function fchownSync(fd: number, uid: number, gid: number): void; - export function lchown(path: string, uid: number, gid: number, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function lchownSync(path: string, uid: number, gid: number): void; - export function chmod(path: string, mode: number, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function chmod(path: string, mode: string, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function chmodSync(path: string, mode: number): void; - export function chmodSync(path: string, mode: string): void; - export function fchmod(fd: number, mode: number, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function fchmod(fd: number, mode: string, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function fchmodSync(fd: number, mode: number): void; - export function fchmodSync(fd: number, mode: string): void; - export function lchmod(path: string, mode: number, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function lchmod(path: string, mode: string, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function lchmodSync(path: string, mode: number): void; - export function lchmodSync(path: string, mode: string): void; - export function stat(path: string, callback?: (err: NodeJS.ErrnoException, stats: Stats) => any): void; - export function lstat(path: string, callback?: (err: NodeJS.ErrnoException, stats: Stats) => any): void; - export function fstat(fd: number, callback?: (err: NodeJS.ErrnoException, stats: Stats) => any): void; - export function statSync(path: string): Stats; - export function lstatSync(path: string): Stats; - export function fstatSync(fd: number): Stats; - export function link(srcpath: string, dstpath: string, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function linkSync(srcpath: string, dstpath: string): void; - export function symlink(srcpath: string, dstpath: string, type?: string, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function symlinkSync(srcpath: string, dstpath: string, type?: string): void; - export function readlink(path: string, callback?: (err: NodeJS.ErrnoException, linkString: string) => any): void; - export function readlinkSync(path: string): string; - export function realpath(path: string, callback?: (err: NodeJS.ErrnoException, resolvedPath: string) => any): void; - export function realpath(path: string, cache: { [path: string]: string }, callback: (err: NodeJS.ErrnoException, resolvedPath: string) => any): void; - export function realpathSync(path: string, cache?: { [path: string]: string }): string; - export function unlink(path: string, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function unlinkSync(path: string): void; - export function rmdir(path: string, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function rmdirSync(path: string): void; - export function mkdir(path: string, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function mkdir(path: string, mode: number, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function mkdir(path: string, mode: string, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function mkdirSync(path: string, mode?: number): void; - export function mkdirSync(path: string, mode?: string): void; - export function readdir(path: string, callback?: (err: NodeJS.ErrnoException, files: string[]) => void): void; - export function readdirSync(path: string): string[]; - export function close(fd: number, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function closeSync(fd: number): void; - export function open(path: string, flags: string, callback?: (err: NodeJS.ErrnoException, fd: number) => any): void; - export function open(path: string, flags: string, mode: number, callback?: (err: NodeJS.ErrnoException, fd: number) => any): void; - export function open(path: string, flags: string, mode: string, callback?: (err: NodeJS.ErrnoException, fd: number) => any): void; - export function openSync(path: string, flags: string, mode?: number): number; - export function openSync(path: string, flags: string, mode?: string): number; - export function utimes(path: string, atime: number, mtime: number, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function utimes(path: string, atime: Date, mtime: Date, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function utimesSync(path: string, atime: number, mtime: number): void; - export function utimesSync(path: string, atime: Date, mtime: Date): void; - export function futimes(fd: number, atime: number, mtime: number, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function futimes(fd: number, atime: Date, mtime: Date, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function futimesSync(fd: number, atime: number, mtime: number): void; - export function futimesSync(fd: number, atime: Date, mtime: Date): void; - export function fsync(fd: number, callback?: (err?: NodeJS.ErrnoException) => void): void; - export function fsyncSync(fd: number): void; - export function write(fd: number, buffer: Buffer, offset: number, length: number, position: number, callback?: (err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void): void; - export function writeSync(fd: number, buffer: Buffer, offset: number, length: number, position: number): number; - export function read(fd: number, buffer: Buffer, offset: number, length: number, position: number, callback?: (err: NodeJS.ErrnoException, bytesRead: number, buffer: Buffer) => void): void; - export function readSync(fd: number, buffer: Buffer, offset: number, length: number, position: number): number; - export function readFile(filename: string, encoding: string, callback: (err: NodeJS.ErrnoException, data: string) => void): void; - export function readFile(filename: string, options: { encoding: string; flag?: string; }, callback: (err: NodeJS.ErrnoException, data: string) => void): void; - export function readFile(filename: string, options: { flag?: string; }, callback: (err: NodeJS.ErrnoException, data: Buffer) => void): void; - export function readFile(filename: string, callback: (err: NodeJS.ErrnoException, data: Buffer) => void): void; - export function readFileSync(filename: string, encoding: string): string; - export function readFileSync(filename: string, options: { encoding: string; flag?: string; }): string; - export function readFileSync(filename: string, options?: { flag?: string; }): Buffer; - export function writeFile(filename: string, data: any, callback?: (err: NodeJS.ErrnoException) => void): void; - export function writeFile(filename: string, data: any, options: { encoding?: string; mode?: number; flag?: string; }, callback?: (err: NodeJS.ErrnoException) => void): void; - export function writeFile(filename: string, data: any, options: { encoding?: string; mode?: string; flag?: string; }, callback?: (err: NodeJS.ErrnoException) => void): void; - export function writeFileSync(filename: string, data: any, options?: { encoding?: string; mode?: number; flag?: string; }): void; - export function writeFileSync(filename: string, data: any, options?: { encoding?: string; mode?: string; flag?: string; }): void; - export function appendFile(filename: string, data: any, options: { encoding?: string; mode?: number; flag?: string; }, callback?: (err: NodeJS.ErrnoException) => void): void; - export function appendFile(filename: string, data: any, options: { encoding?: string; mode?: string; flag?: string; }, callback?: (err: NodeJS.ErrnoException) => void): void; - export function appendFile(filename: string, data: any, callback?: (err: NodeJS.ErrnoException) => void): void; - export function appendFileSync(filename: string, data: any, options?: { encoding?: string; mode?: number; flag?: string; }): void; - export function appendFileSync(filename: string, data: any, options?: { encoding?: string; mode?: string; flag?: string; }): void; - export function watchFile(filename: string, listener: (curr: Stats, prev: Stats) => void): void; - export function watchFile(filename: string, options: { persistent?: boolean; interval?: number; }, listener: (curr: Stats, prev: Stats) => void): void; - export function unwatchFile(filename: string, listener?: (curr: Stats, prev: Stats) => void): void; - export function watch(filename: string, listener?: (event: string, filename: string) => any): FSWatcher; - export function watch(filename: string, options: { persistent?: boolean; }, listener?: (event: string, filename: string) => any): FSWatcher; - export function exists(path: string, callback?: (exists: boolean) => void): void; - export function existsSync(path: string): boolean; - export function createReadStream(path: string, options?: { - flags?: string; - encoding?: string; - fd?: string; - mode?: number; - bufferSize?: number; - }): ReadStream; - export function createReadStream(path: string, options?: { - flags?: string; - encoding?: string; - fd?: string; - mode?: string; - bufferSize?: number; - }): ReadStream; - export function createWriteStream(path: string, options?: { - flags?: string; - encoding?: string; - string?: string; - }): WriteStream; - } -} - -declare namespace NodeJS { - namespace path { - export function normalize(p: string): string; - export function join(...paths: any[]): string; - export function resolve(...pathSegments: any[]): string; - export function relative(from: string, to: string): string; - export function dirname(p: string): string; - export function basename(p: string, ext?: string): string; - export function extname(p: string): string; - export var sep: string; - } -} - -declare namespace NodeJS { - namespace _debugger { - export interface Packet { - raw: string; - headers: string[]; - body: Message; - } - - export interface Message { - seq: number; - type: string; - } - - export interface RequestInfo { - command: string; - arguments: any; - } - - export interface Request extends Message, RequestInfo { - } - - export interface Event extends Message { - event: string; - body?: any; - } - - export interface Response extends Message { - request_seq: number; - success: boolean; - /** Contains error message if success === false. */ - message?: string; - /** Contains message body if success === true. */ - body?: any; - } - - export interface BreakpointMessageBody { - type: string; - target: number; - line: number; - } - - export class Protocol { - res: Packet; - state: string; - execute(data: string): void; - serialize(rq: Request): string; - onResponse: (pkt: Packet) => void; - } - - export var NO_FRAME: number; - export var port: number; - - export interface ScriptDesc { - name: string; - id: number; - isNative?: boolean; - handle?: number; - type: string; - lineOffset?: number; - columnOffset?: number; - lineCount?: number; - } - - export interface Breakpoint { - id: number; - scriptId: number; - script: ScriptDesc; - line: number; - condition?: string; - scriptReq?: string; - } - - export interface RequestHandler { - (err: boolean, body: Message, res: Packet): void; - request_seq?: number; - } - - export interface ResponseBodyHandler { - (err: boolean, body?: any): void; - request_seq?: number; - } - - export interface ExceptionInfo { - text: string; - } - - export interface BreakResponse { - script?: ScriptDesc; - exception?: ExceptionInfo; - sourceLine: number; - sourceLineText: string; - sourceColumn: number; - } - - export function SourceInfo(body: BreakResponse): string; - - export class Client extends events.EventEmitter { - protocol: Protocol; - scripts: ScriptDesc[]; - handles: ScriptDesc[]; - breakpoints: Breakpoint[]; - currentSourceLine: number; - currentSourceColumn: number; - currentSourceLineText: string; - currentFrame: number; - currentScript: string; - - connect(port: number, host: string): void; - req(req: any, cb: RequestHandler): void; - reqFrameEval(code: string, frame: number, cb: RequestHandler): void; - mirrorObject(obj: any, depth: number, cb: ResponseBodyHandler): void; - setBreakpoint(rq: BreakpointMessageBody, cb: RequestHandler): void; - clearBreakpoint(rq: Request, cb: RequestHandler): void; - listbreakpoints(cb: RequestHandler): void; - reqSource(from: number, to: number, cb: RequestHandler): void; - reqScripts(cb: any): void; - reqContinue(cb: RequestHandler): void; - } - } -} \ No newline at end of file From 9cd5b46100dbca0fa825862e2d69fdb53dde6a31 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 19 Jul 2016 10:07:49 -0700 Subject: [PATCH 103/307] strip quotes from the log file name --- src/server/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/server.ts b/src/server/server.ts index dfc5f58a1d6..1c5059351d0 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -190,7 +190,7 @@ namespace ts.server { if (option && value) { switch (option) { case "-file": - logEnv.file = value; + logEnv.file = stripQuotes(value); break; case "-level": const level: LogLevel = (LogLevel)[value]; From 3721a2d799633d8604eab05a7b2791923459f0e0 Mon Sep 17 00:00:00 2001 From: Yui Date: Tue, 19 Jul 2016 10:53:04 -0700 Subject: [PATCH 104/307] [Release-2.0] Fix 9782: do not report blocked-scope-used-before-declaration error in ambient context (#9789) * Do not report block-scoped-used-before-declaration in ambient context * Add tests and baselines --- src/compiler/checker.ts | 2 +- .../noUsedBeforeDefinedErrorInAmbientContext1.symbols | 9 +++++++++ .../noUsedBeforeDefinedErrorInAmbientContext1.types | 9 +++++++++ .../noUsedBeforeDefinedErrorInAmbientContext1.ts | 4 ++++ 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 tests/baselines/reference/noUsedBeforeDefinedErrorInAmbientContext1.symbols create mode 100644 tests/baselines/reference/noUsedBeforeDefinedErrorInAmbientContext1.types create mode 100644 tests/cases/compiler/noUsedBeforeDefinedErrorInAmbientContext1.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index d68502dc8e8..613fae7173a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -976,7 +976,7 @@ namespace ts { Debug.assert(declaration !== undefined, "Block-scoped variable declaration is undefined"); - if (!isBlockScopedNameDeclaredBeforeUse(getAncestor(declaration, SyntaxKind.VariableDeclaration), errorLocation)) { + if (!isInAmbientContext(declaration) && !isBlockScopedNameDeclaredBeforeUse(getAncestor(declaration, SyntaxKind.VariableDeclaration), errorLocation)) { error(errorLocation, Diagnostics.Block_scoped_variable_0_used_before_its_declaration, declarationNameToString(declaration.name)); } } diff --git a/tests/baselines/reference/noUsedBeforeDefinedErrorInAmbientContext1.symbols b/tests/baselines/reference/noUsedBeforeDefinedErrorInAmbientContext1.symbols new file mode 100644 index 00000000000..690a19ef18f --- /dev/null +++ b/tests/baselines/reference/noUsedBeforeDefinedErrorInAmbientContext1.symbols @@ -0,0 +1,9 @@ +=== tests/cases/compiler/test.d.ts === + +declare var S: typeof A; // no error +>S : Symbol(S, Decl(test.d.ts, 1, 11)) +>A : Symbol(A, Decl(test.d.ts, 2, 13)) + +declare const A: number; +>A : Symbol(A, Decl(test.d.ts, 2, 13)) + diff --git a/tests/baselines/reference/noUsedBeforeDefinedErrorInAmbientContext1.types b/tests/baselines/reference/noUsedBeforeDefinedErrorInAmbientContext1.types new file mode 100644 index 00000000000..dfacda30915 --- /dev/null +++ b/tests/baselines/reference/noUsedBeforeDefinedErrorInAmbientContext1.types @@ -0,0 +1,9 @@ +=== tests/cases/compiler/test.d.ts === + +declare var S: typeof A; // no error +>S : number +>A : number + +declare const A: number; +>A : number + diff --git a/tests/cases/compiler/noUsedBeforeDefinedErrorInAmbientContext1.ts b/tests/cases/compiler/noUsedBeforeDefinedErrorInAmbientContext1.ts new file mode 100644 index 00000000000..834356e88b4 --- /dev/null +++ b/tests/cases/compiler/noUsedBeforeDefinedErrorInAmbientContext1.ts @@ -0,0 +1,4 @@ +// @filename: test.d.ts + +declare var S: typeof A; // no error +declare const A: number; \ No newline at end of file From 1ef73758347bbb34f20920ad2ed77e019964a3d9 Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Thu, 7 Jul 2016 11:52:07 -0700 Subject: [PATCH 105/307] Make TemplateStringsArray completely immutable. --- src/lib/es5.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/es5.d.ts b/src/lib/es5.d.ts index 54ae1209cb3..07b5395b4fb 100644 --- a/src/lib/es5.d.ts +++ b/src/lib/es5.d.ts @@ -513,8 +513,8 @@ interface NumberConstructor { /** An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers. */ declare const Number: NumberConstructor; -interface TemplateStringsArray extends Array { - readonly raw: string[]; +interface TemplateStringsArray extends ReadonlyArray { + readonly raw: ReadonlyArray } interface Math { From 3470ef5cbc07622816d396e3548a3b1e535429f5 Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Fri, 8 Jul 2016 17:13:12 -0700 Subject: [PATCH 106/307] Fixed up tests that used 'string[]' instead of 'TemplateStringsArray'. --- ...gedTemplateStringsTypeArgumentInference.ts | 22 +++++++++---------- ...TemplateStringsTypeArgumentInferenceES6.ts | 22 +++++++++---------- ...emplateStringsWithIncompatibleTypedTags.ts | 2 +- ...lateStringsWithIncompatibleTypedTagsES6.ts | 2 +- ...StringsWithManyCallAndMemberExpressions.ts | 2 +- ...ingsWithManyCallAndMemberExpressionsES6.ts | 2 +- ...dTemplateStringsWithOverloadResolution1.ts | 8 +++---- ...plateStringsWithOverloadResolution1_ES6.ts | 8 +++---- ...dTemplateStringsWithOverloadResolution2.ts | 8 +++---- ...plateStringsWithOverloadResolution2_ES6.ts | 8 +++---- .../taggedTemplateStringsWithTypedTags.ts | 2 +- .../taggedTemplateStringsWithTypedTagsES6.ts | 2 +- ...eHelpTaggedTemplatesWithOverloadedTags3.ts | 8 +++---- ...eHelpTaggedTemplatesWithOverloadedTags7.ts | 8 +++---- 14 files changed, 52 insertions(+), 52 deletions(-) diff --git a/tests/cases/conformance/es6/templates/taggedTemplateStringsTypeArgumentInference.ts b/tests/cases/conformance/es6/templates/taggedTemplateStringsTypeArgumentInference.ts index 961544f309a..17f2ea6b848 100644 --- a/tests/cases/conformance/es6/templates/taggedTemplateStringsTypeArgumentInference.ts +++ b/tests/cases/conformance/es6/templates/taggedTemplateStringsTypeArgumentInference.ts @@ -5,60 +5,60 @@ function noParams(n: T) { } noParams ``; // Generic tag with parameter which does not use type parameter -function noGenericParams(n: string[]) { } +function noGenericParams(n: TemplateStringsArray) { } noGenericParams ``; // Generic tag with multiple type parameters and only one used in parameter type annotation function someGenerics1a(n: T, m: number) { } someGenerics1a `${3}`; -function someGenerics1b(n: string[], m: U) { } +function someGenerics1b(n: TemplateStringsArray, m: U) { } someGenerics1b `${3}`; // Generic tag with argument of function type whose parameter is of type parameter type -function someGenerics2a(strs: string[], n: (x: T) => void) { } +function someGenerics2a(strs: TemplateStringsArray, n: (x: T) => void) { } someGenerics2a `${(n: string) => n}`; -function someGenerics2b(strs: string[], n: (x: T, y: U) => void) { } +function someGenerics2b(strs: TemplateStringsArray, n: (x: T, y: U) => void) { } someGenerics2b `${ (n: string, x: number) => n }`; // Generic tag with argument of function type whose parameter is not of type parameter type but body/return type uses type parameter -function someGenerics3(strs: string[], producer: () => T) { } +function someGenerics3(strs: TemplateStringsArray, producer: () => T) { } someGenerics3 `${() => ''}`; someGenerics3 `${() => undefined}`; someGenerics3 `${() => 3}`; // 2 parameter generic tag with argument 1 of type parameter type and argument 2 of function type whose parameter is of type parameter type -function someGenerics4(strs: string[], n: T, f: (x: U) => void) { } +function someGenerics4(strs: TemplateStringsArray, n: T, f: (x: U) => void) { } someGenerics4 `${4}${ () => null }`; someGenerics4 `${''}${ () => 3 }`; someGenerics4 `${ null }${ null }`; // 2 parameter generic tag with argument 2 of type parameter type and argument 1 of function type whose parameter is of type parameter type -function someGenerics5(strs: string[], n: T, f: (x: U) => void) { } +function someGenerics5(strs: TemplateStringsArray, n: T, f: (x: U) => void) { } someGenerics5 `${ 4 } ${ () => null }`; someGenerics5 `${ '' }${ () => 3 }`; someGenerics5 `${null}${null}`; // Generic tag with multiple arguments of function types that each have parameters of the same generic type -function someGenerics6(strs: string[], a: (a: A) => A, b: (b: A) => A, c: (c: A) => A) { } +function someGenerics6(strs: TemplateStringsArray, a: (a: A) => A, b: (b: A) => A, c: (c: A) => A) { } someGenerics6 `${ n => n }${ n => n}${ n => n}`; someGenerics6 `${ n => n }${ n => n}${ n => n}`; someGenerics6 `${ (n: number) => n }${ (n: number) => n }${ (n: number) => n }`; // Generic tag with multiple arguments of function types that each have parameters of different generic type -function someGenerics7(strs: string[], a: (a: A) => A, b: (b: B) => B, c: (c: C) => C) { } +function someGenerics7(strs: TemplateStringsArray, a: (a: A) => A, b: (b: B) => B, c: (c: C) => C) { } someGenerics7 `${ n => n }${ n => n }${ n => n }`; someGenerics7 `${ n => n }${ n => n }${ n => n }`; someGenerics7 `${(n: number) => n}${ (n: string) => n}${ (n: number) => n}`; // Generic tag with argument of generic function type -function someGenerics8(strs: string[], n: T): T { return n; } +function someGenerics8(strs: TemplateStringsArray, n: T): T { return n; } var x = someGenerics8 `${ someGenerics7 }`; x `${null}${null}${null}`; // Generic tag with multiple parameters of generic type passed arguments with no best common type -function someGenerics9(strs: string[], a: T, b: T, c: T): T { +function someGenerics9(strs: TemplateStringsArray, a: T, b: T, c: T): T { return null; } var a9a = someGenerics9 `${ '' }${ 0 }${ [] }`; diff --git a/tests/cases/conformance/es6/templates/taggedTemplateStringsTypeArgumentInferenceES6.ts b/tests/cases/conformance/es6/templates/taggedTemplateStringsTypeArgumentInferenceES6.ts index bc9fd75cac9..a0ca1aa67d6 100644 --- a/tests/cases/conformance/es6/templates/taggedTemplateStringsTypeArgumentInferenceES6.ts +++ b/tests/cases/conformance/es6/templates/taggedTemplateStringsTypeArgumentInferenceES6.ts @@ -5,60 +5,60 @@ function noParams(n: T) { } noParams ``; // Generic tag with parameter which does not use type parameter -function noGenericParams(n: string[]) { } +function noGenericParams(n: TemplateStringsArray) { } noGenericParams ``; // Generic tag with multiple type parameters and only one used in parameter type annotation function someGenerics1a(n: T, m: number) { } someGenerics1a `${3}`; -function someGenerics1b(n: string[], m: U) { } +function someGenerics1b(n: TemplateStringsArray, m: U) { } someGenerics1b `${3}`; // Generic tag with argument of function type whose parameter is of type parameter type -function someGenerics2a(strs: string[], n: (x: T) => void) { } +function someGenerics2a(strs: TemplateStringsArray, n: (x: T) => void) { } someGenerics2a `${(n: string) => n}`; -function someGenerics2b(strs: string[], n: (x: T, y: U) => void) { } +function someGenerics2b(strs: TemplateStringsArray, n: (x: T, y: U) => void) { } someGenerics2b `${ (n: string, x: number) => n }`; // Generic tag with argument of function type whose parameter is not of type parameter type but body/return type uses type parameter -function someGenerics3(strs: string[], producer: () => T) { } +function someGenerics3(strs: TemplateStringsArray, producer: () => T) { } someGenerics3 `${() => ''}`; someGenerics3 `${() => undefined}`; someGenerics3 `${() => 3}`; // 2 parameter generic tag with argument 1 of type parameter type and argument 2 of function type whose parameter is of type parameter type -function someGenerics4(strs: string[], n: T, f: (x: U) => void) { } +function someGenerics4(strs: TemplateStringsArray, n: T, f: (x: U) => void) { } someGenerics4 `${4}${ () => null }`; someGenerics4 `${''}${ () => 3 }`; someGenerics4 `${ null }${ null }`; // 2 parameter generic tag with argument 2 of type parameter type and argument 1 of function type whose parameter is of type parameter type -function someGenerics5(strs: string[], n: T, f: (x: U) => void) { } +function someGenerics5(strs: TemplateStringsArray, n: T, f: (x: U) => void) { } someGenerics5 `${ 4 } ${ () => null }`; someGenerics5 `${ '' }${ () => 3 }`; someGenerics5 `${null}${null}`; // Generic tag with multiple arguments of function types that each have parameters of the same generic type -function someGenerics6(strs: string[], a: (a: A) => A, b: (b: A) => A, c: (c: A) => A) { } +function someGenerics6(strs: TemplateStringsArray, a: (a: A) => A, b: (b: A) => A, c: (c: A) => A) { } someGenerics6 `${ n => n }${ n => n}${ n => n}`; someGenerics6 `${ n => n }${ n => n}${ n => n}`; someGenerics6 `${ (n: number) => n }${ (n: number) => n }${ (n: number) => n }`; // Generic tag with multiple arguments of function types that each have parameters of different generic type -function someGenerics7(strs: string[], a: (a: A) => A, b: (b: B) => B, c: (c: C) => C) { } +function someGenerics7(strs: TemplateStringsArray, a: (a: A) => A, b: (b: B) => B, c: (c: C) => C) { } someGenerics7 `${ n => n }${ n => n }${ n => n }`; someGenerics7 `${ n => n }${ n => n }${ n => n }`; someGenerics7 `${(n: number) => n}${ (n: string) => n}${ (n: number) => n}`; // Generic tag with argument of generic function type -function someGenerics8(strs: string[], n: T): T { return n; } +function someGenerics8(strs: TemplateStringsArray, n: T): T { return n; } var x = someGenerics8 `${ someGenerics7 }`; x `${null}${null}${null}`; // Generic tag with multiple parameters of generic type passed arguments with no best common type -function someGenerics9(strs: string[], a: T, b: T, c: T): T { +function someGenerics9(strs: TemplateStringsArray, a: T, b: T, c: T): T { return null; } var a9a = someGenerics9 `${ '' }${ 0 }${ [] }`; diff --git a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithIncompatibleTypedTags.ts b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithIncompatibleTypedTags.ts index 0f5d203d60b..d8be6cbd7c2 100644 --- a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithIncompatibleTypedTags.ts +++ b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithIncompatibleTypedTags.ts @@ -1,5 +1,5 @@ interface I { - (stringParts: string[], ...rest: boolean[]): I; + (stringParts: TemplateStringsArray, ...rest: boolean[]): I; g: I; h: I; member: I; diff --git a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithIncompatibleTypedTagsES6.ts b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithIncompatibleTypedTagsES6.ts index 36dc249f672..dec483c1678 100644 --- a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithIncompatibleTypedTagsES6.ts +++ b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithIncompatibleTypedTagsES6.ts @@ -1,6 +1,6 @@ // @target: ES6 interface I { - (stringParts: string[], ...rest: boolean[]): I; + (stringParts: TemplateStringsArray, ...rest: boolean[]): I; g: I; h: I; member: I; diff --git a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithManyCallAndMemberExpressions.ts b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithManyCallAndMemberExpressions.ts index d96196ee26e..f86353e7dda 100644 --- a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithManyCallAndMemberExpressions.ts +++ b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithManyCallAndMemberExpressions.ts @@ -1,5 +1,5 @@ interface I { - (strs: string[], ...subs: number[]): I; + (strs: TemplateStringsArray, ...subs: number[]): I; member: { new (s: string): { new (n: number): { diff --git a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts index d0d6604cfde..914f7ca809f 100644 --- a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts +++ b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts @@ -1,6 +1,6 @@ // @target: ES6 interface I { - (strs: string[], ...subs: number[]): I; + (strs: TemplateStringsArray, ...subs: number[]): I; member: { new (s: string): { new (n: number): { diff --git a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1.ts b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1.ts index 5b29beddd48..3743c1a7710 100644 --- a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1.ts +++ b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1.ts @@ -1,7 +1,7 @@ -function foo(strs: string[]): number; -function foo(strs: string[], x: number): string; -function foo(strs: string[], x: number, y: number): boolean; -function foo(strs: string[], x: number, y: string): {}; +function foo(strs: TemplateStringsArray): number; +function foo(strs: TemplateStringsArray, x: number): string; +function foo(strs: TemplateStringsArray, x: number, y: number): boolean; +function foo(strs: TemplateStringsArray, x: number, y: string): {}; function foo(...stuff: any[]): any { return undefined; } diff --git a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1_ES6.ts b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1_ES6.ts index f821d84fbdd..d681c6b98b0 100644 --- a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1_ES6.ts +++ b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1_ES6.ts @@ -1,8 +1,8 @@ //@target: es6 -function foo(strs: string[]): number; -function foo(strs: string[], x: number): string; -function foo(strs: string[], x: number, y: number): boolean; -function foo(strs: string[], x: number, y: string): {}; +function foo(strs: TemplateStringsArray): number; +function foo(strs: TemplateStringsArray, x: number): string; +function foo(strs: TemplateStringsArray, x: number, y: number): boolean; +function foo(strs: TemplateStringsArray, x: number, y: string): {}; function foo(...stuff: any[]): any { return undefined; } diff --git a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution2.ts b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution2.ts index 0ed9b86128b..98799dd92df 100644 --- a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution2.ts +++ b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution2.ts @@ -5,8 +5,8 @@ function foo1(...stuff: any[]): any { return undefined; } -var a = foo1 `${1}`; // string -var b = foo1([], 1); // number +var a = foo1 `${1}`; +var b = foo1([], 1); function foo2(strs: string[], x: number): number; function foo2(strs: TemplateStringsArray, x: number): string; @@ -14,5 +14,5 @@ function foo2(...stuff: any[]): any { return undefined; } -var c = foo2 `${1}`; // number -var d = foo2([], 1); // number \ No newline at end of file +var c = foo2 `${1}`; +var d = foo2([], 1); \ No newline at end of file diff --git a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution2_ES6.ts b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution2_ES6.ts index 6c67de325e2..769c56e47a1 100644 --- a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution2_ES6.ts +++ b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution2_ES6.ts @@ -5,8 +5,8 @@ function foo1(...stuff: any[]): any { return undefined; } -var a = foo1 `${1}`; // string -var b = foo1([], 1); // number +var a = foo1 `${1}`; +var b = foo1([], 1); function foo2(strs: string[], x: number): number; function foo2(strs: TemplateStringsArray, x: number): string; @@ -14,5 +14,5 @@ function foo2(...stuff: any[]): any { return undefined; } -var c = foo2 `${1}`; // number -var d = foo2([], 1); // number \ No newline at end of file +var c = foo2 `${1}`; +var d = foo2([], 1); \ No newline at end of file diff --git a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithTypedTags.ts b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithTypedTags.ts index 9b3852c2ddb..60f97104be3 100644 --- a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithTypedTags.ts +++ b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithTypedTags.ts @@ -1,5 +1,5 @@ interface I { - (stringParts: string[], ...rest: number[]): I; + (stringParts: TemplateStringsArray, ...rest: number[]): I; g: I; h: I; member: I; diff --git a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithTypedTagsES6.ts b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithTypedTagsES6.ts index 0dc10820a98..19fb6a874cc 100644 --- a/tests/cases/conformance/es6/templates/taggedTemplateStringsWithTypedTagsES6.ts +++ b/tests/cases/conformance/es6/templates/taggedTemplateStringsWithTypedTagsES6.ts @@ -1,6 +1,6 @@ // @target: ES6 interface I { - (stringParts: string[], ...rest: number[]): I; + (stringParts: TemplateStringsArray, ...rest: number[]): I; g: I; h: I; member: I; diff --git a/tests/cases/fourslash/signatureHelpTaggedTemplatesWithOverloadedTags3.ts b/tests/cases/fourslash/signatureHelpTaggedTemplatesWithOverloadedTags3.ts index 3843ed34b16..b919afc7703 100644 --- a/tests/cases/fourslash/signatureHelpTaggedTemplatesWithOverloadedTags3.ts +++ b/tests/cases/fourslash/signatureHelpTaggedTemplatesWithOverloadedTags3.ts @@ -1,8 +1,8 @@ /// -//// function f(templateStrings: string[], p1_o1: string): number; -//// function f(templateStrings: string[], p1_o2: number, p2_o2: number, p3_o2: number): string; -//// function f(templateStrings: string[], p1_o3: string, p2_o3: boolean, p3_o3: number): boolean; +//// function f(templateStrings: TemplateStringsArray, p1_o1: string): number; +//// function f(templateStrings: TemplateStringsArray, p1_o2: number, p2_o2: number, p3_o2: number): string; +//// function f(templateStrings: TemplateStringsArray, p1_o3: string, p2_o3: boolean, p3_o3: number): boolean; //// function f(...foo[]: any) { return ""; } //// //// f `${/*1*/ "s/*2*/tring" /*3*/ } ${ @@ -14,7 +14,7 @@ test.markers().forEach(m => { verify.signatureHelpArgumentCountIs(3); verify.currentSignatureParameterCountIs(4); - verify.currentSignatureHelpIs('f(templateStrings: string[], p1_o3: string, p2_o3: boolean, p3_o3: number): boolean'); + verify.currentSignatureHelpIs('f(templateStrings: TemplateStringsArray, p1_o3: string, p2_o3: boolean, p3_o3: number): boolean'); verify.currentParameterHelpArgumentNameIs("p1_o3"); verify.currentParameterSpanIs("p1_o3: string"); }); \ No newline at end of file diff --git a/tests/cases/fourslash/signatureHelpTaggedTemplatesWithOverloadedTags7.ts b/tests/cases/fourslash/signatureHelpTaggedTemplatesWithOverloadedTags7.ts index 9a24aacf86b..5f706b8c0fa 100644 --- a/tests/cases/fourslash/signatureHelpTaggedTemplatesWithOverloadedTags7.ts +++ b/tests/cases/fourslash/signatureHelpTaggedTemplatesWithOverloadedTags7.ts @@ -1,8 +1,8 @@ /// -//// function f(templateStrings: string[], p1_o1: string): number; -//// function f(templateStrings: string[], p1_o2: number, p2_o2: number, p3_o2: number): string; -//// function f(templateStrings: string[], p1_o3: string, p2_o3: boolean, p3_o3: number): boolean; +//// function f(templateStrings: TemplateStringsArray, p1_o1: string): number; +//// function f(templateStrings: TemplateStringsArray, p1_o2: number, p2_o2: number, p3_o2: number): string; +//// function f(templateStrings: TemplateStringsArray, p1_o3: string, p2_o3: boolean, p3_o3: number): boolean; //// function f(...foo[]: any) { return ""; } //// //// f `${ } ${/*1*/ fa/*2*/lse /*3*/} @@ -14,7 +14,7 @@ test.markers().forEach(m => { verify.signatureHelpArgumentCountIs(3); verify.currentSignatureParameterCountIs(4); - verify.currentSignatureHelpIs('f(templateStrings: string[], p1_o3: string, p2_o3: boolean, p3_o3: number): boolean'); + verify.currentSignatureHelpIs('f(templateStrings: TemplateStringsArray, p1_o3: string, p2_o3: boolean, p3_o3: number): boolean'); verify.currentParameterHelpArgumentNameIs("p2_o3"); verify.currentParameterSpanIs("p2_o3: boolean"); }); \ No newline at end of file From 6968ebf84e72bd166e320a0ea70afc57a635a956 Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Wed, 20 Jul 2016 18:07:45 -0700 Subject: [PATCH 107/307] Accepted baselines. --- ...ateStringsTypeArgumentInference.errors.txt | 22 +++++++------- ...gedTemplateStringsTypeArgumentInference.js | 22 +++++++------- ...StringsTypeArgumentInferenceES6.errors.txt | 22 +++++++------- ...TemplateStringsTypeArgumentInferenceES6.js | 22 +++++++------- ...tringsWithIncompatibleTypedTags.errors.txt | 2 +- ...emplateStringsWithIncompatibleTypedTags.js | 2 +- ...ngsWithIncompatibleTypedTagsES6.errors.txt | 2 +- ...lateStringsWithIncompatibleTypedTagsES6.js | 2 +- ...StringsWithManyCallAndMemberExpressions.js | 2 +- ...gsWithManyCallAndMemberExpressions.symbols | 11 +++---- ...ingsWithManyCallAndMemberExpressions.types | 5 ++-- ...ingsWithManyCallAndMemberExpressionsES6.js | 2 +- ...ithManyCallAndMemberExpressionsES6.symbols | 11 +++---- ...sWithManyCallAndMemberExpressionsES6.types | 5 ++-- ...eStringsWithOverloadResolution1.errors.txt | 30 ++++++++++++++----- ...dTemplateStringsWithOverloadResolution1.js | 8 ++--- ...ingsWithOverloadResolution1_ES6.errors.txt | 30 ++++++++++++++----- ...plateStringsWithOverloadResolution1_ES6.js | 8 ++--- ...dTemplateStringsWithOverloadResolution2.js | 16 +++++----- ...lateStringsWithOverloadResolution2.symbols | 8 ++--- ...mplateStringsWithOverloadResolution2.types | 12 ++++---- ...plateStringsWithOverloadResolution2_ES6.js | 16 +++++----- ...StringsWithOverloadResolution2_ES6.symbols | 8 ++--- ...teStringsWithOverloadResolution2_ES6.types | 12 ++++---- .../taggedTemplateStringsWithTypedTags.js | 2 +- ...taggedTemplateStringsWithTypedTags.symbols | 7 +++-- .../taggedTemplateStringsWithTypedTags.types | 5 ++-- .../taggedTemplateStringsWithTypedTagsES6.js | 2 +- ...gedTemplateStringsWithTypedTagsES6.symbols | 7 +++-- ...aggedTemplateStringsWithTypedTagsES6.types | 5 ++-- 30 files changed, 172 insertions(+), 136 deletions(-) diff --git a/tests/baselines/reference/taggedTemplateStringsTypeArgumentInference.errors.txt b/tests/baselines/reference/taggedTemplateStringsTypeArgumentInference.errors.txt index be5c21f1a0b..b345e971ab2 100644 --- a/tests/baselines/reference/taggedTemplateStringsTypeArgumentInference.errors.txt +++ b/tests/baselines/reference/taggedTemplateStringsTypeArgumentInference.errors.txt @@ -13,60 +13,60 @@ tests/cases/conformance/es6/templates/taggedTemplateStringsTypeArgumentInference noParams ``; // Generic tag with parameter which does not use type parameter - function noGenericParams(n: string[]) { } + function noGenericParams(n: TemplateStringsArray) { } noGenericParams ``; // Generic tag with multiple type parameters and only one used in parameter type annotation function someGenerics1a(n: T, m: number) { } someGenerics1a `${3}`; - function someGenerics1b(n: string[], m: U) { } + function someGenerics1b(n: TemplateStringsArray, m: U) { } someGenerics1b `${3}`; // Generic tag with argument of function type whose parameter is of type parameter type - function someGenerics2a(strs: string[], n: (x: T) => void) { } + function someGenerics2a(strs: TemplateStringsArray, n: (x: T) => void) { } someGenerics2a `${(n: string) => n}`; - function someGenerics2b(strs: string[], n: (x: T, y: U) => void) { } + function someGenerics2b(strs: TemplateStringsArray, n: (x: T, y: U) => void) { } someGenerics2b `${ (n: string, x: number) => n }`; // Generic tag with argument of function type whose parameter is not of type parameter type but body/return type uses type parameter - function someGenerics3(strs: string[], producer: () => T) { } + function someGenerics3(strs: TemplateStringsArray, producer: () => T) { } someGenerics3 `${() => ''}`; someGenerics3 `${() => undefined}`; someGenerics3 `${() => 3}`; // 2 parameter generic tag with argument 1 of type parameter type and argument 2 of function type whose parameter is of type parameter type - function someGenerics4(strs: string[], n: T, f: (x: U) => void) { } + function someGenerics4(strs: TemplateStringsArray, n: T, f: (x: U) => void) { } someGenerics4 `${4}${ () => null }`; someGenerics4 `${''}${ () => 3 }`; someGenerics4 `${ null }${ null }`; // 2 parameter generic tag with argument 2 of type parameter type and argument 1 of function type whose parameter is of type parameter type - function someGenerics5(strs: string[], n: T, f: (x: U) => void) { } + function someGenerics5(strs: TemplateStringsArray, n: T, f: (x: U) => void) { } someGenerics5 `${ 4 } ${ () => null }`; someGenerics5 `${ '' }${ () => 3 }`; someGenerics5 `${null}${null}`; // Generic tag with multiple arguments of function types that each have parameters of the same generic type - function someGenerics6(strs: string[], a: (a: A) => A, b: (b: A) => A, c: (c: A) => A) { } + function someGenerics6(strs: TemplateStringsArray, a: (a: A) => A, b: (b: A) => A, c: (c: A) => A) { } someGenerics6 `${ n => n }${ n => n}${ n => n}`; someGenerics6 `${ n => n }${ n => n}${ n => n}`; someGenerics6 `${ (n: number) => n }${ (n: number) => n }${ (n: number) => n }`; // Generic tag with multiple arguments of function types that each have parameters of different generic type - function someGenerics7(strs: string[], a: (a: A) => A, b: (b: B) => B, c: (c: C) => C) { } + function someGenerics7(strs: TemplateStringsArray, a: (a: A) => A, b: (b: B) => B, c: (c: C) => C) { } someGenerics7 `${ n => n }${ n => n }${ n => n }`; someGenerics7 `${ n => n }${ n => n }${ n => n }`; someGenerics7 `${(n: number) => n}${ (n: string) => n}${ (n: number) => n}`; // Generic tag with argument of generic function type - function someGenerics8(strs: string[], n: T): T { return n; } + function someGenerics8(strs: TemplateStringsArray, n: T): T { return n; } var x = someGenerics8 `${ someGenerics7 }`; x `${null}${null}${null}`; // Generic tag with multiple parameters of generic type passed arguments with no best common type - function someGenerics9(strs: string[], a: T, b: T, c: T): T { + function someGenerics9(strs: TemplateStringsArray, a: T, b: T, c: T): T { return null; } var a9a = someGenerics9 `${ '' }${ 0 }${ [] }`; diff --git a/tests/baselines/reference/taggedTemplateStringsTypeArgumentInference.js b/tests/baselines/reference/taggedTemplateStringsTypeArgumentInference.js index 1b6e3ab92c8..69dcd284183 100644 --- a/tests/baselines/reference/taggedTemplateStringsTypeArgumentInference.js +++ b/tests/baselines/reference/taggedTemplateStringsTypeArgumentInference.js @@ -6,60 +6,60 @@ function noParams(n: T) { } noParams ``; // Generic tag with parameter which does not use type parameter -function noGenericParams(n: string[]) { } +function noGenericParams(n: TemplateStringsArray) { } noGenericParams ``; // Generic tag with multiple type parameters and only one used in parameter type annotation function someGenerics1a(n: T, m: number) { } someGenerics1a `${3}`; -function someGenerics1b(n: string[], m: U) { } +function someGenerics1b(n: TemplateStringsArray, m: U) { } someGenerics1b `${3}`; // Generic tag with argument of function type whose parameter is of type parameter type -function someGenerics2a(strs: string[], n: (x: T) => void) { } +function someGenerics2a(strs: TemplateStringsArray, n: (x: T) => void) { } someGenerics2a `${(n: string) => n}`; -function someGenerics2b(strs: string[], n: (x: T, y: U) => void) { } +function someGenerics2b(strs: TemplateStringsArray, n: (x: T, y: U) => void) { } someGenerics2b `${ (n: string, x: number) => n }`; // Generic tag with argument of function type whose parameter is not of type parameter type but body/return type uses type parameter -function someGenerics3(strs: string[], producer: () => T) { } +function someGenerics3(strs: TemplateStringsArray, producer: () => T) { } someGenerics3 `${() => ''}`; someGenerics3 `${() => undefined}`; someGenerics3 `${() => 3}`; // 2 parameter generic tag with argument 1 of type parameter type and argument 2 of function type whose parameter is of type parameter type -function someGenerics4(strs: string[], n: T, f: (x: U) => void) { } +function someGenerics4(strs: TemplateStringsArray, n: T, f: (x: U) => void) { } someGenerics4 `${4}${ () => null }`; someGenerics4 `${''}${ () => 3 }`; someGenerics4 `${ null }${ null }`; // 2 parameter generic tag with argument 2 of type parameter type and argument 1 of function type whose parameter is of type parameter type -function someGenerics5(strs: string[], n: T, f: (x: U) => void) { } +function someGenerics5(strs: TemplateStringsArray, n: T, f: (x: U) => void) { } someGenerics5 `${ 4 } ${ () => null }`; someGenerics5 `${ '' }${ () => 3 }`; someGenerics5 `${null}${null}`; // Generic tag with multiple arguments of function types that each have parameters of the same generic type -function someGenerics6(strs: string[], a: (a: A) => A, b: (b: A) => A, c: (c: A) => A) { } +function someGenerics6(strs: TemplateStringsArray, a: (a: A) => A, b: (b: A) => A, c: (c: A) => A) { } someGenerics6 `${ n => n }${ n => n}${ n => n}`; someGenerics6 `${ n => n }${ n => n}${ n => n}`; someGenerics6 `${ (n: number) => n }${ (n: number) => n }${ (n: number) => n }`; // Generic tag with multiple arguments of function types that each have parameters of different generic type -function someGenerics7(strs: string[], a: (a: A) => A, b: (b: B) => B, c: (c: C) => C) { } +function someGenerics7(strs: TemplateStringsArray, a: (a: A) => A, b: (b: B) => B, c: (c: C) => C) { } someGenerics7 `${ n => n }${ n => n }${ n => n }`; someGenerics7 `${ n => n }${ n => n }${ n => n }`; someGenerics7 `${(n: number) => n}${ (n: string) => n}${ (n: number) => n}`; // Generic tag with argument of generic function type -function someGenerics8(strs: string[], n: T): T { return n; } +function someGenerics8(strs: TemplateStringsArray, n: T): T { return n; } var x = someGenerics8 `${ someGenerics7 }`; x `${null}${null}${null}`; // Generic tag with multiple parameters of generic type passed arguments with no best common type -function someGenerics9(strs: string[], a: T, b: T, c: T): T { +function someGenerics9(strs: TemplateStringsArray, a: T, b: T, c: T): T { return null; } var a9a = someGenerics9 `${ '' }${ 0 }${ [] }`; diff --git a/tests/baselines/reference/taggedTemplateStringsTypeArgumentInferenceES6.errors.txt b/tests/baselines/reference/taggedTemplateStringsTypeArgumentInferenceES6.errors.txt index 63d04a42b3b..619e5081a3b 100644 --- a/tests/baselines/reference/taggedTemplateStringsTypeArgumentInferenceES6.errors.txt +++ b/tests/baselines/reference/taggedTemplateStringsTypeArgumentInferenceES6.errors.txt @@ -12,60 +12,60 @@ tests/cases/conformance/es6/templates/taggedTemplateStringsTypeArgumentInference noParams ``; // Generic tag with parameter which does not use type parameter - function noGenericParams(n: string[]) { } + function noGenericParams(n: TemplateStringsArray) { } noGenericParams ``; // Generic tag with multiple type parameters and only one used in parameter type annotation function someGenerics1a(n: T, m: number) { } someGenerics1a `${3}`; - function someGenerics1b(n: string[], m: U) { } + function someGenerics1b(n: TemplateStringsArray, m: U) { } someGenerics1b `${3}`; // Generic tag with argument of function type whose parameter is of type parameter type - function someGenerics2a(strs: string[], n: (x: T) => void) { } + function someGenerics2a(strs: TemplateStringsArray, n: (x: T) => void) { } someGenerics2a `${(n: string) => n}`; - function someGenerics2b(strs: string[], n: (x: T, y: U) => void) { } + function someGenerics2b(strs: TemplateStringsArray, n: (x: T, y: U) => void) { } someGenerics2b `${ (n: string, x: number) => n }`; // Generic tag with argument of function type whose parameter is not of type parameter type but body/return type uses type parameter - function someGenerics3(strs: string[], producer: () => T) { } + function someGenerics3(strs: TemplateStringsArray, producer: () => T) { } someGenerics3 `${() => ''}`; someGenerics3 `${() => undefined}`; someGenerics3 `${() => 3}`; // 2 parameter generic tag with argument 1 of type parameter type and argument 2 of function type whose parameter is of type parameter type - function someGenerics4(strs: string[], n: T, f: (x: U) => void) { } + function someGenerics4(strs: TemplateStringsArray, n: T, f: (x: U) => void) { } someGenerics4 `${4}${ () => null }`; someGenerics4 `${''}${ () => 3 }`; someGenerics4 `${ null }${ null }`; // 2 parameter generic tag with argument 2 of type parameter type and argument 1 of function type whose parameter is of type parameter type - function someGenerics5(strs: string[], n: T, f: (x: U) => void) { } + function someGenerics5(strs: TemplateStringsArray, n: T, f: (x: U) => void) { } someGenerics5 `${ 4 } ${ () => null }`; someGenerics5 `${ '' }${ () => 3 }`; someGenerics5 `${null}${null}`; // Generic tag with multiple arguments of function types that each have parameters of the same generic type - function someGenerics6(strs: string[], a: (a: A) => A, b: (b: A) => A, c: (c: A) => A) { } + function someGenerics6(strs: TemplateStringsArray, a: (a: A) => A, b: (b: A) => A, c: (c: A) => A) { } someGenerics6 `${ n => n }${ n => n}${ n => n}`; someGenerics6 `${ n => n }${ n => n}${ n => n}`; someGenerics6 `${ (n: number) => n }${ (n: number) => n }${ (n: number) => n }`; // Generic tag with multiple arguments of function types that each have parameters of different generic type - function someGenerics7(strs: string[], a: (a: A) => A, b: (b: B) => B, c: (c: C) => C) { } + function someGenerics7(strs: TemplateStringsArray, a: (a: A) => A, b: (b: B) => B, c: (c: C) => C) { } someGenerics7 `${ n => n }${ n => n }${ n => n }`; someGenerics7 `${ n => n }${ n => n }${ n => n }`; someGenerics7 `${(n: number) => n}${ (n: string) => n}${ (n: number) => n}`; // Generic tag with argument of generic function type - function someGenerics8(strs: string[], n: T): T { return n; } + function someGenerics8(strs: TemplateStringsArray, n: T): T { return n; } var x = someGenerics8 `${ someGenerics7 }`; x `${null}${null}${null}`; // Generic tag with multiple parameters of generic type passed arguments with no best common type - function someGenerics9(strs: string[], a: T, b: T, c: T): T { + function someGenerics9(strs: TemplateStringsArray, a: T, b: T, c: T): T { return null; } var a9a = someGenerics9 `${ '' }${ 0 }${ [] }`; diff --git a/tests/baselines/reference/taggedTemplateStringsTypeArgumentInferenceES6.js b/tests/baselines/reference/taggedTemplateStringsTypeArgumentInferenceES6.js index 2f57882b4b7..3097e40c9ad 100644 --- a/tests/baselines/reference/taggedTemplateStringsTypeArgumentInferenceES6.js +++ b/tests/baselines/reference/taggedTemplateStringsTypeArgumentInferenceES6.js @@ -5,60 +5,60 @@ function noParams(n: T) { } noParams ``; // Generic tag with parameter which does not use type parameter -function noGenericParams(n: string[]) { } +function noGenericParams(n: TemplateStringsArray) { } noGenericParams ``; // Generic tag with multiple type parameters and only one used in parameter type annotation function someGenerics1a(n: T, m: number) { } someGenerics1a `${3}`; -function someGenerics1b(n: string[], m: U) { } +function someGenerics1b(n: TemplateStringsArray, m: U) { } someGenerics1b `${3}`; // Generic tag with argument of function type whose parameter is of type parameter type -function someGenerics2a(strs: string[], n: (x: T) => void) { } +function someGenerics2a(strs: TemplateStringsArray, n: (x: T) => void) { } someGenerics2a `${(n: string) => n}`; -function someGenerics2b(strs: string[], n: (x: T, y: U) => void) { } +function someGenerics2b(strs: TemplateStringsArray, n: (x: T, y: U) => void) { } someGenerics2b `${ (n: string, x: number) => n }`; // Generic tag with argument of function type whose parameter is not of type parameter type but body/return type uses type parameter -function someGenerics3(strs: string[], producer: () => T) { } +function someGenerics3(strs: TemplateStringsArray, producer: () => T) { } someGenerics3 `${() => ''}`; someGenerics3 `${() => undefined}`; someGenerics3 `${() => 3}`; // 2 parameter generic tag with argument 1 of type parameter type and argument 2 of function type whose parameter is of type parameter type -function someGenerics4(strs: string[], n: T, f: (x: U) => void) { } +function someGenerics4(strs: TemplateStringsArray, n: T, f: (x: U) => void) { } someGenerics4 `${4}${ () => null }`; someGenerics4 `${''}${ () => 3 }`; someGenerics4 `${ null }${ null }`; // 2 parameter generic tag with argument 2 of type parameter type and argument 1 of function type whose parameter is of type parameter type -function someGenerics5(strs: string[], n: T, f: (x: U) => void) { } +function someGenerics5(strs: TemplateStringsArray, n: T, f: (x: U) => void) { } someGenerics5 `${ 4 } ${ () => null }`; someGenerics5 `${ '' }${ () => 3 }`; someGenerics5 `${null}${null}`; // Generic tag with multiple arguments of function types that each have parameters of the same generic type -function someGenerics6(strs: string[], a: (a: A) => A, b: (b: A) => A, c: (c: A) => A) { } +function someGenerics6(strs: TemplateStringsArray, a: (a: A) => A, b: (b: A) => A, c: (c: A) => A) { } someGenerics6 `${ n => n }${ n => n}${ n => n}`; someGenerics6 `${ n => n }${ n => n}${ n => n}`; someGenerics6 `${ (n: number) => n }${ (n: number) => n }${ (n: number) => n }`; // Generic tag with multiple arguments of function types that each have parameters of different generic type -function someGenerics7(strs: string[], a: (a: A) => A, b: (b: B) => B, c: (c: C) => C) { } +function someGenerics7(strs: TemplateStringsArray, a: (a: A) => A, b: (b: B) => B, c: (c: C) => C) { } someGenerics7 `${ n => n }${ n => n }${ n => n }`; someGenerics7 `${ n => n }${ n => n }${ n => n }`; someGenerics7 `${(n: number) => n}${ (n: string) => n}${ (n: number) => n}`; // Generic tag with argument of generic function type -function someGenerics8(strs: string[], n: T): T { return n; } +function someGenerics8(strs: TemplateStringsArray, n: T): T { return n; } var x = someGenerics8 `${ someGenerics7 }`; x `${null}${null}${null}`; // Generic tag with multiple parameters of generic type passed arguments with no best common type -function someGenerics9(strs: string[], a: T, b: T, c: T): T { +function someGenerics9(strs: TemplateStringsArray, a: T, b: T, c: T): T { return null; } var a9a = someGenerics9 `${ '' }${ 0 }${ [] }`; diff --git a/tests/baselines/reference/taggedTemplateStringsWithIncompatibleTypedTags.errors.txt b/tests/baselines/reference/taggedTemplateStringsWithIncompatibleTypedTags.errors.txt index 166769fbbf5..44b30fb535f 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithIncompatibleTypedTags.errors.txt +++ b/tests/baselines/reference/taggedTemplateStringsWithIncompatibleTypedTags.errors.txt @@ -8,7 +8,7 @@ tests/cases/conformance/es6/templates/taggedTemplateStringsWithIncompatibleTyped ==== tests/cases/conformance/es6/templates/taggedTemplateStringsWithIncompatibleTypedTags.ts (6 errors) ==== interface I { - (stringParts: string[], ...rest: boolean[]): I; + (stringParts: TemplateStringsArray, ...rest: boolean[]): I; g: I; h: I; member: I; diff --git a/tests/baselines/reference/taggedTemplateStringsWithIncompatibleTypedTags.js b/tests/baselines/reference/taggedTemplateStringsWithIncompatibleTypedTags.js index 9d113793228..511928e1342 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithIncompatibleTypedTags.js +++ b/tests/baselines/reference/taggedTemplateStringsWithIncompatibleTypedTags.js @@ -1,6 +1,6 @@ //// [taggedTemplateStringsWithIncompatibleTypedTags.ts] interface I { - (stringParts: string[], ...rest: boolean[]): I; + (stringParts: TemplateStringsArray, ...rest: boolean[]): I; g: I; h: I; member: I; diff --git a/tests/baselines/reference/taggedTemplateStringsWithIncompatibleTypedTagsES6.errors.txt b/tests/baselines/reference/taggedTemplateStringsWithIncompatibleTypedTagsES6.errors.txt index 89b48b0193c..c2825c19e63 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithIncompatibleTypedTagsES6.errors.txt +++ b/tests/baselines/reference/taggedTemplateStringsWithIncompatibleTypedTagsES6.errors.txt @@ -8,7 +8,7 @@ tests/cases/conformance/es6/templates/taggedTemplateStringsWithIncompatibleTyped ==== tests/cases/conformance/es6/templates/taggedTemplateStringsWithIncompatibleTypedTagsES6.ts (6 errors) ==== interface I { - (stringParts: string[], ...rest: boolean[]): I; + (stringParts: TemplateStringsArray, ...rest: boolean[]): I; g: I; h: I; member: I; diff --git a/tests/baselines/reference/taggedTemplateStringsWithIncompatibleTypedTagsES6.js b/tests/baselines/reference/taggedTemplateStringsWithIncompatibleTypedTagsES6.js index 8b636356068..99fa7c3d550 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithIncompatibleTypedTagsES6.js +++ b/tests/baselines/reference/taggedTemplateStringsWithIncompatibleTypedTagsES6.js @@ -1,6 +1,6 @@ //// [taggedTemplateStringsWithIncompatibleTypedTagsES6.ts] interface I { - (stringParts: string[], ...rest: boolean[]): I; + (stringParts: TemplateStringsArray, ...rest: boolean[]): I; g: I; h: I; member: I; diff --git a/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressions.js b/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressions.js index 1814816283b..a896a811ffb 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressions.js +++ b/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressions.js @@ -1,6 +1,6 @@ //// [taggedTemplateStringsWithManyCallAndMemberExpressions.ts] interface I { - (strs: string[], ...subs: number[]): I; + (strs: TemplateStringsArray, ...subs: number[]): I; member: { new (s: string): { new (n: number): { diff --git a/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressions.symbols b/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressions.symbols index 226729907de..c8883fda4fe 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressions.symbols +++ b/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressions.symbols @@ -2,13 +2,14 @@ interface I { >I : Symbol(I, Decl(taggedTemplateStringsWithManyCallAndMemberExpressions.ts, 0, 0)) - (strs: string[], ...subs: number[]): I; + (strs: TemplateStringsArray, ...subs: number[]): I; >strs : Symbol(strs, Decl(taggedTemplateStringsWithManyCallAndMemberExpressions.ts, 1, 5)) ->subs : Symbol(subs, Decl(taggedTemplateStringsWithManyCallAndMemberExpressions.ts, 1, 20)) +>TemplateStringsArray : Symbol(TemplateStringsArray, Decl(lib.d.ts, --, --)) +>subs : Symbol(subs, Decl(taggedTemplateStringsWithManyCallAndMemberExpressions.ts, 1, 32)) >I : Symbol(I, Decl(taggedTemplateStringsWithManyCallAndMemberExpressions.ts, 0, 0)) member: { ->member : Symbol(I.member, Decl(taggedTemplateStringsWithManyCallAndMemberExpressions.ts, 1, 43)) +>member : Symbol(I.member, Decl(taggedTemplateStringsWithManyCallAndMemberExpressions.ts, 1, 55)) new (s: string): { >s : Symbol(s, Decl(taggedTemplateStringsWithManyCallAndMemberExpressions.ts, 3, 13)) @@ -27,8 +28,8 @@ var f: I; var x = new new new f `abc${ 0 }def`.member("hello")(42) === true; >x : Symbol(x, Decl(taggedTemplateStringsWithManyCallAndMemberExpressions.ts, 12, 3)) ->f `abc${ 0 }def`.member : Symbol(I.member, Decl(taggedTemplateStringsWithManyCallAndMemberExpressions.ts, 1, 43)) +>f `abc${ 0 }def`.member : Symbol(I.member, Decl(taggedTemplateStringsWithManyCallAndMemberExpressions.ts, 1, 55)) >f : Symbol(f, Decl(taggedTemplateStringsWithManyCallAndMemberExpressions.ts, 10, 3)) ->member : Symbol(I.member, Decl(taggedTemplateStringsWithManyCallAndMemberExpressions.ts, 1, 43)) +>member : Symbol(I.member, Decl(taggedTemplateStringsWithManyCallAndMemberExpressions.ts, 1, 55)) diff --git a/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressions.types b/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressions.types index 83a0f2cf9f4..553cda2b2b9 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressions.types +++ b/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressions.types @@ -2,8 +2,9 @@ interface I { >I : I - (strs: string[], ...subs: number[]): I; ->strs : string[] + (strs: TemplateStringsArray, ...subs: number[]): I; +>strs : TemplateStringsArray +>TemplateStringsArray : TemplateStringsArray >subs : number[] >I : I diff --git a/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressionsES6.js b/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressionsES6.js index b985860f065..f1acbaa1546 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressionsES6.js +++ b/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressionsES6.js @@ -1,6 +1,6 @@ //// [taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts] interface I { - (strs: string[], ...subs: number[]): I; + (strs: TemplateStringsArray, ...subs: number[]): I; member: { new (s: string): { new (n: number): { diff --git a/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressionsES6.symbols b/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressionsES6.symbols index 6535e998808..0d163daa640 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressionsES6.symbols +++ b/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressionsES6.symbols @@ -2,13 +2,14 @@ interface I { >I : Symbol(I, Decl(taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts, 0, 0)) - (strs: string[], ...subs: number[]): I; + (strs: TemplateStringsArray, ...subs: number[]): I; >strs : Symbol(strs, Decl(taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts, 1, 5)) ->subs : Symbol(subs, Decl(taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts, 1, 20)) +>TemplateStringsArray : Symbol(TemplateStringsArray, Decl(lib.es5.d.ts, --, --)) +>subs : Symbol(subs, Decl(taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts, 1, 32)) >I : Symbol(I, Decl(taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts, 0, 0)) member: { ->member : Symbol(I.member, Decl(taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts, 1, 43)) +>member : Symbol(I.member, Decl(taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts, 1, 55)) new (s: string): { >s : Symbol(s, Decl(taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts, 3, 13)) @@ -27,8 +28,8 @@ var f: I; var x = new new new f `abc${ 0 }def`.member("hello")(42) === true; >x : Symbol(x, Decl(taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts, 12, 3)) ->f `abc${ 0 }def`.member : Symbol(I.member, Decl(taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts, 1, 43)) +>f `abc${ 0 }def`.member : Symbol(I.member, Decl(taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts, 1, 55)) >f : Symbol(f, Decl(taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts, 10, 3)) ->member : Symbol(I.member, Decl(taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts, 1, 43)) +>member : Symbol(I.member, Decl(taggedTemplateStringsWithManyCallAndMemberExpressionsES6.ts, 1, 55)) diff --git a/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressionsES6.types b/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressionsES6.types index c240b7ced11..f9d7c605f7f 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressionsES6.types +++ b/tests/baselines/reference/taggedTemplateStringsWithManyCallAndMemberExpressionsES6.types @@ -2,8 +2,9 @@ interface I { >I : I - (strs: string[], ...subs: number[]): I; ->strs : string[] + (strs: TemplateStringsArray, ...subs: number[]): I; +>strs : TemplateStringsArray +>TemplateStringsArray : TemplateStringsArray >subs : number[] >I : I diff --git a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution1.errors.txt b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution1.errors.txt index 9e450cbb368..426eef8ce7f 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution1.errors.txt +++ b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution1.errors.txt @@ -1,25 +1,39 @@ -tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1.ts(12,20): error TS2345: Argument of type 'boolean' is not assignable to parameter of type 'string'. +tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1.ts(9,13): error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. + Property 'raw' is missing in type 'undefined[]'. +tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1.ts(10,13): error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. +tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1.ts(11,13): error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. +tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1.ts(12,13): error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. +tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1.ts(13,13): error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1.ts(14,9): error TS2346: Supplied parameters do not match any signature of call target. tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1.ts(19,20): error TS2345: Argument of type 'boolean' is not assignable to parameter of type 'string'. tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1.ts(21,9): error TS2346: Supplied parameters do not match any signature of call target. -==== tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1.ts (4 errors) ==== - function foo(strs: string[]): number; - function foo(strs: string[], x: number): string; - function foo(strs: string[], x: number, y: number): boolean; - function foo(strs: string[], x: number, y: string): {}; +==== tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1.ts (8 errors) ==== + function foo(strs: TemplateStringsArray): number; + function foo(strs: TemplateStringsArray, x: number): string; + function foo(strs: TemplateStringsArray, x: number, y: number): boolean; + function foo(strs: TemplateStringsArray, x: number, y: string): {}; function foo(...stuff: any[]): any { return undefined; } var a = foo([]); // number + ~~ +!!! error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. +!!! error TS2345: Property 'raw' is missing in type 'undefined[]'. var b = foo([], 1); // string + ~~ +!!! error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. var c = foo([], 1, 2); // boolean + ~~ +!!! error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. var d = foo([], 1, true); // boolean (with error) - ~~~~ -!!! error TS2345: Argument of type 'boolean' is not assignable to parameter of type 'string'. + ~~ +!!! error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. var e = foo([], 1, "2"); // {} + ~~ +!!! error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. var f = foo([], 1, 2, 3); // any (with error) ~~~~~~~~~~~~~~~~ !!! error TS2346: Supplied parameters do not match any signature of call target. diff --git a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution1.js b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution1.js index 431447df22e..4245b0cb0f4 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution1.js +++ b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution1.js @@ -1,8 +1,8 @@ //// [taggedTemplateStringsWithOverloadResolution1.ts] -function foo(strs: string[]): number; -function foo(strs: string[], x: number): string; -function foo(strs: string[], x: number, y: number): boolean; -function foo(strs: string[], x: number, y: string): {}; +function foo(strs: TemplateStringsArray): number; +function foo(strs: TemplateStringsArray, x: number): string; +function foo(strs: TemplateStringsArray, x: number, y: number): boolean; +function foo(strs: TemplateStringsArray, x: number, y: string): {}; function foo(...stuff: any[]): any { return undefined; } diff --git a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution1_ES6.errors.txt b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution1_ES6.errors.txt index 53361f2052d..73adc0c0302 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution1_ES6.errors.txt +++ b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution1_ES6.errors.txt @@ -1,25 +1,39 @@ -tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1_ES6.ts(12,20): error TS2345: Argument of type 'boolean' is not assignable to parameter of type 'string'. +tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1_ES6.ts(9,13): error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. + Property 'raw' is missing in type 'undefined[]'. +tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1_ES6.ts(10,13): error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. +tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1_ES6.ts(11,13): error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. +tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1_ES6.ts(12,13): error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. +tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1_ES6.ts(13,13): error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1_ES6.ts(14,9): error TS2346: Supplied parameters do not match any signature of call target. tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1_ES6.ts(19,20): error TS2345: Argument of type 'boolean' is not assignable to parameter of type 'string'. tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1_ES6.ts(21,9): error TS2346: Supplied parameters do not match any signature of call target. -==== tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1_ES6.ts (4 errors) ==== - function foo(strs: string[]): number; - function foo(strs: string[], x: number): string; - function foo(strs: string[], x: number, y: number): boolean; - function foo(strs: string[], x: number, y: string): {}; +==== tests/cases/conformance/es6/templates/taggedTemplateStringsWithOverloadResolution1_ES6.ts (8 errors) ==== + function foo(strs: TemplateStringsArray): number; + function foo(strs: TemplateStringsArray, x: number): string; + function foo(strs: TemplateStringsArray, x: number, y: number): boolean; + function foo(strs: TemplateStringsArray, x: number, y: string): {}; function foo(...stuff: any[]): any { return undefined; } var a = foo([]); // number + ~~ +!!! error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. +!!! error TS2345: Property 'raw' is missing in type 'undefined[]'. var b = foo([], 1); // string + ~~ +!!! error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. var c = foo([], 1, 2); // boolean + ~~ +!!! error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. var d = foo([], 1, true); // boolean (with error) - ~~~~ -!!! error TS2345: Argument of type 'boolean' is not assignable to parameter of type 'string'. + ~~ +!!! error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. var e = foo([], 1, "2"); // {} + ~~ +!!! error TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'TemplateStringsArray'. var f = foo([], 1, 2, 3); // any (with error) ~~~~~~~~~~~~~~~~ !!! error TS2346: Supplied parameters do not match any signature of call target. diff --git a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution1_ES6.js b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution1_ES6.js index bd121933b32..b62671d33ac 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution1_ES6.js +++ b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution1_ES6.js @@ -1,8 +1,8 @@ //// [taggedTemplateStringsWithOverloadResolution1_ES6.ts] -function foo(strs: string[]): number; -function foo(strs: string[], x: number): string; -function foo(strs: string[], x: number, y: number): boolean; -function foo(strs: string[], x: number, y: string): {}; +function foo(strs: TemplateStringsArray): number; +function foo(strs: TemplateStringsArray, x: number): string; +function foo(strs: TemplateStringsArray, x: number, y: number): boolean; +function foo(strs: TemplateStringsArray, x: number, y: string): {}; function foo(...stuff: any[]): any { return undefined; } diff --git a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2.js b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2.js index f12008f9581..c16bfa5454f 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2.js +++ b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2.js @@ -6,8 +6,8 @@ function foo1(...stuff: any[]): any { return undefined; } -var a = foo1 `${1}`; // string -var b = foo1([], 1); // number +var a = foo1 `${1}`; +var b = foo1([], 1); function foo2(strs: string[], x: number): number; function foo2(strs: TemplateStringsArray, x: number): string; @@ -15,8 +15,8 @@ function foo2(...stuff: any[]): any { return undefined; } -var c = foo2 `${1}`; // number -var d = foo2([], 1); // number +var c = foo2 `${1}`; +var d = foo2([], 1); //// [taggedTemplateStringsWithOverloadResolution2.js] function foo1() { @@ -26,8 +26,8 @@ function foo1() { } return undefined; } -var a = (_a = ["", ""], _a.raw = ["", ""], foo1(_a, 1)); // string -var b = foo1([], 1); // number +var a = (_a = ["", ""], _a.raw = ["", ""], foo1(_a, 1)); +var b = foo1([], 1); function foo2() { var stuff = []; for (var _i = 0; _i < arguments.length; _i++) { @@ -35,6 +35,6 @@ function foo2() { } return undefined; } -var c = (_b = ["", ""], _b.raw = ["", ""], foo2(_b, 1)); // number -var d = foo2([], 1); // number +var c = (_b = ["", ""], _b.raw = ["", ""], foo2(_b, 1)); +var d = foo2([], 1); var _a, _b; diff --git a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2.symbols b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2.symbols index 52b8f3ab6f0..241c43c4d78 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2.symbols +++ b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2.symbols @@ -19,11 +19,11 @@ function foo1(...stuff: any[]): any { >undefined : Symbol(undefined) } -var a = foo1 `${1}`; // string +var a = foo1 `${1}`; >a : Symbol(a, Decl(taggedTemplateStringsWithOverloadResolution2.ts, 7, 3)) >foo1 : Symbol(foo1, Decl(taggedTemplateStringsWithOverloadResolution2.ts, 0, 0), Decl(taggedTemplateStringsWithOverloadResolution2.ts, 1, 61), Decl(taggedTemplateStringsWithOverloadResolution2.ts, 2, 49)) -var b = foo1([], 1); // number +var b = foo1([], 1); >b : Symbol(b, Decl(taggedTemplateStringsWithOverloadResolution2.ts, 8, 3)) >foo1 : Symbol(foo1, Decl(taggedTemplateStringsWithOverloadResolution2.ts, 0, 0), Decl(taggedTemplateStringsWithOverloadResolution2.ts, 1, 61), Decl(taggedTemplateStringsWithOverloadResolution2.ts, 2, 49)) @@ -46,11 +46,11 @@ function foo2(...stuff: any[]): any { >undefined : Symbol(undefined) } -var c = foo2 `${1}`; // number +var c = foo2 `${1}`; >c : Symbol(c, Decl(taggedTemplateStringsWithOverloadResolution2.ts, 16, 3)) >foo2 : Symbol(foo2, Decl(taggedTemplateStringsWithOverloadResolution2.ts, 8, 20), Decl(taggedTemplateStringsWithOverloadResolution2.ts, 10, 49), Decl(taggedTemplateStringsWithOverloadResolution2.ts, 11, 61)) -var d = foo2([], 1); // number +var d = foo2([], 1); >d : Symbol(d, Decl(taggedTemplateStringsWithOverloadResolution2.ts, 17, 3)) >foo2 : Symbol(foo2, Decl(taggedTemplateStringsWithOverloadResolution2.ts, 8, 20), Decl(taggedTemplateStringsWithOverloadResolution2.ts, 10, 49), Decl(taggedTemplateStringsWithOverloadResolution2.ts, 11, 61)) diff --git a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2.types b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2.types index 77353e9b3ce..3c35974ddec 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2.types +++ b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2.types @@ -19,14 +19,14 @@ function foo1(...stuff: any[]): any { >undefined : undefined } -var a = foo1 `${1}`; // string +var a = foo1 `${1}`; >a : string >foo1 `${1}` : string >foo1 : { (strs: TemplateStringsArray, x: number): string; (strs: string[], x: number): number; } >`${1}` : string >1 : number -var b = foo1([], 1); // number +var b = foo1([], 1); >b : number >foo1([], 1) : number >foo1 : { (strs: TemplateStringsArray, x: number): string; (strs: string[], x: number): number; } @@ -52,14 +52,14 @@ function foo2(...stuff: any[]): any { >undefined : undefined } -var c = foo2 `${1}`; // number ->c : number ->foo2 `${1}` : number +var c = foo2 `${1}`; +>c : string +>foo2 `${1}` : string >foo2 : { (strs: string[], x: number): number; (strs: TemplateStringsArray, x: number): string; } >`${1}` : string >1 : number -var d = foo2([], 1); // number +var d = foo2([], 1); >d : number >foo2([], 1) : number >foo2 : { (strs: string[], x: number): number; (strs: TemplateStringsArray, x: number): string; } diff --git a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2_ES6.js b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2_ES6.js index f208a63354d..502fa4552a3 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2_ES6.js +++ b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2_ES6.js @@ -5,8 +5,8 @@ function foo1(...stuff: any[]): any { return undefined; } -var a = foo1 `${1}`; // string -var b = foo1([], 1); // number +var a = foo1 `${1}`; +var b = foo1([], 1); function foo2(strs: string[], x: number): number; function foo2(strs: TemplateStringsArray, x: number): string; @@ -14,17 +14,17 @@ function foo2(...stuff: any[]): any { return undefined; } -var c = foo2 `${1}`; // number -var d = foo2([], 1); // number +var c = foo2 `${1}`; +var d = foo2([], 1); //// [taggedTemplateStringsWithOverloadResolution2_ES6.js] function foo1(...stuff) { return undefined; } -var a = foo1 `${1}`; // string -var b = foo1([], 1); // number +var a = foo1 `${1}`; +var b = foo1([], 1); function foo2(...stuff) { return undefined; } -var c = foo2 `${1}`; // number -var d = foo2([], 1); // number +var c = foo2 `${1}`; +var d = foo2([], 1); diff --git a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2_ES6.symbols b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2_ES6.symbols index cadfa135202..9cc79f2596f 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2_ES6.symbols +++ b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2_ES6.symbols @@ -18,11 +18,11 @@ function foo1(...stuff: any[]): any { >undefined : Symbol(undefined) } -var a = foo1 `${1}`; // string +var a = foo1 `${1}`; >a : Symbol(a, Decl(taggedTemplateStringsWithOverloadResolution2_ES6.ts, 6, 3)) >foo1 : Symbol(foo1, Decl(taggedTemplateStringsWithOverloadResolution2_ES6.ts, 0, 0), Decl(taggedTemplateStringsWithOverloadResolution2_ES6.ts, 0, 61), Decl(taggedTemplateStringsWithOverloadResolution2_ES6.ts, 1, 49)) -var b = foo1([], 1); // number +var b = foo1([], 1); >b : Symbol(b, Decl(taggedTemplateStringsWithOverloadResolution2_ES6.ts, 7, 3)) >foo1 : Symbol(foo1, Decl(taggedTemplateStringsWithOverloadResolution2_ES6.ts, 0, 0), Decl(taggedTemplateStringsWithOverloadResolution2_ES6.ts, 0, 61), Decl(taggedTemplateStringsWithOverloadResolution2_ES6.ts, 1, 49)) @@ -45,11 +45,11 @@ function foo2(...stuff: any[]): any { >undefined : Symbol(undefined) } -var c = foo2 `${1}`; // number +var c = foo2 `${1}`; >c : Symbol(c, Decl(taggedTemplateStringsWithOverloadResolution2_ES6.ts, 15, 3)) >foo2 : Symbol(foo2, Decl(taggedTemplateStringsWithOverloadResolution2_ES6.ts, 7, 20), Decl(taggedTemplateStringsWithOverloadResolution2_ES6.ts, 9, 49), Decl(taggedTemplateStringsWithOverloadResolution2_ES6.ts, 10, 61)) -var d = foo2([], 1); // number +var d = foo2([], 1); >d : Symbol(d, Decl(taggedTemplateStringsWithOverloadResolution2_ES6.ts, 16, 3)) >foo2 : Symbol(foo2, Decl(taggedTemplateStringsWithOverloadResolution2_ES6.ts, 7, 20), Decl(taggedTemplateStringsWithOverloadResolution2_ES6.ts, 9, 49), Decl(taggedTemplateStringsWithOverloadResolution2_ES6.ts, 10, 61)) diff --git a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2_ES6.types b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2_ES6.types index 306bf38cac4..7faeec19c4a 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2_ES6.types +++ b/tests/baselines/reference/taggedTemplateStringsWithOverloadResolution2_ES6.types @@ -18,14 +18,14 @@ function foo1(...stuff: any[]): any { >undefined : undefined } -var a = foo1 `${1}`; // string +var a = foo1 `${1}`; >a : string >foo1 `${1}` : string >foo1 : { (strs: TemplateStringsArray, x: number): string; (strs: string[], x: number): number; } >`${1}` : string >1 : number -var b = foo1([], 1); // number +var b = foo1([], 1); >b : number >foo1([], 1) : number >foo1 : { (strs: TemplateStringsArray, x: number): string; (strs: string[], x: number): number; } @@ -51,14 +51,14 @@ function foo2(...stuff: any[]): any { >undefined : undefined } -var c = foo2 `${1}`; // number ->c : number ->foo2 `${1}` : number +var c = foo2 `${1}`; +>c : string +>foo2 `${1}` : string >foo2 : { (strs: string[], x: number): number; (strs: TemplateStringsArray, x: number): string; } >`${1}` : string >1 : number -var d = foo2([], 1); // number +var d = foo2([], 1); >d : number >foo2([], 1) : number >foo2 : { (strs: string[], x: number): number; (strs: TemplateStringsArray, x: number): string; } diff --git a/tests/baselines/reference/taggedTemplateStringsWithTypedTags.js b/tests/baselines/reference/taggedTemplateStringsWithTypedTags.js index fcc2cda86dc..d011c3048a7 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithTypedTags.js +++ b/tests/baselines/reference/taggedTemplateStringsWithTypedTags.js @@ -1,6 +1,6 @@ //// [taggedTemplateStringsWithTypedTags.ts] interface I { - (stringParts: string[], ...rest: number[]): I; + (stringParts: TemplateStringsArray, ...rest: number[]): I; g: I; h: I; member: I; diff --git a/tests/baselines/reference/taggedTemplateStringsWithTypedTags.symbols b/tests/baselines/reference/taggedTemplateStringsWithTypedTags.symbols index d3f85d8375a..cd28177f587 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithTypedTags.symbols +++ b/tests/baselines/reference/taggedTemplateStringsWithTypedTags.symbols @@ -2,13 +2,14 @@ interface I { >I : Symbol(I, Decl(taggedTemplateStringsWithTypedTags.ts, 0, 0)) - (stringParts: string[], ...rest: number[]): I; + (stringParts: TemplateStringsArray, ...rest: number[]): I; >stringParts : Symbol(stringParts, Decl(taggedTemplateStringsWithTypedTags.ts, 1, 5)) ->rest : Symbol(rest, Decl(taggedTemplateStringsWithTypedTags.ts, 1, 27)) +>TemplateStringsArray : Symbol(TemplateStringsArray, Decl(lib.d.ts, --, --)) +>rest : Symbol(rest, Decl(taggedTemplateStringsWithTypedTags.ts, 1, 39)) >I : Symbol(I, Decl(taggedTemplateStringsWithTypedTags.ts, 0, 0)) g: I; ->g : Symbol(I.g, Decl(taggedTemplateStringsWithTypedTags.ts, 1, 50)) +>g : Symbol(I.g, Decl(taggedTemplateStringsWithTypedTags.ts, 1, 62)) >I : Symbol(I, Decl(taggedTemplateStringsWithTypedTags.ts, 0, 0)) h: I; diff --git a/tests/baselines/reference/taggedTemplateStringsWithTypedTags.types b/tests/baselines/reference/taggedTemplateStringsWithTypedTags.types index 2c992a2d233..c7c521154c7 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithTypedTags.types +++ b/tests/baselines/reference/taggedTemplateStringsWithTypedTags.types @@ -2,8 +2,9 @@ interface I { >I : I - (stringParts: string[], ...rest: number[]): I; ->stringParts : string[] + (stringParts: TemplateStringsArray, ...rest: number[]): I; +>stringParts : TemplateStringsArray +>TemplateStringsArray : TemplateStringsArray >rest : number[] >I : I diff --git a/tests/baselines/reference/taggedTemplateStringsWithTypedTagsES6.js b/tests/baselines/reference/taggedTemplateStringsWithTypedTagsES6.js index 9eefc46ec30..35a616f05f4 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithTypedTagsES6.js +++ b/tests/baselines/reference/taggedTemplateStringsWithTypedTagsES6.js @@ -1,6 +1,6 @@ //// [taggedTemplateStringsWithTypedTagsES6.ts] interface I { - (stringParts: string[], ...rest: number[]): I; + (stringParts: TemplateStringsArray, ...rest: number[]): I; g: I; h: I; member: I; diff --git a/tests/baselines/reference/taggedTemplateStringsWithTypedTagsES6.symbols b/tests/baselines/reference/taggedTemplateStringsWithTypedTagsES6.symbols index f0b088bd6f6..f7f9c62d492 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithTypedTagsES6.symbols +++ b/tests/baselines/reference/taggedTemplateStringsWithTypedTagsES6.symbols @@ -2,13 +2,14 @@ interface I { >I : Symbol(I, Decl(taggedTemplateStringsWithTypedTagsES6.ts, 0, 0)) - (stringParts: string[], ...rest: number[]): I; + (stringParts: TemplateStringsArray, ...rest: number[]): I; >stringParts : Symbol(stringParts, Decl(taggedTemplateStringsWithTypedTagsES6.ts, 1, 5)) ->rest : Symbol(rest, Decl(taggedTemplateStringsWithTypedTagsES6.ts, 1, 27)) +>TemplateStringsArray : Symbol(TemplateStringsArray, Decl(lib.es5.d.ts, --, --)) +>rest : Symbol(rest, Decl(taggedTemplateStringsWithTypedTagsES6.ts, 1, 39)) >I : Symbol(I, Decl(taggedTemplateStringsWithTypedTagsES6.ts, 0, 0)) g: I; ->g : Symbol(I.g, Decl(taggedTemplateStringsWithTypedTagsES6.ts, 1, 50)) +>g : Symbol(I.g, Decl(taggedTemplateStringsWithTypedTagsES6.ts, 1, 62)) >I : Symbol(I, Decl(taggedTemplateStringsWithTypedTagsES6.ts, 0, 0)) h: I; diff --git a/tests/baselines/reference/taggedTemplateStringsWithTypedTagsES6.types b/tests/baselines/reference/taggedTemplateStringsWithTypedTagsES6.types index 4bda0f3fbb0..bf45144c329 100644 --- a/tests/baselines/reference/taggedTemplateStringsWithTypedTagsES6.types +++ b/tests/baselines/reference/taggedTemplateStringsWithTypedTagsES6.types @@ -2,8 +2,9 @@ interface I { >I : I - (stringParts: string[], ...rest: number[]): I; ->stringParts : string[] + (stringParts: TemplateStringsArray, ...rest: number[]): I; +>stringParts : TemplateStringsArray +>TemplateStringsArray : TemplateStringsArray >rest : number[] >I : I From e091f6702e368f7b5f5455f6e703f7f9cd18b9e8 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 21 Jul 2016 11:27:29 -0700 Subject: [PATCH 108/307] introduce ExternalFile interface --- .../unittests/tsserverProjectSystem.ts | 94 ++++++++++--------- src/server/editorServices.ts | 29 +++++- src/server/protocol.d.ts | 8 +- 3 files changed, 82 insertions(+), 49 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 24d450beeb8..70e1f34300c 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -30,6 +30,14 @@ namespace ts { return combinePaths(getDirectoryPath(libFile.path), "tsc.js"); } + function toExternalFile(fileName: string): server.protocol.ExternalFile { + return { fileName } + } + + function toExternalFiles(fileNames: string[]) { + return map(fileNames, toExternalFile); + } + interface TestServerHostCreationParameters { useCaseSensitiveFileNames?: boolean; executingFilePath?: string; @@ -335,7 +343,7 @@ namespace ts { } // TOOD: record and invoke callbacks to simulate timer events - setTimeout (callback: TimeOutCallback, time: number, ...args: any[]) { + setTimeout(callback: TimeOutCallback, time: number, ...args: any[]) { return this.timeoutCallbacks.register(callback, args); }; @@ -352,7 +360,7 @@ namespace ts { this.timeoutCallbacks.invoke(); } - setImmediate (callback: TimeOutCallback, time: number, ...args: any[]) { + setImmediate(callback: TimeOutCallback, time: number, ...args: any[]) { return this.immediateCallbacks.register(callback, args); }; @@ -752,7 +760,7 @@ namespace ts { checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, libFile.path]); }); - it ("should close configured project after closing last open file", () => { + it("should close configured project after closing last open file", () => { const file1 = { path: "/a/b/main.ts", content: "let x =1;" @@ -775,7 +783,7 @@ namespace ts { checkNumberOfConfiguredProjects(projectService, 0); }); - it ("should not close external project with no open files", () => { + it("should not close external project with no open files", () => { const file1 = { path: "/a/b/f1.ts", content: "let x =1;" @@ -788,7 +796,7 @@ namespace ts { const host = createServerHost([file1, file2]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openExternalProject({ - rootFiles: [ file1.path, file2.path ], + rootFiles: toExternalFiles([file1.path, file2.path]), options: {}, projectFileName: externalProjectName }); @@ -811,7 +819,7 @@ namespace ts { checkNumberOfInferredProjects(projectService, 0); }); - it ("external project that included config files", () => { + it("external project that included config files", () => { const file1 = { path: "/a/b/f1.ts", content: "let x =1;" @@ -846,7 +854,7 @@ namespace ts { const host = createServerHost([file1, file2, file3, config1, config2]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useOneInferredProject*/ false); projectService.openExternalProject({ - rootFiles: [ config1.path, config2.path, file3.path ], + rootFiles: toExternalFiles([config1.path, config2.path, file3.path]), options: {}, projectFileName: externalProjectName }); @@ -888,7 +896,7 @@ namespace ts { checkNumberOfProjects(projectService, { configuredProjects: 1 }); projectService.openExternalProject({ - rootFiles: [ configFile.path ], + rootFiles: toExternalFiles([configFile.path]), options: {}, projectFileName: externalProjectName }); @@ -919,7 +927,7 @@ namespace ts { checkNumberOfProjects(projectService, { configuredProjects: 1 }); projectService.openExternalProject({ - rootFiles: [ configFile.path ], + rootFiles: toExternalFiles([configFile.path]), options: {}, projectFileName: externalProjectName }); @@ -953,11 +961,11 @@ namespace ts { projectService.openClientFile(file1.path); checkNumberOfInferredProjects(projectService, 1); - checkProjectActualFiles(projectService.inferredProjects[0], [ file1.path, file2.path ]); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); projectService.openClientFile(file3.path); checkNumberOfInferredProjects(projectService, 2); - checkProjectActualFiles(projectService.inferredProjects[1], [ file3.path ]); + checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); const modifiedFile2 = { path: file2.path, @@ -968,7 +976,7 @@ namespace ts { host.triggerFileWatcherCallback(modifiedFile2.path, /*removed*/ false); checkNumberOfInferredProjects(projectService, 1); - checkProjectActualFiles(projectService.inferredProjects[0], [ file1.path, modifiedFile2.path, file3.path ]); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, modifiedFile2.path, file3.path]); }); it("deleted files affect project structure", () => { @@ -991,7 +999,7 @@ namespace ts { checkNumberOfProjects(projectService, { inferredProjects: 1 }); - checkProjectActualFiles(projectService.inferredProjects[0], [ file1.path, file2.path, file3.path ]); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); projectService.openClientFile(file3.path); checkNumberOfProjects(projectService, { inferredProjects: 1 }); @@ -1001,8 +1009,8 @@ namespace ts { checkNumberOfProjects(projectService, { inferredProjects: 2 }); - checkProjectActualFiles(projectService.inferredProjects[0], [ file1.path ]); - checkProjectActualFiles(projectService.inferredProjects[1], [ file3.path ]); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); }); it("open file become a part of configured project if it is referenced from root file", () => { @@ -1020,7 +1028,7 @@ namespace ts { }; const configFile = { path: "/a/c/tsconfig.json", - content: JSON.stringify({ compilerOptions: {}, files: [ "f2.ts", "f3.ts" ] }) + content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] }) }; const host = createServerHost([file1, file2, file3]); @@ -1028,17 +1036,17 @@ namespace ts { projectService.openClientFile(file1.path); checkNumberOfProjects(projectService, { inferredProjects: 1 }); - checkProjectActualFiles(projectService.inferredProjects[0], [ file1.path ]); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); projectService.openClientFile(file3.path); checkNumberOfProjects(projectService, { inferredProjects: 2 }); - checkProjectActualFiles(projectService.inferredProjects[0], [ file1.path ]); - checkProjectActualFiles(projectService.inferredProjects[1], [ file3.path ]); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); host.reloadFS([file1, file2, file3, configFile]); host.triggerDirectoryWatcherCallback(getDirectoryPath(configFile.path), configFile.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [ file1.path, file2.path, file3.path ]); + checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path, file3.path]); }); it("correctly migrate files between projects", () => { @@ -1096,7 +1104,7 @@ namespace ts { projectService.openClientFile(file1.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [ file1.path ]); + checkProjectActualFiles(projectService.configuredProjects[0], [file1.path]); host.reloadFS([file1, file2, configFile]); @@ -1105,7 +1113,7 @@ namespace ts { host.runQueuedTimeoutCallbacks(); // to execute throttled requests checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectRootFiles(projectService.configuredProjects[0], [ file1.path, file2.path ]); + checkProjectRootFiles(projectService.configuredProjects[0], [file1.path, file2.path]); }); it("can correctly update configured project when set of root files has changed (new file in list of files)", () => { @@ -1119,7 +1127,7 @@ namespace ts { }; const configFile = { path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {}, files: [ "f1.ts" ] }) + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts"] }) }; const host = createServerHost([file1, file2, configFile]); @@ -1127,18 +1135,18 @@ namespace ts { projectService.openClientFile(file1.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [ file1.path ]); + checkProjectActualFiles(projectService.configuredProjects[0], [file1.path]); const modifiedConfigFile = { path: configFile.path, - content: JSON.stringify({ compilerOptions: {}, files: [ "f1.ts", "f2.ts" ] }) + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) }; host.reloadFS([file1, file2, modifiedConfigFile]); host.triggerFileWatcherCallback(configFile.path, /*removed*/ false); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectRootFiles(projectService.configuredProjects[0], [ file1.path, file2.path ]); + checkProjectRootFiles(projectService.configuredProjects[0], [file1.path, file2.path]); }); it("can update configured project when set of root files was not changed", () => { @@ -1152,7 +1160,7 @@ namespace ts { }; const configFile = { path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {}, files: [ "f1.ts", "f2.ts" ] }) + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) }; const host = createServerHost([file1, file2, configFile]); @@ -1160,18 +1168,18 @@ namespace ts { projectService.openClientFile(file1.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [ file1.path, file2.path ]); + checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path]); const modifiedConfigFile = { path: configFile.path, - content: JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: [ "f1.ts", "f2.ts" ] }) + content: JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] }) }; host.reloadFS([file1, file2, modifiedConfigFile]); host.triggerFileWatcherCallback(configFile.path, /*removed*/ false); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectRootFiles(projectService.configuredProjects[0], [ file1.path, file2.path ]); + checkProjectRootFiles(projectService.configuredProjects[0], [file1.path, file2.path]); }); it("can correctly update external project when set of root files has changed", () => { @@ -1186,13 +1194,13 @@ namespace ts { const host = createServerHost([file1, file2]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useSingleInferredProject*/ false); - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: [file1.path] }); + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path]) }); checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectActualFiles(projectService.externalProjects[0], [ file1.path ]); + checkProjectActualFiles(projectService.externalProjects[0], [file1.path]); - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: [file1.path, file2.path] }); + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]) }); checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectRootFiles(projectService.externalProjects[0], [ file1.path, file2.path ]); + checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); }); it("can update external project when set of root files was not changed", () => { @@ -1212,15 +1220,15 @@ namespace ts { const host = createServerHost([file1, file2, file3]); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useSingleInferredProject*/ false); - projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.NodeJs }, rootFiles: [file1.path, file2.path] }); + projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.NodeJs }, rootFiles: toExternalFiles([file1.path, file2.path]) }); checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectRootFiles(projectService.externalProjects[0], [ file1.path, file2.path ]); - checkProjectActualFiles(projectService.externalProjects[0], [ file1.path, file2.path ]); + checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); + checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path]); - projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.Classic }, rootFiles: [file1.path, file2.path] }); + projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.Classic }, rootFiles: toExternalFiles([file1.path, file2.path]) }); checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectRootFiles(projectService.externalProjects[0], [ file1.path, file2.path ]); - checkProjectActualFiles(projectService.externalProjects[0], [ file1.path, file2.path, file3.path ]); + checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); + checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path, file3.path]); }); it("config file is deleted", () => { @@ -1241,11 +1249,11 @@ namespace ts { projectService.openClientFile(file1.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [ file1.path, file2.path ]); + checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path]); projectService.openClientFile(file2.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [ file1.path, file2.path ]); + checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path]); host.reloadFS([file1, file2]); host.triggerFileWatcherCallback(config.path, /*removed*/ true); @@ -1273,7 +1281,7 @@ namespace ts { checkNumberOfProjects(projectService, { inferredProjects: 1 }); projectService.applyChangesInOpenFiles( /*openFiles*/ undefined, - /*changedFiles*/ [{ fileName: file1.path, changes: [ { span: createTextSpan(0, file1.path.length), newText: "let y = 1" } ] }], + /*changedFiles*/[{ fileName: file1.path, changes: [{ span: createTextSpan(0, file1.path.length), newText: "let y = 1" }] }], /*closedFiles*/ undefined); checkNumberOfProjects(projectService, { inferredProjects: 1 }); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 8139decfd05..5d28a352b17 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -46,6 +46,24 @@ namespace ts.server { configFileErrors?: Diagnostic[]; } + interface FilePropertyReader { + getFileName(f: T): string; + getScriptKind(f: T): ScriptKind; + hasMixedContent(f: T): boolean; + } + + const fileNamePropertyReader: FilePropertyReader = { + getFileName: x => x, + getScriptKind: _ => undefined, + hasMixedContent: _ => false + }; + + const externalFilePropertyReader: FilePropertyReader = { + getFileName: x => x.fileName, + getScriptKind: x => x.scriptKind, + hasMixedContent: x => x.hasMixedContent + }; + function findProjectByName(projectName: string, projects: T[]): T { for (const proj of projects) { if (proj.getProjectName() === projectName) { @@ -712,13 +730,14 @@ namespace ts.server { return { success: true, project, errors }; } - private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedRootFiles: string[], newOptions: CompilerOptions) { + private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, newOptions: CompilerOptions) { const oldRootScriptInfos = project.getRootScriptInfos(); const newRootScriptInfos: ScriptInfo[] = []; const newRootScriptInfoMap: NormalizedPathMap = createNormalizedPathMap(); let rootFilesChanged = false; - for (const newRootFile of newUncheckedRootFiles) { + for (const f of newUncheckedFiles) { + const newRootFile = propertyReader.getFileName(f); if (!this.host.fileExists(newRootFile)) { continue; } @@ -798,7 +817,7 @@ namespace ts.server { project.enableLanguageService(); } this.watchConfigDirectoryForProject(project, projectOptions); - this.updateNonInferredProject(project, projectOptions.files, projectOptions.compilerOptions); + this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions); } } @@ -1066,14 +1085,14 @@ namespace ts.server { openExternalProject(proj: protocol.ExternalProject): void { const externalProject = this.findExternalProjectByProjectName(proj.projectFileName); if (externalProject) { - this.updateNonInferredProject(externalProject, proj.rootFiles, proj.options); + this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, proj.options); return; } let tsConfigFiles: NormalizedPath[]; const rootFiles: NormalizedPath[] = []; for (const file of proj.rootFiles) { - const normalized = toNormalizedPath(file); + const normalized = toNormalizedPath(file.fileName); if (getBaseFileName(normalized) === "tsconfig.json") { (tsConfigFiles || (tsConfigFiles = [])).push(normalized); } diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 3bef3e600eb..3f49e869775 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -490,9 +490,15 @@ declare namespace ts.server.protocol { body?: RenameResponseBody; } + export interface ExternalFile { + fileName: string; + scriptKind?: ScriptKind; + hasMixedContent?: boolean; + } + export interface ExternalProject { projectFileName: string; - rootFiles: string[]; + rootFiles: ExternalFile[]; options: CompilerOptions; } From 7da455c3903e7d63db974037a79142d54e0b49ce Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 21 Jul 2016 11:40:03 -0700 Subject: [PATCH 109/307] read ScriptKind\HasMixedContent when opening external project --- src/harness/unittests/tsserverProjectSystem.ts | 2 +- src/server/editorServices.ts | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 70e1f34300c..b645ef29189 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -31,7 +31,7 @@ namespace ts { } function toExternalFile(fileName: string): server.protocol.ExternalFile { - return { fileName } + return { fileName }; } function toExternalFiles(fileNames: string[]) { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 5d28a352b17..442f7687617 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -746,7 +746,9 @@ namespace ts.server { if (!scriptInfo || !project.isRoot(scriptInfo)) { rootFilesChanged = true; if (!scriptInfo) { - scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ false); + const scriptKind = propertyReader.getScriptKind(f); + const hasMixedContent = propertyReader.hasMixedContent(f); + scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent); } } newRootScriptInfos.push(scriptInfo); @@ -855,12 +857,14 @@ namespace ts.server { return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); } - getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { + getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean) { let info = this.getScriptInfoForNormalizedPath(fileName); if (!info) { let content: string; if (this.host.fileExists(fileName)) { - content = fileContent || this.host.readFile(fileName); + // by default pick whatever content was supplied as the argument + // if argument was not given - then for mixed content files assume that its content is empty string + content = fileContent || (hasMixedContent ? "" : this.host.readFile(fileName)); } if (!content) { if (openedByClient) { @@ -871,7 +875,8 @@ namespace ts.server { info = new ScriptInfo(this.host, fileName, content, scriptKind, openedByClient); info.setFormatOptions(toEditorSettings(this.getFormatCodeOptions())); this.filenameToScriptInfo.set(fileName, info); - if (!info.isOpen) { + if (!info.isOpen && !hasMixedContent) { + // do not watch files with mixed content - server doesn't know how to interpret it info.setWatcher(this.host.watchFile(fileName, _ => this.onSourceFileChanged(fileName))); } } From 2c254773bbf8b42357a7054694c848197c1f004e Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 21 Jul 2016 14:17:22 -0700 Subject: [PATCH 110/307] added tests --- .../unittests/tsserverProjectSystem.ts | 34 +++++++++++++++++++ src/server/editorServices.ts | 32 +++++++++-------- src/server/scriptInfo.ts | 13 +++++-- 3 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index b645ef29189..bf76a453784 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1291,5 +1291,39 @@ namespace ts { projectService.ensureInferredProjectsUpToDate_TestOnly(); checkNumberOfProjects(projectService, { inferredProjects: 2 }); }); + + it("files with mixed content are handled correctly", () => { + const file1 = { + path: "/a/b/f1.html", + content: ` - +