diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 7ccb51fbb16..42187c8c1d9 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -827,7 +827,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[] = [], mixedContentFileExtensions: string[] = []): 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); @@ -964,7 +964,7 @@ namespace ts { includeSpecs = ["**/*"]; } - const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors, mixedContentFileExtensions); + const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors, fileExtensionMap); if (result.fileNames.length === 0 && !hasProperty(json, "files") && resolutionStack.length === 0) { errors.push( @@ -1166,7 +1166,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[], mixedContentFileExtensions: string[]): 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 @@ -1200,7 +1200,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, mixedContentFileExtensions); + 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 5ffb4bb0393..16b42d5084e 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1942,8 +1942,16 @@ namespace ts { export const supportedJavascriptExtensions = [".js", ".jsx"]; const allSupportedExtensions = supportedTypeScriptExtensions.concat(supportedJavascriptExtensions); - export function getSupportedExtensions(options?: CompilerOptions, mixedContentFileExtensions?: string[]): string[] { - return options && options.allowJs ? concatenate(allSupportedExtensions, mixedContentFileExtensions) : 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) { @@ -1954,10 +1962,10 @@ namespace ts { return forEach(supportedTypeScriptExtensions, extension => fileExtensionIs(fileName, extension)); } - export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions, mixedContentFileExtensions?: string[]) { + export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions, fileExtensionMap?: FileExtensionMap) { if (!fileName) { return false; } - for (const extension of getSupportedExtensions(compilerOptions, mixedContentFileExtensions)) { + 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 abd78bced19..2d92561ad93 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, mixedContentFileExtensions?: string[]): Program { + export function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, fileExtensionMap?: FileExtensionMap): 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, mixedContentFileExtensions); + 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 2b8b6fe294e..654b3c2afe5 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3075,6 +3075,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 fd784987d30..4cfe6587ce5 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1464,7 +1464,7 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.configuredProjects[0], [file1.path]); // Specify .html extension as mixed content - const configureHostRequest = makeSessionRequest(CommandNames.Configure, { mixedContentFileExtensions: [".html"] }); + const configureHostRequest = makeSessionRequest(CommandNames.Configure, { fileExtensionMap: { mixedContent: [".html"] } }); 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 77fe36de27b..8fe4ea96bd8 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; - mixedContentFileExtensions?: string[]; + fileExtensionMap?: FileExtensionMap; } interface ConfigFileConversionResult { @@ -133,13 +133,13 @@ namespace ts.server { interface FilePropertyReader { getFileName(f: T): string; getScriptKind(f: T): ScriptKind; - hasMixedContent(f: T, mixedContentFileExtensions: string[]): boolean; + hasMixedContent(f: T, mixedContentExtensions: string[]): boolean; } const fileNamePropertyReader: FilePropertyReader = { getFileName: x => x, getScriptKind: _ => undefined, - hasMixedContent: (fileName, mixedContentFileExtensions) => forEach(mixedContentFileExtensions, extension => fileExtensionIs(fileName, extension)) + hasMixedContent: (fileName, mixedContentExtensions) => forEach(mixedContentExtensions, extension => fileExtensionIs(fileName, extension)) }; const externalFilePropertyReader: FilePropertyReader = { @@ -284,7 +284,7 @@ namespace ts.server { this.hostConfiguration = { formatCodeOptions: getDefaultFormatCodeSettings(this.host), hostInfo: "Unknown host", - mixedContentFileExtensions: [] + fileExtensionMap: {} }; this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); @@ -488,7 +488,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.mixedContentFileExtensions)) { + if (fileName && !ts.isSupportedSourceFileName(fileName, project.getCompilerOptions(), this.hostConfiguration.fileExtensionMap)) { return; } @@ -810,7 +810,7 @@ namespace ts.server { /*existingOptions*/ {}, configFilename, /*resolutionStack*/ [], - this.hostConfiguration.mixedContentFileExtensions); + this.hostConfiguration.fileExtensionMap); if (parsedCommandLine.errors.length) { errors = concatenate(errors, parsedCommandLine.errors); @@ -914,7 +914,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.mixedContentFileExtensions); + 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); @@ -960,7 +960,7 @@ namespace ts.server { rootFilesChanged = true; if (!scriptInfo) { const scriptKind = propertyReader.getScriptKind(f); - const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.mixedContentFileExtensions); + const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.fileExtensionMap.mixedContent); scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent); } } @@ -1144,9 +1144,9 @@ 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"); + if (args.fileExtensionMap) { + this.hostConfiguration.fileExtensionMap = args.fileExtensionMap; + this.logger.info("Host file extension mappings updated"); } } } diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index ecf8d1c7516..f8b4d28af84 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -5,7 +5,7 @@ namespace ts.server { export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost { private compilationSettings: ts.CompilerOptions; - private mixedContentFileExtensions: string[]; + private fileExtensionMap: FileExtensionMap; private readonly resolvedModuleNames = createFileMap>(); private readonly resolvedTypeReferenceDirectives = createFileMap>(); private readonly getCanonicalFileName: (fileName: string) => string; @@ -144,8 +144,8 @@ namespace ts.server { return this.compilationSettings; } - getMixedContentFileExtensions() { - return this.mixedContentFileExtensions; + getFileExtensionMap() { + return this.fileExtensionMap; } useCaseSensitiveFileNames() { @@ -237,8 +237,8 @@ namespace ts.server { this.compilationSettings = opt; } - setMixedContentFileExtensions(mixedContentFileExtensions: string[]) { - this.mixedContentFileExtensions = mixedContentFileExtensions || []; + 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 17b073e0aec..31c651e7cc3 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -254,7 +254,7 @@ namespace ts.server { this.lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken); this.lsHost.setCompilationSettings(this.compilerOptions); - this.lsHost.setMixedContentFileExtensions(this.projectService.hostConfiguration.mixedContentFileExtensions); + this.lsHost.setFileExtensionMap(this.projectService.hostConfiguration.fileExtensionMap); this.languageService = ts.createLanguageService(this.lsHost, this.documentRegistry); this.noSemanticFeaturesLanguageService = createNoSemanticFeaturesWrapper(this.languageService); diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 4dc75b27180..2c8942c4ccb 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -992,9 +992,9 @@ namespace ts.server.protocol { formatOptions?: FormatCodeSettings; /** - * List of host's supported mixed content file extensions + * The host's supported file extension mappings */ - mixedContentFileExtensions?: string[]; + fileExtensionMap?: FileExtensionMap; } /** diff --git a/src/services/completions.ts b/src/services/completions.ts index d150a0eb76a..8820f4fee54 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -271,14 +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() : []; + const fileExtensionMap = host.getFileExtensionMap ? host.getFileExtensionMap() : {}; if (compilerOptions.rootDirs) { entries = getCompletionEntriesForDirectoryFragmentWithRootDirs( - compilerOptions.rootDirs, literalValue, scriptDirectory, getSupportedExtensions(compilerOptions, mixedContentFileExtensions), /*includeExtensions*/false, span, scriptPath); + compilerOptions.rootDirs, literalValue, scriptDirectory, getSupportedExtensions(compilerOptions, fileExtensionMap), /*includeExtensions*/false, span, scriptPath); } else { entries = getCompletionEntriesForDirectoryFragment( - literalValue, scriptDirectory, getSupportedExtensions(compilerOptions, mixedContentFileExtensions), /*includeExtensions*/false, span, scriptPath); + literalValue, scriptDirectory, getSupportedExtensions(compilerOptions, fileExtensionMap), /*includeExtensions*/false, span, scriptPath); } } else { @@ -412,8 +412,8 @@ namespace ts.Completions { let result: CompletionEntry[]; if (baseUrl) { - const mixedContentFileExtensions = host.getMixedContentFileExtensions ? host.getMixedContentFileExtensions() : []; - const fileExtensions = getSupportedExtensions(compilerOptions, mixedContentFileExtensions); + 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); @@ -590,8 +590,8 @@ namespace ts.Completions { if (kind === "path") { // Give completions for a relative path const span: TextSpan = getDirectoryFragmentTextSpan(toComplete, range.pos + prefix.length); - const mixedContentFileExtensions = host.getMixedContentFileExtensions ? host.getMixedContentFileExtensions() : []; - completionInfo.entries = getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getSupportedExtensions(compilerOptions, mixedContentFileExtensions), /*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 5fdda1ec630..66b0b10a090 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -757,7 +757,7 @@ namespace ts { class HostCache { private fileNameToEntry: FileMap; private _compilationSettings: CompilerOptions; - private _mixedContentFileExtensions: string[]; + private _fileExtensionMap: FileExtensionMap; private currentDirectory: string; constructor(private host: LanguageServiceHost, private getCanonicalFileName: (fileName: string) => string) { @@ -774,15 +774,15 @@ namespace ts { // store the compilation settings this._compilationSettings = host.getCompilationSettings() || getDefaultCompilerOptions(); - this._mixedContentFileExtensions = host.getMixedContentFileExtensions ? host.getMixedContentFileExtensions() : []; + this._fileExtensionMap = host.getFileExtensionMap ? host.getFileExtensionMap() : {}; } public compilationSettings() { return this._compilationSettings; } - public mixedContentFileExtensions() { - return this._mixedContentFileExtensions; + public fileExtensionMap() { + return this._fileExtensionMap; } private createEntry(fileName: string, path: Path) { @@ -1107,7 +1107,7 @@ namespace ts { } const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings); - const newProgram = createProgram(hostCache.getRootFileNames(), newSettings, compilerHost, program, hostCache.mixedContentFileExtensions()); + 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 72611cc1a03..6e688e409af 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -126,7 +126,7 @@ namespace ts { // export interface LanguageServiceHost { getCompilationSettings(): CompilerOptions; - getMixedContentFileExtensions?(): string[]; + getFileExtensionMap?(): FileExtensionMap; getNewLine?(): string; getProjectVersion?(): string; getScriptFileNames(): string[];