From da7f824a2a4a6a286bda1689daa51095e338a8c3 Mon Sep 17 00:00:00 2001 From: Jason Ramsay Date: Thu, 10 Nov 2016 11:42:42 -0800 Subject: [PATCH] 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[];