From da7f824a2a4a6a286bda1689daa51095e338a8c3 Mon Sep 17 00:00:00 2001 From: Jason Ramsay Date: Thu, 10 Nov 2016 11:42:42 -0800 Subject: [PATCH 1/9] tsconfig.json mixed content support --- src/compiler/commandLineParser.ts | 8 +-- src/compiler/core.ts | 8 +-- src/compiler/program.ts | 4 +- .../unittests/tsserverProjectSystem.ts | 60 +++++++++++++++++++ src/server/editorServices.ts | 34 +++++++---- src/server/lsHost.ts | 9 +++ src/server/project.ts | 15 ++++- src/server/protocol.ts | 9 +++ src/server/scriptInfo.ts | 3 +- src/server/utilities.ts | 4 +- src/services/completions.ts | 11 ++-- src/services/services.ts | 9 ++- src/services/types.ts | 1 + 13 files changed, 145 insertions(+), 30 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index d96035091c713..0e984f55673ef 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -826,7 +826,7 @@ namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = []): ParsedCommandLine { + export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = [], mixedContentFileExtensions: string[] = []): ParsedCommandLine { const errors: Diagnostic[] = []; const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); const resolvedPath = toPath(configFileName || "", basePath, getCanonicalFileName); @@ -963,7 +963,7 @@ namespace ts { includeSpecs = ["**/*"]; } - const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors); + const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors, mixedContentFileExtensions); if (result.fileNames.length === 0 && !hasProperty(json, "files") && resolutionStack.length === 0) { errors.push( @@ -1165,7 +1165,7 @@ namespace ts { * @param host The host used to resolve files and directories. * @param errors An array for diagnostic reporting. */ - function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[]): ExpandResult { + function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], mixedContentFileExtensions: string[]): ExpandResult { basePath = normalizePath(basePath); // The exclude spec list is converted into a regular expression, which allows us to quickly @@ -1199,7 +1199,7 @@ namespace ts { // Rather than requery this for each file and filespec, we query the supported extensions // once and store it on the expansion context. - const supportedExtensions = getSupportedExtensions(options); + const supportedExtensions = getSupportedExtensions(options, mixedContentFileExtensions); // Literal files are always included verbatim. An "include" or "exclude" specification cannot // remove a literal file. diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 8175730159b0b..f0a00003f0749 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1912,8 +1912,8 @@ namespace ts { export const supportedJavascriptExtensions = [".js", ".jsx"]; const allSupportedExtensions = supportedTypeScriptExtensions.concat(supportedJavascriptExtensions); - export function getSupportedExtensions(options?: CompilerOptions): string[] { - return options && options.allowJs ? allSupportedExtensions : supportedTypeScriptExtensions; + export function getSupportedExtensions(options?: CompilerOptions, mixedContentFileExtensions?: string[]): string[] { + return options && options.allowJs ? concatenate(allSupportedExtensions, mixedContentFileExtensions) : supportedTypeScriptExtensions; } export function hasJavaScriptFileExtension(fileName: string) { @@ -1924,10 +1924,10 @@ namespace ts { return forEach(supportedTypeScriptExtensions, extension => fileExtensionIs(fileName, extension)); } - export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions) { + export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions, mixedContentFileExtensions?: string[]) { if (!fileName) { return false; } - for (const extension of getSupportedExtensions(compilerOptions)) { + for (const extension of getSupportedExtensions(compilerOptions, mixedContentFileExtensions)) { if (fileExtensionIs(fileName, extension)) { return true; } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 5cd957708f51b..335920d8dcf1c 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -289,7 +289,7 @@ namespace ts { return resolutions; } - export function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program): Program { + export function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, mixedContentFileExtensions?: string[]): Program { let program: Program; let files: SourceFile[] = []; let commonSourceDirectory: string; @@ -324,7 +324,7 @@ namespace ts { let skipDefaultLib = options.noLib; const programDiagnostics = createDiagnosticCollection(); const currentDirectory = host.getCurrentDirectory(); - const supportedExtensions = getSupportedExtensions(options); + const supportedExtensions = getSupportedExtensions(options, mixedContentFileExtensions); // Map storing if there is emit blocking diagnostics for given input const hasEmitBlockingDiagnostics = createFileMap(getCanonicalFileName); diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 17b1dd3e585f7..c223a46040526 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1441,6 +1441,66 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); }); + it("tsconfig script block support", () => { + const file1 = { + path: "/a/b/f1.ts", + content: ` ` + }; + const file2 = { + path: "/a/b/f2.html", + content: `var hello = "hello";` + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true } }) + }; + const host = createServerHost([file1, file2, config]); + const session = createSession(host); + openFilesForSession([file1], session); + const projectService = session.getProjectService(); + + // HTML file will not be included in any projects yet + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(projectService.configuredProjects[0], [file1.path]); + + // Specify .html extension as mixed content + const configureHostRequest = makeSessionRequest(CommandNames.Configure, { mixedContentFileExtensions: [".html"] }); + session.executeCommand(configureHostRequest).response; + + // HTML file still not included in the project as it is closed + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(projectService.configuredProjects[0], [file1.path]); + + // Open HTML file + projectService.applyChangesInOpenFiles( + /*openFiles*/[{ fileName: file2.path, hasMixedContent: true, scriptKind: ScriptKind.JS, content: `var hello = "hello";` }], + /*changedFiles*/undefined, + /*closedFiles*/undefined); + + // Now HTML file is included in the project + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path]); + + // Check identifiers defined in HTML content are available in .ts file + const project = projectService.configuredProjects[0]; + let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1); + assert(completions && completions.entries[0].name === "hello", `expected entry hello to be in completion list`); + + // Close HTML file + projectService.applyChangesInOpenFiles( + /*openFiles*/undefined, + /*changedFiles*/undefined, + /*closedFiles*/[file2.path]); + + // HTML file is still included in project + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path]); + + // Check identifiers defined in HTML content are not available in .ts file + completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5); + assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`); + }); + it("project structure update is deferred if files are not added\removed", () => { const file1 = { path: "/a/b/f1.ts", diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index d7dcfc810b47e..93692f02480d3 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -90,6 +90,7 @@ namespace ts.server { export interface HostConfiguration { formatCodeOptions: FormatCodeSettings; hostInfo: string; + mixedContentFileExtensions?: string[]; } interface ConfigFileConversionResult { @@ -114,13 +115,13 @@ namespace ts.server { interface FilePropertyReader { getFileName(f: T): string; getScriptKind(f: T): ScriptKind; - hasMixedContent(f: T): boolean; + hasMixedContent(f: T, mixedContentFileExtensions: string[]): boolean; } const fileNamePropertyReader: FilePropertyReader = { getFileName: x => x, getScriptKind: _ => undefined, - hasMixedContent: _ => false + hasMixedContent: (fileName, mixedContentFileExtensions) => forEach(mixedContentFileExtensions, extension => fileExtensionIs(fileName, extension)) }; const externalFilePropertyReader: FilePropertyReader = { @@ -235,12 +236,12 @@ namespace ts.server { private readonly directoryWatchers: DirectoryWatchers; private readonly throttledOperations: ThrottledOperations; - private readonly hostConfiguration: HostConfiguration; - private changedFiles: ScriptInfo[]; private toCanonicalFileName: (f: string) => string; + public readonly hostConfiguration: HostConfiguration; + public lastDeletedFile: ScriptInfo; constructor(public readonly host: ServerHost, @@ -264,7 +265,8 @@ namespace ts.server { this.hostConfiguration = { formatCodeOptions: getDefaultFormatCodeSettings(this.host), - hostInfo: "Unknown host" + hostInfo: "Unknown host", + mixedContentFileExtensions: [] }; this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); @@ -455,7 +457,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.getCompilerOptions())) { + if (fileName && !ts.isSupportedSourceFileName(fileName, project.getCompilerOptions(), this.hostConfiguration.mixedContentFileExtensions)) { return; } @@ -610,6 +612,9 @@ namespace ts.server { let projectsToRemove: Project[]; for (const p of info.containingProjects) { if (p.projectKind === ProjectKind.Configured) { + if (info.hasMixedContent) { + info.hasChanges = true; + } // last open file in configured project - close it if ((p).deleteOpenRef() === 0) { (projectsToRemove || (projectsToRemove = [])).push(p); @@ -772,7 +777,9 @@ namespace ts.server { this.host, getDirectoryPath(configFilename), /*existingOptions*/ {}, - configFilename); + configFilename, + /*resolutionStack*/ [], + this.hostConfiguration.mixedContentFileExtensions); if (parsedCommandLine.errors.length) { errors = concatenate(errors, parsedCommandLine.errors); @@ -876,7 +883,7 @@ namespace ts.server { for (const f of files) { const rootFilename = propertyReader.getFileName(f); const scriptKind = propertyReader.getScriptKind(f); - const hasMixedContent = propertyReader.hasMixedContent(f); + const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.mixedContentFileExtensions); if (this.host.fileExists(rootFilename)) { const info = this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(rootFilename), /*openedByClient*/ clientFileName == rootFilename, /*fileContent*/ undefined, scriptKind, hasMixedContent); project.addRoot(info); @@ -922,7 +929,7 @@ namespace ts.server { rootFilesChanged = true; if (!scriptInfo) { const scriptKind = propertyReader.getScriptKind(f); - const hasMixedContent = propertyReader.hasMixedContent(f); + const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.mixedContentFileExtensions); scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent); } } @@ -1072,6 +1079,9 @@ namespace ts.server { } if (openedByClient) { info.isOpen = true; + if (hasMixedContent) { + info.hasChanges = true; + } } } return info; @@ -1103,6 +1113,10 @@ namespace ts.server { mergeMaps(this.hostConfiguration.formatCodeOptions, convertFormatOptions(args.formatOptions)); this.logger.info("Format host information updated"); } + if (args.mixedContentFileExtensions) { + this.hostConfiguration.mixedContentFileExtensions = args.mixedContentFileExtensions; + this.logger.info("Host mixed content file extensions updated"); + } } } @@ -1168,12 +1182,12 @@ namespace ts.server { } openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean): OpenConfiguredProjectResult { + const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent); 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, hasMixedContent); this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ true); this.printProjects(); return { configFileName, configFileErrors }; diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index f1e80d95880e2..45ea3f90f41a8 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -5,6 +5,7 @@ namespace ts.server { export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost, ServerLanguageServiceHost { private compilationSettings: ts.CompilerOptions; + private mixedContentFileExtensions: string[]; private readonly resolvedModuleNames= createFileMap>(); private readonly resolvedTypeReferenceDirectives = createFileMap>(); private readonly getCanonicalFileName: (fileName: string) => string; @@ -143,6 +144,10 @@ namespace ts.server { return this.compilationSettings; } + getMixedContentFileExtensions() { + return this.mixedContentFileExtensions; + } + useCaseSensitiveFileNames() { return this.host.useCaseSensitiveFileNames; } @@ -231,5 +236,9 @@ namespace ts.server { } this.compilationSettings = opt; } + + setMixedContentFileExtensions(mixedContentFileExtensions: string[]) { + this.mixedContentFileExtensions = mixedContentFileExtensions || []; + } } } \ No newline at end of file diff --git a/src/server/project.ts b/src/server/project.ts index d01df728248c6..fbd44056e49ad 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1,4 +1,4 @@ -/// +/// /// /// /// @@ -202,6 +202,7 @@ namespace ts.server { enableLanguageService() { const lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken); lsHost.setCompilationSettings(this.compilerOptions); + lsHost.setMixedContentFileExtensions(this.projectService.hostConfiguration.mixedContentFileExtensions); this.languageService = ts.createLanguageService(lsHost, this.documentRegistry); this.lsHost = lsHost; @@ -462,6 +463,10 @@ namespace ts.server { return !hasChanges; } + private hasChangedFiles() { + return this.rootFiles && forEach(this.rootFiles, info => info.hasChanges); + } + private setTypings(typings: SortedReadonlyArray): boolean { if (arrayIsEqualTo(this.typingFiles, typings)) { return false; @@ -475,7 +480,7 @@ namespace ts.server { const oldProgram = this.program; this.program = this.languageService.getProgram(); - let hasChanges = false; + let hasChanges = this.hasChangedFiles(); // 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. @@ -578,6 +583,7 @@ namespace ts.server { const added: string[] = []; const removed: string[] = []; + const updated = this.rootFiles.filter(info => info.hasChanges).map(info => info.fileName); for (const id in currentFiles) { if (!hasProperty(lastReportedFileNames, id)) { added.push(id); @@ -588,9 +594,12 @@ namespace ts.server { removed.push(id); } } + for (const root of this.rootFiles) { + root.hasChanges = false; + } this.lastReportedFileNames = currentFiles; this.lastReportedVersion = this.projectStructureVersion; - return { info, changes: { added, removed }, projectErrors: this.projectErrors }; + return { info, changes: { added, removed, updated }, projectErrors: this.projectErrors }; } else { // unknown version - return everything diff --git a/src/server/protocol.ts b/src/server/protocol.ts index d13caf7f01b0c..f2f567a28ba83 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -914,6 +914,10 @@ namespace ts.server.protocol { * List of removed files */ removed: string[]; + /** + * List of updated files + */ + updated: string[]; } /** @@ -986,6 +990,11 @@ namespace ts.server.protocol { * The format options to use during formatting and other code editing features. */ formatOptions?: FormatCodeSettings; + + /** + * List of host's supported mixed content file extensions + */ + mixedContentFileExtensions?: string[]; } /** diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 84649863a7b3a..e5e1c3577e017 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -13,7 +13,6 @@ namespace ts.server { private fileWatcher: FileWatcher; private svc: ScriptVersionCache; - // TODO: allow to update hasMixedContent from the outside constructor( private readonly host: ServerHost, readonly fileName: NormalizedPath, @@ -29,6 +28,8 @@ namespace ts.server { : getScriptKindFromFileName(fileName); } + public hasChanges = false; + getFormatCodeSettings() { return this.formatCodeSettings; } diff --git a/src/server/utilities.ts b/src/server/utilities.ts index ac80965211923..457ce6a204f59 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -1,4 +1,4 @@ -/// +/// /// namespace ts.server { @@ -211,6 +211,7 @@ namespace ts.server { export interface ServerLanguageServiceHost { setCompilationSettings(options: CompilerOptions): void; + setMixedContentFileExtensions(mixedContentFileExtensions: string[]): void; notifyFileRemoved(info: ScriptInfo): void; startRecordingFilesWithChangedResolutions(): void; finishRecordingFilesWithChangedResolutions(): Path[]; @@ -218,6 +219,7 @@ namespace ts.server { export const nullLanguageServiceHost: ServerLanguageServiceHost = { setCompilationSettings: () => undefined, + setMixedContentFileExtensions: () => undefined, notifyFileRemoved: () => undefined, startRecordingFilesWithChangedResolutions: () => undefined, finishRecordingFilesWithChangedResolutions: () => undefined diff --git a/src/services/completions.ts b/src/services/completions.ts index b710aa8cd78bb..d150a0eb76abd 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -271,13 +271,14 @@ namespace ts.Completions { const span = getDirectoryFragmentTextSpan((node).text, node.getStart() + 1); let entries: CompletionEntry[]; if (isPathRelativeToScript(literalValue) || isRootedDiskPath(literalValue)) { + const mixedContentFileExtensions = host.getMixedContentFileExtensions ? host.getMixedContentFileExtensions() : []; if (compilerOptions.rootDirs) { entries = getCompletionEntriesForDirectoryFragmentWithRootDirs( - compilerOptions.rootDirs, literalValue, scriptDirectory, getSupportedExtensions(compilerOptions), /*includeExtensions*/false, span, scriptPath); + compilerOptions.rootDirs, literalValue, scriptDirectory, getSupportedExtensions(compilerOptions, mixedContentFileExtensions), /*includeExtensions*/false, span, scriptPath); } else { entries = getCompletionEntriesForDirectoryFragment( - literalValue, scriptDirectory, getSupportedExtensions(compilerOptions), /*includeExtensions*/false, span, scriptPath); + literalValue, scriptDirectory, getSupportedExtensions(compilerOptions, mixedContentFileExtensions), /*includeExtensions*/false, span, scriptPath); } } else { @@ -411,7 +412,8 @@ namespace ts.Completions { let result: CompletionEntry[]; if (baseUrl) { - const fileExtensions = getSupportedExtensions(compilerOptions); + const mixedContentFileExtensions = host.getMixedContentFileExtensions ? host.getMixedContentFileExtensions() : []; + const fileExtensions = getSupportedExtensions(compilerOptions, mixedContentFileExtensions); const projectDir = compilerOptions.project || host.getCurrentDirectory(); const absolute = isRootedDiskPath(baseUrl) ? baseUrl : combinePaths(projectDir, baseUrl); result = getCompletionEntriesForDirectoryFragment(fragment, normalizePath(absolute), fileExtensions, /*includeExtensions*/false, span); @@ -588,7 +590,8 @@ namespace ts.Completions { if (kind === "path") { // Give completions for a relative path const span: TextSpan = getDirectoryFragmentTextSpan(toComplete, range.pos + prefix.length); - completionInfo.entries = getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getSupportedExtensions(compilerOptions), /*includeExtensions*/true, span, sourceFile.path); + const mixedContentFileExtensions = host.getMixedContentFileExtensions ? host.getMixedContentFileExtensions() : []; + completionInfo.entries = getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getSupportedExtensions(compilerOptions, mixedContentFileExtensions), /*includeExtensions*/true, span, sourceFile.path); } else { // Give completions based on the typings available diff --git a/src/services/services.ts b/src/services/services.ts index 56e604abeb3eb..c062f8d5cbbcd 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -740,6 +740,7 @@ namespace ts { class HostCache { private fileNameToEntry: FileMap; private _compilationSettings: CompilerOptions; + private _mixedContentFileExtensions: string[]; private currentDirectory: string; constructor(private host: LanguageServiceHost, private getCanonicalFileName: (fileName: string) => string) { @@ -755,12 +756,18 @@ namespace ts { // store the compilation settings this._compilationSettings = host.getCompilationSettings() || getDefaultCompilerOptions(); + + this._mixedContentFileExtensions = host.getMixedContentFileExtensions ? host.getMixedContentFileExtensions() : []; } public compilationSettings() { return this._compilationSettings; } + public mixedContentFileExtensions() { + return this._mixedContentFileExtensions; + } + private createEntry(fileName: string, path: Path) { let entry: HostFileInformation; const scriptSnapshot = this.host.getScriptSnapshot(fileName); @@ -1083,7 +1090,7 @@ namespace ts { } const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings); - const newProgram = createProgram(hostCache.getRootFileNames(), newSettings, compilerHost, program); + const newProgram = createProgram(hostCache.getRootFileNames(), newSettings, compilerHost, program, hostCache.mixedContentFileExtensions()); // Release any files we have acquired in the old program but are // not part of the new program. diff --git a/src/services/types.ts b/src/services/types.ts index 4e04df3fc7caf..2a43c561a7c8b 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -125,6 +125,7 @@ namespace ts { // export interface LanguageServiceHost { getCompilationSettings(): CompilerOptions; + getMixedContentFileExtensions?(): string[]; getNewLine?(): string; getProjectVersion?(): string; getScriptFileNames(): string[]; From 7dd30dbfd4e6433beed5e5166813993cbcdd58b6 Mon Sep 17 00:00:00 2001 From: Jason Ramsay Date: Thu, 10 Nov 2016 11:42:42 -0800 Subject: [PATCH 2/9] tsconfig.json mixed content support --- src/compiler/commandLineParser.ts | 8 +-- src/compiler/core.ts | 16 +++-- src/compiler/program.ts | 4 +- src/compiler/types.ts | 6 ++ .../unittests/tsserverProjectSystem.ts | 60 +++++++++++++++++++ src/server/editorServices.ts | 34 +++++++---- src/server/lsHost.ts | 9 +++ src/server/project.ts | 15 ++++- src/server/protocol.ts | 9 +++ src/server/scriptInfo.ts | 3 +- src/server/utilities.ts | 4 +- src/services/completions.ts | 11 ++-- src/services/services.ts | 9 ++- src/services/types.ts | 1 + 14 files changed, 159 insertions(+), 30 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index d96035091c713..f875babe1ec8f 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -826,7 +826,7 @@ namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = []): ParsedCommandLine { + export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = [], fileExtensionMap: FileExtensionMap = {}): ParsedCommandLine { const errors: Diagnostic[] = []; const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); const resolvedPath = toPath(configFileName || "", basePath, getCanonicalFileName); @@ -963,7 +963,7 @@ namespace ts { includeSpecs = ["**/*"]; } - const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors); + const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors, fileExtensionMap); if (result.fileNames.length === 0 && !hasProperty(json, "files") && resolutionStack.length === 0) { errors.push( @@ -1165,7 +1165,7 @@ namespace ts { * @param host The host used to resolve files and directories. * @param errors An array for diagnostic reporting. */ - function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[]): ExpandResult { + function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], fileExtensionMap: FileExtensionMap): ExpandResult { basePath = normalizePath(basePath); // The exclude spec list is converted into a regular expression, which allows us to quickly @@ -1199,7 +1199,7 @@ namespace ts { // Rather than requery this for each file and filespec, we query the supported extensions // once and store it on the expansion context. - const supportedExtensions = getSupportedExtensions(options); + const supportedExtensions = getSupportedExtensions(options, fileExtensionMap); // Literal files are always included verbatim. An "include" or "exclude" specification cannot // remove a literal file. diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 8175730159b0b..6d4aeed0fd5a0 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1912,8 +1912,16 @@ namespace ts { export const supportedJavascriptExtensions = [".js", ".jsx"]; const allSupportedExtensions = supportedTypeScriptExtensions.concat(supportedJavascriptExtensions); - export function getSupportedExtensions(options?: CompilerOptions): string[] { - return options && options.allowJs ? allSupportedExtensions : supportedTypeScriptExtensions; + export function getSupportedExtensions(options?: CompilerOptions, fileExtensionMap?: FileExtensionMap): string[] { + let typeScriptHostExtensions: string[] = []; + let allHostExtensions: string[] = []; + if (fileExtensionMap) { + allHostExtensions = concatenate(concatenate(fileExtensionMap.javaScript, fileExtensionMap.typeScript), fileExtensionMap.mixedContent); + typeScriptHostExtensions = fileExtensionMap.typeScript; + } + const allTypeScriptExtensions = concatenate(supportedTypeScriptExtensions, typeScriptHostExtensions); + const allExtensions = concatenate(allSupportedExtensions, allHostExtensions); + return options && options.allowJs ? allExtensions : allTypeScriptExtensions; } export function hasJavaScriptFileExtension(fileName: string) { @@ -1924,10 +1932,10 @@ namespace ts { return forEach(supportedTypeScriptExtensions, extension => fileExtensionIs(fileName, extension)); } - export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions) { + export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions, fileExtensionMap?: FileExtensionMap) { if (!fileName) { return false; } - for (const extension of getSupportedExtensions(compilerOptions)) { + for (const extension of getSupportedExtensions(compilerOptions, fileExtensionMap)) { if (fileExtensionIs(fileName, extension)) { return true; } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 5cd957708f51b..d346700c14d92 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -289,7 +289,7 @@ namespace ts { return resolutions; } - export function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program): Program { + export function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, fileExtensionMap?: FileExtensionMap): Program { let program: Program; let files: SourceFile[] = []; let commonSourceDirectory: string; @@ -324,7 +324,7 @@ namespace ts { let skipDefaultLib = options.noLib; const programDiagnostics = createDiagnosticCollection(); const currentDirectory = host.getCurrentDirectory(); - const supportedExtensions = getSupportedExtensions(options); + const supportedExtensions = getSupportedExtensions(options, fileExtensionMap); // Map storing if there is emit blocking diagnostics for given input const hasEmitBlockingDiagnostics = createFileMap(getCanonicalFileName); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 9889d3ad1af25..add641e37d405 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2997,6 +2997,12 @@ namespace ts { ThisProperty } + export interface FileExtensionMap { + javaScript?: string[]; + typeScript?: string[]; + mixedContent?: string[]; + } + export interface DiagnosticMessage { key: string; category: DiagnosticCategory; diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 17b1dd3e585f7..fcac76f15df81 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1441,6 +1441,66 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); }); + it("tsconfig script block support", () => { + const file1 = { + path: "/a/b/f1.ts", + content: ` ` + }; + const file2 = { + path: "/a/b/f2.html", + content: `var hello = "hello";` + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true } }) + }; + const host = createServerHost([file1, file2, config]); + const session = createSession(host); + openFilesForSession([file1], session); + const projectService = session.getProjectService(); + + // HTML file will not be included in any projects yet + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(projectService.configuredProjects[0], [file1.path]); + + // Specify .html extension as mixed content + const configureHostRequest = makeSessionRequest(CommandNames.Configure, { fileExtensionMap: { mixedContent: [".html"] } }); + session.executeCommand(configureHostRequest).response; + + // HTML file still not included in the project as it is closed + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(projectService.configuredProjects[0], [file1.path]); + + // Open HTML file + projectService.applyChangesInOpenFiles( + /*openFiles*/[{ fileName: file2.path, hasMixedContent: true, scriptKind: ScriptKind.JS, content: `var hello = "hello";` }], + /*changedFiles*/undefined, + /*closedFiles*/undefined); + + // Now HTML file is included in the project + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path]); + + // Check identifiers defined in HTML content are available in .ts file + const project = projectService.configuredProjects[0]; + let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1); + assert(completions && completions.entries[0].name === "hello", `expected entry hello to be in completion list`); + + // Close HTML file + projectService.applyChangesInOpenFiles( + /*openFiles*/undefined, + /*changedFiles*/undefined, + /*closedFiles*/[file2.path]); + + // HTML file is still included in project + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path]); + + // Check identifiers defined in HTML content are not available in .ts file + completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5); + assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`); + }); + it("project structure update is deferred if files are not added\removed", () => { const file1 = { path: "/a/b/f1.ts", diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index d7dcfc810b47e..1469ab96c8bc9 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -90,6 +90,7 @@ namespace ts.server { export interface HostConfiguration { formatCodeOptions: FormatCodeSettings; hostInfo: string; + fileExtensionMap?: FileExtensionMap; } interface ConfigFileConversionResult { @@ -114,13 +115,13 @@ namespace ts.server { interface FilePropertyReader { getFileName(f: T): string; getScriptKind(f: T): ScriptKind; - hasMixedContent(f: T): boolean; + hasMixedContent(f: T, mixedContentExtensions: string[]): boolean; } const fileNamePropertyReader: FilePropertyReader = { getFileName: x => x, getScriptKind: _ => undefined, - hasMixedContent: _ => false + hasMixedContent: (fileName, mixedContentExtensions) => forEach(mixedContentExtensions, extension => fileExtensionIs(fileName, extension)) }; const externalFilePropertyReader: FilePropertyReader = { @@ -235,12 +236,12 @@ namespace ts.server { private readonly directoryWatchers: DirectoryWatchers; private readonly throttledOperations: ThrottledOperations; - private readonly hostConfiguration: HostConfiguration; - private changedFiles: ScriptInfo[]; private toCanonicalFileName: (f: string) => string; + public readonly hostConfiguration: HostConfiguration; + public lastDeletedFile: ScriptInfo; constructor(public readonly host: ServerHost, @@ -264,7 +265,8 @@ namespace ts.server { this.hostConfiguration = { formatCodeOptions: getDefaultFormatCodeSettings(this.host), - hostInfo: "Unknown host" + hostInfo: "Unknown host", + fileExtensionMap: {} }; this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); @@ -455,7 +457,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.getCompilerOptions())) { + if (fileName && !ts.isSupportedSourceFileName(fileName, project.getCompilerOptions(), this.hostConfiguration.fileExtensionMap)) { return; } @@ -610,6 +612,9 @@ namespace ts.server { let projectsToRemove: Project[]; for (const p of info.containingProjects) { if (p.projectKind === ProjectKind.Configured) { + if (info.hasMixedContent) { + info.hasChanges = true; + } // last open file in configured project - close it if ((p).deleteOpenRef() === 0) { (projectsToRemove || (projectsToRemove = [])).push(p); @@ -772,7 +777,9 @@ namespace ts.server { this.host, getDirectoryPath(configFilename), /*existingOptions*/ {}, - configFilename); + configFilename, + /*resolutionStack*/ [], + this.hostConfiguration.fileExtensionMap); if (parsedCommandLine.errors.length) { errors = concatenate(errors, parsedCommandLine.errors); @@ -876,7 +883,7 @@ namespace ts.server { for (const f of files) { const rootFilename = propertyReader.getFileName(f); const scriptKind = propertyReader.getScriptKind(f); - const hasMixedContent = propertyReader.hasMixedContent(f); + const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.fileExtensionMap.mixedContent); if (this.host.fileExists(rootFilename)) { const info = this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(rootFilename), /*openedByClient*/ clientFileName == rootFilename, /*fileContent*/ undefined, scriptKind, hasMixedContent); project.addRoot(info); @@ -922,7 +929,7 @@ namespace ts.server { rootFilesChanged = true; if (!scriptInfo) { const scriptKind = propertyReader.getScriptKind(f); - const hasMixedContent = propertyReader.hasMixedContent(f); + const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.fileExtensionMap.mixedContent); scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent); } } @@ -1072,6 +1079,9 @@ namespace ts.server { } if (openedByClient) { info.isOpen = true; + if (hasMixedContent) { + info.hasChanges = true; + } } } return info; @@ -1103,6 +1113,10 @@ namespace ts.server { mergeMaps(this.hostConfiguration.formatCodeOptions, convertFormatOptions(args.formatOptions)); this.logger.info("Format host information updated"); } + if (args.fileExtensionMap) { + this.hostConfiguration.fileExtensionMap = args.fileExtensionMap; + this.logger.info("Host file extension mappings updated"); + } } } @@ -1168,12 +1182,12 @@ namespace ts.server { } openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean): OpenConfiguredProjectResult { + const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent); 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, hasMixedContent); this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ true); this.printProjects(); return { configFileName, configFileErrors }; diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index f1e80d95880e2..ced51a256f06c 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -5,6 +5,7 @@ namespace ts.server { export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost, ServerLanguageServiceHost { private compilationSettings: ts.CompilerOptions; + private fileExtensionMap: FileExtensionMap; private readonly resolvedModuleNames= createFileMap>(); private readonly resolvedTypeReferenceDirectives = createFileMap>(); private readonly getCanonicalFileName: (fileName: string) => string; @@ -143,6 +144,10 @@ namespace ts.server { return this.compilationSettings; } + getFileExtensionMap() { + return this.fileExtensionMap; + } + useCaseSensitiveFileNames() { return this.host.useCaseSensitiveFileNames; } @@ -231,5 +236,9 @@ namespace ts.server { } this.compilationSettings = opt; } + + setFileExtensionMap(fileExtensionMap: FileExtensionMap) { + this.fileExtensionMap = fileExtensionMap || {}; + } } } \ No newline at end of file diff --git a/src/server/project.ts b/src/server/project.ts index d01df728248c6..47ddc5abf74fa 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1,4 +1,4 @@ -/// +/// /// /// /// @@ -202,6 +202,7 @@ namespace ts.server { enableLanguageService() { const lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken); lsHost.setCompilationSettings(this.compilerOptions); + lsHost.setFileExtensionMap(this.projectService.hostConfiguration.fileExtensionMap); this.languageService = ts.createLanguageService(lsHost, this.documentRegistry); this.lsHost = lsHost; @@ -462,6 +463,10 @@ namespace ts.server { return !hasChanges; } + private hasChangedFiles() { + return this.rootFiles && forEach(this.rootFiles, info => info.hasChanges); + } + private setTypings(typings: SortedReadonlyArray): boolean { if (arrayIsEqualTo(this.typingFiles, typings)) { return false; @@ -475,7 +480,7 @@ namespace ts.server { const oldProgram = this.program; this.program = this.languageService.getProgram(); - let hasChanges = false; + let hasChanges = this.hasChangedFiles(); // 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. @@ -578,6 +583,7 @@ namespace ts.server { const added: string[] = []; const removed: string[] = []; + const updated = this.rootFiles.filter(info => info.hasChanges).map(info => info.fileName); for (const id in currentFiles) { if (!hasProperty(lastReportedFileNames, id)) { added.push(id); @@ -588,9 +594,12 @@ namespace ts.server { removed.push(id); } } + for (const root of this.rootFiles) { + root.hasChanges = false; + } this.lastReportedFileNames = currentFiles; this.lastReportedVersion = this.projectStructureVersion; - return { info, changes: { added, removed }, projectErrors: this.projectErrors }; + return { info, changes: { added, removed, updated }, projectErrors: this.projectErrors }; } else { // unknown version - return everything diff --git a/src/server/protocol.ts b/src/server/protocol.ts index d13caf7f01b0c..23ff2e02a8bf2 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -914,6 +914,10 @@ namespace ts.server.protocol { * List of removed files */ removed: string[]; + /** + * List of updated files + */ + updated: string[]; } /** @@ -986,6 +990,11 @@ namespace ts.server.protocol { * The format options to use during formatting and other code editing features. */ formatOptions?: FormatCodeSettings; + + /** + * The host's supported file extension mappings + */ + fileExtensionMap?: FileExtensionMap; } /** diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 84649863a7b3a..e5e1c3577e017 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -13,7 +13,6 @@ namespace ts.server { private fileWatcher: FileWatcher; private svc: ScriptVersionCache; - // TODO: allow to update hasMixedContent from the outside constructor( private readonly host: ServerHost, readonly fileName: NormalizedPath, @@ -29,6 +28,8 @@ namespace ts.server { : getScriptKindFromFileName(fileName); } + public hasChanges = false; + getFormatCodeSettings() { return this.formatCodeSettings; } diff --git a/src/server/utilities.ts b/src/server/utilities.ts index ac80965211923..407f126ec37a5 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -1,4 +1,4 @@ -/// +/// /// namespace ts.server { @@ -211,6 +211,7 @@ namespace ts.server { export interface ServerLanguageServiceHost { setCompilationSettings(options: CompilerOptions): void; + setFileExtensionMap(fileExtensionMap: FileExtensionMap): void; notifyFileRemoved(info: ScriptInfo): void; startRecordingFilesWithChangedResolutions(): void; finishRecordingFilesWithChangedResolutions(): Path[]; @@ -218,6 +219,7 @@ namespace ts.server { export const nullLanguageServiceHost: ServerLanguageServiceHost = { setCompilationSettings: () => undefined, + setFileExtensionMap: () => undefined, notifyFileRemoved: () => undefined, startRecordingFilesWithChangedResolutions: () => undefined, finishRecordingFilesWithChangedResolutions: () => undefined diff --git a/src/services/completions.ts b/src/services/completions.ts index b710aa8cd78bb..8820f4fee5466 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -271,13 +271,14 @@ namespace ts.Completions { const span = getDirectoryFragmentTextSpan((node).text, node.getStart() + 1); let entries: CompletionEntry[]; if (isPathRelativeToScript(literalValue) || isRootedDiskPath(literalValue)) { + const fileExtensionMap = host.getFileExtensionMap ? host.getFileExtensionMap() : {}; if (compilerOptions.rootDirs) { entries = getCompletionEntriesForDirectoryFragmentWithRootDirs( - compilerOptions.rootDirs, literalValue, scriptDirectory, getSupportedExtensions(compilerOptions), /*includeExtensions*/false, span, scriptPath); + compilerOptions.rootDirs, literalValue, scriptDirectory, getSupportedExtensions(compilerOptions, fileExtensionMap), /*includeExtensions*/false, span, scriptPath); } else { entries = getCompletionEntriesForDirectoryFragment( - literalValue, scriptDirectory, getSupportedExtensions(compilerOptions), /*includeExtensions*/false, span, scriptPath); + literalValue, scriptDirectory, getSupportedExtensions(compilerOptions, fileExtensionMap), /*includeExtensions*/false, span, scriptPath); } } else { @@ -411,7 +412,8 @@ namespace ts.Completions { let result: CompletionEntry[]; if (baseUrl) { - const fileExtensions = getSupportedExtensions(compilerOptions); + const fileExtensionMap = host.getFileExtensionMap ? host.getFileExtensionMap() : {}; + const fileExtensions = getSupportedExtensions(compilerOptions, fileExtensionMap); const projectDir = compilerOptions.project || host.getCurrentDirectory(); const absolute = isRootedDiskPath(baseUrl) ? baseUrl : combinePaths(projectDir, baseUrl); result = getCompletionEntriesForDirectoryFragment(fragment, normalizePath(absolute), fileExtensions, /*includeExtensions*/false, span); @@ -588,7 +590,8 @@ namespace ts.Completions { if (kind === "path") { // Give completions for a relative path const span: TextSpan = getDirectoryFragmentTextSpan(toComplete, range.pos + prefix.length); - completionInfo.entries = getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getSupportedExtensions(compilerOptions), /*includeExtensions*/true, span, sourceFile.path); + const fileExtensionMap = host.getFileExtensionMap ? host.getFileExtensionMap() : {}; + completionInfo.entries = getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getSupportedExtensions(compilerOptions, fileExtensionMap), /*includeExtensions*/true, span, sourceFile.path); } else { // Give completions based on the typings available diff --git a/src/services/services.ts b/src/services/services.ts index 56e604abeb3eb..77a975246320d 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -740,6 +740,7 @@ namespace ts { class HostCache { private fileNameToEntry: FileMap; private _compilationSettings: CompilerOptions; + private _fileExtensionMap: FileExtensionMap; private currentDirectory: string; constructor(private host: LanguageServiceHost, private getCanonicalFileName: (fileName: string) => string) { @@ -755,12 +756,18 @@ namespace ts { // store the compilation settings this._compilationSettings = host.getCompilationSettings() || getDefaultCompilerOptions(); + + this._fileExtensionMap = host.getFileExtensionMap ? host.getFileExtensionMap() : {}; } public compilationSettings() { return this._compilationSettings; } + public fileExtensionMap() { + return this._fileExtensionMap; + } + private createEntry(fileName: string, path: Path) { let entry: HostFileInformation; const scriptSnapshot = this.host.getScriptSnapshot(fileName); @@ -1083,7 +1090,7 @@ namespace ts { } const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings); - const newProgram = createProgram(hostCache.getRootFileNames(), newSettings, compilerHost, program); + const newProgram = createProgram(hostCache.getRootFileNames(), newSettings, compilerHost, program, hostCache.fileExtensionMap()); // Release any files we have acquired in the old program but are // not part of the new program. diff --git a/src/services/types.ts b/src/services/types.ts index 4e04df3fc7caf..1481b68e5b8f7 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -125,6 +125,7 @@ namespace ts { // export interface LanguageServiceHost { getCompilationSettings(): CompilerOptions; + getFileExtensionMap?(): FileExtensionMap; getNewLine?(): string; getProjectVersion?(): string; getScriptFileNames(): string[]; From 64dad30ca0c2d2870bf58d7b189ab671689e992c Mon Sep 17 00:00:00 2001 From: Jason Ramsay Date: Wed, 7 Dec 2016 15:31:46 -0800 Subject: [PATCH 3/9] Reduced version from CR comments --- src/compiler/program.ts | 4 ++-- src/server/lsHost.ts | 9 --------- src/server/project.ts | 1 - src/services/completions.ts | 11 ++++------- src/services/services.ts | 9 +-------- src/services/types.ts | 1 - 6 files changed, 7 insertions(+), 28 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 2d92561ad93a2..73976d5d02e73 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -285,7 +285,7 @@ namespace ts { return resolutions; } - export function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, fileExtensionMap?: FileExtensionMap): Program { + export function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program): Program { let program: Program; let files: SourceFile[] = []; let commonSourceDirectory: string; @@ -320,7 +320,7 @@ namespace ts { let skipDefaultLib = options.noLib; const programDiagnostics = createDiagnosticCollection(); const currentDirectory = host.getCurrentDirectory(); - const supportedExtensions = getSupportedExtensions(options, fileExtensionMap); + const supportedExtensions = getSupportedExtensions(options); // Map storing if there is emit blocking diagnostics for given input const hasEmitBlockingDiagnostics = createFileMap(getCanonicalFileName); diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index f8b4d28af8451..8f57cbf40746b 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -5,7 +5,6 @@ namespace ts.server { export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost { private compilationSettings: ts.CompilerOptions; - private fileExtensionMap: FileExtensionMap; private readonly resolvedModuleNames = createFileMap>(); private readonly resolvedTypeReferenceDirectives = createFileMap>(); private readonly getCanonicalFileName: (fileName: string) => string; @@ -144,10 +143,6 @@ namespace ts.server { return this.compilationSettings; } - getFileExtensionMap() { - return this.fileExtensionMap; - } - useCaseSensitiveFileNames() { return this.host.useCaseSensitiveFileNames; } @@ -236,9 +231,5 @@ namespace ts.server { } this.compilationSettings = opt; } - - setFileExtensionMap(fileExtensionMap: FileExtensionMap) { - this.fileExtensionMap = fileExtensionMap || {}; - } } } \ No newline at end of file diff --git a/src/server/project.ts b/src/server/project.ts index 31c651e7cc3c0..f17494149e580 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -254,7 +254,6 @@ namespace ts.server { this.lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken); this.lsHost.setCompilationSettings(this.compilerOptions); - this.lsHost.setFileExtensionMap(this.projectService.hostConfiguration.fileExtensionMap); this.languageService = ts.createLanguageService(this.lsHost, this.documentRegistry); this.noSemanticFeaturesLanguageService = createNoSemanticFeaturesWrapper(this.languageService); diff --git a/src/services/completions.ts b/src/services/completions.ts index 8820f4fee5466..b710aa8cd78bb 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -271,14 +271,13 @@ namespace ts.Completions { const span = getDirectoryFragmentTextSpan((node).text, node.getStart() + 1); let entries: CompletionEntry[]; if (isPathRelativeToScript(literalValue) || isRootedDiskPath(literalValue)) { - const fileExtensionMap = host.getFileExtensionMap ? host.getFileExtensionMap() : {}; if (compilerOptions.rootDirs) { entries = getCompletionEntriesForDirectoryFragmentWithRootDirs( - compilerOptions.rootDirs, literalValue, scriptDirectory, getSupportedExtensions(compilerOptions, fileExtensionMap), /*includeExtensions*/false, span, scriptPath); + compilerOptions.rootDirs, literalValue, scriptDirectory, getSupportedExtensions(compilerOptions), /*includeExtensions*/false, span, scriptPath); } else { entries = getCompletionEntriesForDirectoryFragment( - literalValue, scriptDirectory, getSupportedExtensions(compilerOptions, fileExtensionMap), /*includeExtensions*/false, span, scriptPath); + literalValue, scriptDirectory, getSupportedExtensions(compilerOptions), /*includeExtensions*/false, span, scriptPath); } } else { @@ -412,8 +411,7 @@ namespace ts.Completions { let result: CompletionEntry[]; if (baseUrl) { - const fileExtensionMap = host.getFileExtensionMap ? host.getFileExtensionMap() : {}; - const fileExtensions = getSupportedExtensions(compilerOptions, fileExtensionMap); + const fileExtensions = getSupportedExtensions(compilerOptions); const projectDir = compilerOptions.project || host.getCurrentDirectory(); const absolute = isRootedDiskPath(baseUrl) ? baseUrl : combinePaths(projectDir, baseUrl); result = getCompletionEntriesForDirectoryFragment(fragment, normalizePath(absolute), fileExtensions, /*includeExtensions*/false, span); @@ -590,8 +588,7 @@ namespace ts.Completions { if (kind === "path") { // Give completions for a relative path const span: TextSpan = getDirectoryFragmentTextSpan(toComplete, range.pos + prefix.length); - const fileExtensionMap = host.getFileExtensionMap ? host.getFileExtensionMap() : {}; - completionInfo.entries = getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getSupportedExtensions(compilerOptions, fileExtensionMap), /*includeExtensions*/true, span, sourceFile.path); + completionInfo.entries = getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getSupportedExtensions(compilerOptions), /*includeExtensions*/true, span, sourceFile.path); } else { // Give completions based on the typings available diff --git a/src/services/services.ts b/src/services/services.ts index 66b0b10a09015..ef50f4b5a2f3a 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -757,7 +757,6 @@ namespace ts { class HostCache { private fileNameToEntry: FileMap; private _compilationSettings: CompilerOptions; - private _fileExtensionMap: FileExtensionMap; private currentDirectory: string; constructor(private host: LanguageServiceHost, private getCanonicalFileName: (fileName: string) => string) { @@ -773,18 +772,12 @@ namespace ts { // store the compilation settings this._compilationSettings = host.getCompilationSettings() || getDefaultCompilerOptions(); - - this._fileExtensionMap = host.getFileExtensionMap ? host.getFileExtensionMap() : {}; } public compilationSettings() { return this._compilationSettings; } - public fileExtensionMap() { - return this._fileExtensionMap; - } - private createEntry(fileName: string, path: Path) { let entry: HostFileInformation; const scriptSnapshot = this.host.getScriptSnapshot(fileName); @@ -1107,7 +1100,7 @@ namespace ts { } const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings); - const newProgram = createProgram(hostCache.getRootFileNames(), newSettings, compilerHost, program, hostCache.fileExtensionMap()); + const newProgram = createProgram(hostCache.getRootFileNames(), newSettings, compilerHost, program); // Release any files we have acquired in the old program but are // not part of the new program. diff --git a/src/services/types.ts b/src/services/types.ts index 6e688e409af23..6a0e6e886b583 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -126,7 +126,6 @@ namespace ts { // export interface LanguageServiceHost { getCompilationSettings(): CompilerOptions; - getFileExtensionMap?(): FileExtensionMap; getNewLine?(): string; getProjectVersion?(): string; getScriptFileNames(): string[]; From 7a11453e3599a40f41495ac7a656e6e47779b2d0 Mon Sep 17 00:00:00 2001 From: Jason Ramsay Date: Wed, 7 Dec 2016 15:45:41 -0800 Subject: [PATCH 4/9] Fix merge issues --- src/server/editorServices.ts | 4 ++-- src/server/project.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 19e9d6870bbd2..44381e9e19ff8 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -254,12 +254,12 @@ namespace ts.server { private readonly directoryWatchers: DirectoryWatchers; private readonly throttledOperations: ThrottledOperations; + private readonly hostConfiguration: HostConfiguration; + private changedFiles: ScriptInfo[]; readonly toCanonicalFileName: (f: string) => string; - public readonly hostConfiguration: HostConfiguration; - public lastDeletedFile: ScriptInfo; constructor(public readonly host: ServerHost, diff --git a/src/server/project.ts b/src/server/project.ts index 5b20da1dbebe6..9df839aad289e 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -296,6 +296,9 @@ namespace ts.server { } enableLanguageService() { + if (this.languageServiceEnabled) { + return; + } this.languageServiceEnabled = true; this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ true); } From d52894302abffb97fa5c949e66a5986e14b80ba1 Mon Sep 17 00:00:00 2001 From: Jason Ramsay Date: Thu, 8 Dec 2016 17:56:08 -0800 Subject: [PATCH 5/9] Changes due to CR comments --- src/compiler/commandLineParser.ts | 4 ++-- src/compiler/core.ts | 8 +++---- src/compiler/types.ts | 8 +++---- .../unittests/tsserverProjectSystem.ts | 3 ++- src/server/editorServices.ts | 21 +++++++++-------- src/server/project.ts | 23 +++++++++++-------- src/server/protocol.ts | 2 +- src/server/scriptInfo.ts | 8 +++++-- 8 files changed, 44 insertions(+), 33 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 4dcee48d692c3..9c7b6f48b3a9f 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -841,7 +841,7 @@ namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = [], fileExtensionMap: FileExtensionMap = {}): ParsedCommandLine { + export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = [], fileExtensionMap: FileExtensionMapItem[] = []): ParsedCommandLine { const errors: Diagnostic[] = []; const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); const resolvedPath = toPath(configFileName || "", basePath, getCanonicalFileName); @@ -1185,7 +1185,7 @@ namespace ts { * @param host The host used to resolve files and directories. * @param errors An array for diagnostic reporting. */ - function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], fileExtensionMap: FileExtensionMap): ExpandResult { + function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], fileExtensionMap: FileExtensionMapItem[]): ExpandResult { basePath = normalizePath(basePath); // The exclude spec list is converted into a regular expression, which allows us to quickly diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 16b42d5084e0b..f1cf4ffcaa991 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1942,12 +1942,12 @@ namespace ts { export const supportedJavascriptExtensions = [".js", ".jsx"]; const allSupportedExtensions = supportedTypeScriptExtensions.concat(supportedJavascriptExtensions); - export function getSupportedExtensions(options?: CompilerOptions, fileExtensionMap?: FileExtensionMap): string[] { + export function getSupportedExtensions(options?: CompilerOptions, fileExtensionMap?: FileExtensionMapItem[]): string[] { let typeScriptHostExtensions: string[] = []; let allHostExtensions: string[] = []; if (fileExtensionMap) { - allHostExtensions = concatenate(concatenate(fileExtensionMap.javaScript, fileExtensionMap.typeScript), fileExtensionMap.mixedContent); - typeScriptHostExtensions = fileExtensionMap.typeScript; + allHostExtensions = ts.map(fileExtensionMap, item => item.extension); + typeScriptHostExtensions = ts.map(ts.filter(fileExtensionMap, item => item.scriptKind === ScriptKind.TS), item => item.extension); } const allTypeScriptExtensions = concatenate(supportedTypeScriptExtensions, typeScriptHostExtensions); const allExtensions = concatenate(allSupportedExtensions, allHostExtensions); @@ -1962,7 +1962,7 @@ namespace ts { return forEach(supportedTypeScriptExtensions, extension => fileExtensionIs(fileName, extension)); } - export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions, fileExtensionMap?: FileExtensionMap) { + export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions, fileExtensionMap?: FileExtensionMapItem[]) { if (!fileName) { return false; } for (const extension of getSupportedExtensions(compilerOptions, fileExtensionMap)) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 97eb145916a9c..3f1a5465c879e 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3086,10 +3086,10 @@ namespace ts { ThisProperty } - export interface FileExtensionMap { - javaScript?: string[]; - typeScript?: string[]; - mixedContent?: string[]; + export interface FileExtensionMapItem { + extension: string; + scriptKind: ScriptKind; + isMixedContent: boolean; } export interface DiagnosticMessage { diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 7218b419f7c51..c3ced46e4532a 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1552,7 +1552,8 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.configuredProjects[0], [file1.path]); // Specify .html extension as mixed content - const configureHostRequest = makeSessionRequest(CommandNames.Configure, { fileExtensionMap: { mixedContent: [".html"] } }); + const fileExtensionMap = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }]; + const configureHostRequest = makeSessionRequest(CommandNames.Configure, { fileExtensionMap }); session.executeCommand(configureHostRequest).response; // HTML file still not included in the project as it is closed diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 44381e9e19ff8..69d7d6076419b 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -108,7 +108,7 @@ namespace ts.server { export interface HostConfiguration { formatCodeOptions: FormatCodeSettings; hostInfo: string; - fileExtensionMap?: FileExtensionMap; + fileExtensionMap?: FileExtensionMapItem[]; } interface ConfigFileConversionResult { @@ -133,13 +133,16 @@ namespace ts.server { interface FilePropertyReader { getFileName(f: T): string; getScriptKind(f: T): ScriptKind; - hasMixedContent(f: T, mixedContentExtensions: string[]): boolean; + hasMixedContent(f: T, fileExtensionMap: FileExtensionMapItem[]): boolean; } const fileNamePropertyReader: FilePropertyReader = { getFileName: x => x, getScriptKind: _ => undefined, - hasMixedContent: (fileName, mixedContentExtensions) => forEach(mixedContentExtensions, extension => fileExtensionIs(fileName, extension)) + hasMixedContent: (fileName, fileExtensionMap) => { + const mixedContentExtensions = ts.map(ts.filter(fileExtensionMap, item => item.isMixedContent), item => item.extension); + return forEach(mixedContentExtensions, extension => fileExtensionIs(fileName, extension)) + } }; const externalFilePropertyReader: FilePropertyReader = { @@ -284,7 +287,7 @@ namespace ts.server { this.hostConfiguration = { formatCodeOptions: getDefaultFormatCodeSettings(this.host), hostInfo: "Unknown host", - fileExtensionMap: {} + fileExtensionMap: [] }; this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); @@ -646,7 +649,7 @@ namespace ts.server { for (const p of info.containingProjects) { if (p.projectKind === ProjectKind.Configured) { if (info.hasMixedContent) { - info.hasChanges = true; + info.registerFileUpdate(); } // last open file in configured project - close it if ((p).deleteOpenRef() === 0) { @@ -922,7 +925,7 @@ namespace ts.server { for (const f of files) { const rootFilename = propertyReader.getFileName(f); const scriptKind = propertyReader.getScriptKind(f); - const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.fileExtensionMap.mixedContent); + const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.fileExtensionMap); if (this.host.fileExists(rootFilename)) { const info = this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(rootFilename), /*openedByClient*/ clientFileName == rootFilename, /*fileContent*/ undefined, scriptKind, hasMixedContent); project.addRoot(info); @@ -968,7 +971,7 @@ namespace ts.server { rootFilesChanged = true; if (!scriptInfo) { const scriptKind = propertyReader.getScriptKind(f); - const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.fileExtensionMap.mixedContent); + const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.fileExtensionMap); scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent); } } @@ -1123,7 +1126,7 @@ namespace ts.server { if (openedByClient) { info.isOpen = true; if (hasMixedContent) { - info.hasChanges = true; + info.registerFileUpdate(); } } } @@ -1225,12 +1228,12 @@ namespace ts.server { } openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean): OpenConfiguredProjectResult { - const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent); 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, hasMixedContent); this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ true); this.printProjects(); return { configFileName, configFileErrors }; diff --git a/src/server/project.ts b/src/server/project.ts index 9df839aad289e..bf1bd6a9fb956 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -187,6 +187,10 @@ namespace ts.server { public languageServiceEnabled = true; builder: Builder; + /** + * Set of files names that were updated since the last call to getChangesSinceVersion. + */ + private updatedFileNames: Map; /** * Set of files that was returned from the last call to getChangesSinceVersion. */ @@ -208,6 +212,7 @@ namespace ts.server { */ private projectStateVersion = 0; + private typingFiles: SortedReadonlyArray; protected projectErrors: Diagnostic[]; @@ -480,6 +485,10 @@ namespace ts.server { this.markAsDirty(); } + registerFileUpdate(fileName: string) { + (this.updatedFileNames || (this.updatedFileNames = createMap()))[fileName] = fileName; + } + markAsDirty() { this.projectStateVersion++; } @@ -562,10 +571,6 @@ namespace ts.server { return !hasChanges; } - private hasChangedFiles() { - return this.rootFiles && forEach(this.rootFiles, info => info.hasChanges); - } - private setTypings(typings: SortedReadonlyArray): boolean { if (arrayIsEqualTo(this.typingFiles, typings)) { return false; @@ -579,7 +584,7 @@ namespace ts.server { const oldProgram = this.program; this.program = this.languageService.getProgram(); - let hasChanges = this.hasChangedFiles(); + let hasChanges = false; // 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. @@ -674,7 +679,7 @@ namespace ts.server { // 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) { + if (this.projectStructureVersion == this.lastReportedVersion && !this.updatedFileNames) { return { info, projectErrors: this.projectErrors }; } // compute and return the difference @@ -683,7 +688,7 @@ namespace ts.server { const added: string[] = []; const removed: string[] = []; - const updated = this.rootFiles.filter(info => info.hasChanges).map(info => info.fileName); + const updated: string[] = getOwnKeys(this.updatedFileNames); for (const id in currentFiles) { if (!hasProperty(lastReportedFileNames, id)) { added.push(id); @@ -694,11 +699,9 @@ namespace ts.server { removed.push(id); } } - for (const root of this.rootFiles) { - root.hasChanges = false; - } this.lastReportedFileNames = currentFiles; this.lastReportedVersion = this.projectStructureVersion; + this.updatedFileNames = undefined; return { info, changes: { added, removed, updated }, projectErrors: this.projectErrors }; } else { diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 2e721dc8967b1..6df390344ccc9 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -998,7 +998,7 @@ namespace ts.server.protocol { /** * The host's supported file extension mappings */ - fileExtensionMap?: FileExtensionMap; + fileExtensionMap?: FileExtensionMapItem[]; } /** diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index e5e1c3577e017..ee3cfb0e1372a 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -28,8 +28,6 @@ namespace ts.server { : getScriptKindFromFileName(fileName); } - public hasChanges = false; - getFormatCodeSettings() { return this.formatCodeSettings; } @@ -91,6 +89,12 @@ namespace ts.server { return this.containingProjects[0]; } + registerFileUpdate(): void { + for (const p of this.containingProjects) { + p.registerFileUpdate(this.path); + } + } + setFormatOptions(formatSettings: FormatCodeSettings): void { if (formatSettings) { if (!this.formatCodeSettings) { From 5f46e488b7e70a8c0c37e84da326de304468512a Mon Sep 17 00:00:00 2001 From: Jason Ramsay Date: Fri, 9 Dec 2016 11:08:12 -0800 Subject: [PATCH 6/9] Mark containing project as dirty when file is closed (Note: adding this until PR #12789 is merged in so that unit tests pass) --- src/server/project.ts | 1 - src/server/scriptInfo.ts | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/server/project.ts b/src/server/project.ts index 608ba9917e3e7..005e26a1bc8f1 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -212,7 +212,6 @@ namespace ts.server { */ private projectStateVersion = 0; - private typingFiles: SortedReadonlyArray; protected projectErrors: Diagnostic[]; diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index b8d4ec09a13b8..0acd45d0287eb 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -28,9 +28,9 @@ namespace ts.server { this.switchToScriptVersionCache(newText); } - public useText() { + public useText(newText?: string) { this.svc = undefined; - this.reloadFromFile(); + this.setText(newText); } public edit(start: number, end: number, newText: string) { @@ -198,7 +198,8 @@ namespace ts.server { public close() { this.isOpen = false; - this.textStorage.useText(); + this.textStorage.useText(this.hasMixedContent ? "" : undefined); + this.markContainingProjectsAsDirty(); } public getSnapshot() { From 05160cae8ecde10fd7e2f7948ac4d9069aa56325 Mon Sep 17 00:00:00 2001 From: Jason Ramsay Date: Fri, 9 Dec 2016 13:36:43 -0800 Subject: [PATCH 7/9] Rename fileExtensionMap: fileExtensionMapItem[] to extraFileExtensions: FileExtensionInfo[] --- src/compiler/commandLineParser.ts | 8 +++---- src/compiler/core.ts | 12 +++++----- src/compiler/types.ts | 2 +- .../unittests/tsserverProjectSystem.ts | 4 ++-- src/server/editorServices.ts | 22 +++++++++---------- src/server/protocol.ts | 6 ++--- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 9c7b6f48b3a9f..251eeb58b1569 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -841,7 +841,7 @@ namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = [], fileExtensionMap: FileExtensionMapItem[] = []): ParsedCommandLine { + export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, configFileName?: string, resolutionStack: Path[] = [], extraFileExtensions: FileExtensionInfo[] = []): ParsedCommandLine { const errors: Diagnostic[] = []; const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); const resolvedPath = toPath(configFileName || "", basePath, getCanonicalFileName); @@ -981,7 +981,7 @@ namespace ts { includeSpecs = ["**/*"]; } - const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors, fileExtensionMap); + const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors, extraFileExtensions); if (result.fileNames.length === 0 && !hasProperty(json, "files") && resolutionStack.length === 0) { errors.push( @@ -1185,7 +1185,7 @@ namespace ts { * @param host The host used to resolve files and directories. * @param errors An array for diagnostic reporting. */ - function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], fileExtensionMap: FileExtensionMapItem[]): ExpandResult { + function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], extraFileExtensions: FileExtensionInfo[]): ExpandResult { basePath = normalizePath(basePath); // The exclude spec list is converted into a regular expression, which allows us to quickly @@ -1219,7 +1219,7 @@ namespace ts { // Rather than requery this for each file and filespec, we query the supported extensions // once and store it on the expansion context. - const supportedExtensions = getSupportedExtensions(options, fileExtensionMap); + const supportedExtensions = getSupportedExtensions(options, extraFileExtensions); // Literal files are always included verbatim. An "include" or "exclude" specification cannot // remove a literal file. diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 01cb80725efa3..f35bfd28f74fc 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1924,12 +1924,12 @@ namespace ts { export const supportedJavascriptExtensions = [".js", ".jsx"]; const allSupportedExtensions = supportedTypeScriptExtensions.concat(supportedJavascriptExtensions); - export function getSupportedExtensions(options?: CompilerOptions, fileExtensionMap?: FileExtensionMapItem[]): string[] { + export function getSupportedExtensions(options?: CompilerOptions, extraFileExtensions?: FileExtensionInfo[]): string[] { let typeScriptHostExtensions: string[] = []; let allHostExtensions: string[] = []; - if (fileExtensionMap) { - allHostExtensions = ts.map(fileExtensionMap, item => item.extension); - typeScriptHostExtensions = ts.map(ts.filter(fileExtensionMap, item => item.scriptKind === ScriptKind.TS), item => item.extension); + if (extraFileExtensions) { + allHostExtensions = ts.map(extraFileExtensions, item => item.extension); + typeScriptHostExtensions = ts.map(ts.filter(extraFileExtensions, item => item.scriptKind === ScriptKind.TS), item => item.extension); } const allTypeScriptExtensions = concatenate(supportedTypeScriptExtensions, typeScriptHostExtensions); const allExtensions = concatenate(allSupportedExtensions, allHostExtensions); @@ -1944,10 +1944,10 @@ namespace ts { return forEach(supportedTypeScriptExtensions, extension => fileExtensionIs(fileName, extension)); } - export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions, fileExtensionMap?: FileExtensionMapItem[]) { + export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions, extraFileExtensions?: FileExtensionInfo[]) { if (!fileName) { return false; } - for (const extension of getSupportedExtensions(compilerOptions, fileExtensionMap)) { + for (const extension of getSupportedExtensions(compilerOptions, extraFileExtensions)) { if (fileExtensionIs(fileName, extension)) { return true; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 3f1a5465c879e..da9d91e2acf9e 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3086,7 +3086,7 @@ namespace ts { ThisProperty } - export interface FileExtensionMapItem { + export interface FileExtensionInfo { extension: string; scriptKind: ScriptKind; isMixedContent: boolean; diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 6ad525299fbf4..bc0d6662b9a2f 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1552,8 +1552,8 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.configuredProjects[0], [file1.path]); // Specify .html extension as mixed content - const fileExtensionMap = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }]; - const configureHostRequest = makeSessionRequest(CommandNames.Configure, { fileExtensionMap }); + const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }]; + const configureHostRequest = makeSessionRequest(CommandNames.Configure, { extraFileExtensions }); session.executeCommand(configureHostRequest).response; // HTML file still not included in the project as it is closed diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index ded9f786dba1e..a70b16a5b1e5d 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -108,7 +108,7 @@ namespace ts.server { export interface HostConfiguration { formatCodeOptions: FormatCodeSettings; hostInfo: string; - fileExtensionMap?: FileExtensionMapItem[]; + extraFileExtensions?: FileExtensionInfo[]; } interface ConfigFileConversionResult { @@ -133,14 +133,14 @@ namespace ts.server { interface FilePropertyReader { getFileName(f: T): string; getScriptKind(f: T): ScriptKind; - hasMixedContent(f: T, fileExtensionMap: FileExtensionMapItem[]): boolean; + hasMixedContent(f: T, extraFileExtensions: FileExtensionInfo[]): boolean; } const fileNamePropertyReader: FilePropertyReader = { getFileName: x => x, getScriptKind: _ => undefined, - hasMixedContent: (fileName, fileExtensionMap) => { - const mixedContentExtensions = ts.map(ts.filter(fileExtensionMap, item => item.isMixedContent), item => item.extension); + hasMixedContent: (fileName, extraFileExtensions) => { + const mixedContentExtensions = ts.map(ts.filter(extraFileExtensions, item => item.isMixedContent), item => item.extension); return forEach(mixedContentExtensions, extension => fileExtensionIs(fileName, extension)) } }; @@ -287,7 +287,7 @@ namespace ts.server { this.hostConfiguration = { formatCodeOptions: getDefaultFormatCodeSettings(this.host), hostInfo: "Unknown host", - fileExtensionMap: [] + extraFileExtensions: [] }; this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); @@ -491,7 +491,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.getCompilerOptions(), this.hostConfiguration.fileExtensionMap)) { + if (fileName && !ts.isSupportedSourceFileName(fileName, project.getCompilerOptions(), this.hostConfiguration.extraFileExtensions)) { return; } @@ -820,7 +820,7 @@ namespace ts.server { /*existingOptions*/ {}, configFilename, /*resolutionStack*/ [], - this.hostConfiguration.fileExtensionMap); + this.hostConfiguration.extraFileExtensions); if (parsedCommandLine.errors.length) { errors = concatenate(errors, parsedCommandLine.errors); @@ -924,7 +924,7 @@ namespace ts.server { for (const f of files) { const rootFilename = propertyReader.getFileName(f); const scriptKind = propertyReader.getScriptKind(f); - const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.fileExtensionMap); + const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); if (this.host.fileExists(rootFilename)) { const info = this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(rootFilename), /*openedByClient*/ clientFileName == rootFilename, /*fileContent*/ undefined, scriptKind, hasMixedContent); project.addRoot(info); @@ -970,7 +970,7 @@ namespace ts.server { rootFilesChanged = true; if (!scriptInfo) { const scriptKind = propertyReader.getScriptKind(f); - const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.fileExtensionMap); + const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent); } } @@ -1157,8 +1157,8 @@ namespace ts.server { mergeMaps(this.hostConfiguration.formatCodeOptions, convertFormatOptions(args.formatOptions)); this.logger.info("Format host information updated"); } - if (args.fileExtensionMap) { - this.hostConfiguration.fileExtensionMap = args.fileExtensionMap; + if (args.extraFileExtensions) { + this.hostConfiguration.extraFileExtensions = args.extraFileExtensions; this.logger.info("Host file extension mappings updated"); } } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 6df390344ccc9..27faf41728241 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -1,4 +1,4 @@ -/** +/** * Declaration module describing the TypeScript Server protocol */ namespace ts.server.protocol { @@ -996,9 +996,9 @@ namespace ts.server.protocol { formatOptions?: FormatCodeSettings; /** - * The host's supported file extension mappings + * The host's additional supported file extensions */ - fileExtensionMap?: FileExtensionMapItem[]; + extraFileExtensions?: FileExtensionInfo[]; } /** From 5829ca82d0a06cc46738b1566bcbcda4d74b2a30 Mon Sep 17 00:00:00 2001 From: Jason Ramsay Date: Fri, 9 Dec 2016 14:44:08 -0800 Subject: [PATCH 8/9] use localUse local updatedFileNames - this way we'll know that set of names is definitely cleared --- src/server/project.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/server/project.ts b/src/server/project.ts index 005e26a1bc8f1..392008a9fd6dc 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -675,10 +675,12 @@ namespace ts.server { isInferred: this.projectKind === ProjectKind.Inferred, options: this.getCompilerOptions() }; + const updatedFileNames = this.updatedFileNames; + this.updatedFileNames = undefined; // 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 && !this.updatedFileNames) { + // if current structure version is the same - return info without any changes + if (this.projectStructureVersion == this.lastReportedVersion && !updatedFileNames) { return { info, projectErrors: this.projectErrors }; } // compute and return the difference @@ -687,7 +689,7 @@ namespace ts.server { const added: string[] = []; const removed: string[] = []; - const updated: string[] = getOwnKeys(this.updatedFileNames); + const updated: string[] = getOwnKeys(updatedFileNames); for (const id in currentFiles) { if (!hasProperty(lastReportedFileNames, id)) { added.push(id); @@ -700,7 +702,6 @@ namespace ts.server { } this.lastReportedFileNames = currentFiles; this.lastReportedVersion = this.projectStructureVersion; - this.updatedFileNames = undefined; return { info, changes: { added, removed, updated }, projectErrors: this.projectErrors }; } else { From c40508cf1cbe8e8a5f361a20ce6ab5220d6f106c Mon Sep 17 00:00:00 2001 From: Jason Ramsay Date: Fri, 9 Dec 2016 16:19:51 -0800 Subject: [PATCH 9/9] getSupportedExtensions optimization to reduce allocations --- src/compiler/core.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index f35bfd28f74fc..ceff135957940 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1925,15 +1925,17 @@ namespace ts { const allSupportedExtensions = supportedTypeScriptExtensions.concat(supportedJavascriptExtensions); export function getSupportedExtensions(options?: CompilerOptions, extraFileExtensions?: FileExtensionInfo[]): string[] { - let typeScriptHostExtensions: string[] = []; - let allHostExtensions: string[] = []; - if (extraFileExtensions) { - allHostExtensions = ts.map(extraFileExtensions, item => item.extension); - typeScriptHostExtensions = ts.map(ts.filter(extraFileExtensions, item => item.scriptKind === ScriptKind.TS), item => item.extension); - } - const allTypeScriptExtensions = concatenate(supportedTypeScriptExtensions, typeScriptHostExtensions); - const allExtensions = concatenate(allSupportedExtensions, allHostExtensions); - return options && options.allowJs ? allExtensions : allTypeScriptExtensions; + const needAllExtensions = options && options.allowJs; + if (!extraFileExtensions || extraFileExtensions.length === 0) { + return needAllExtensions ? allSupportedExtensions : supportedTypeScriptExtensions; + } + const extensions = (needAllExtensions ? allSupportedExtensions : supportedTypeScriptExtensions).slice(0); + for (const extInfo of extraFileExtensions) { + if (needAllExtensions || extInfo.scriptKind === ScriptKind.TS) { + extensions.push(extInfo.extension); + } + } + return extensions; } export function hasJavaScriptFileExtension(fileName: string) {