/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import type * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { Disposable } from '../utils/dispose'; import * as fileSchemes from '../utils/fileSchemes'; import { isTypeScriptDocument } from '../utils/languageModeIds'; import { ResourceMap } from '../utils/resourceMap'; function objsAreEqual(a: T, b: T): boolean { let keys = Object.keys(a); for (const key of keys) { if ((a as any)[key] !== (b as any)[key]) { return false; } } return true; } interface FileConfiguration { readonly formatOptions: Proto.FormatCodeSettings; readonly preferences: Proto.UserPreferences; } function areFileConfigurationsEqual(a: FileConfiguration, b: FileConfiguration): boolean { return ( objsAreEqual(a.formatOptions, b.formatOptions) && objsAreEqual(a.preferences, b.preferences) ); } export default class FileConfigurationManager extends Disposable { private readonly formatOptions = new ResourceMap>(); public constructor( private readonly client: ITypeScriptServiceClient ) { super(); vscode.workspace.onDidCloseTextDocument(textDocument => { // When a document gets closed delete the cached formatting options. // This is necessary since the tsserver now closed a project when its // last file in it closes which drops the stored formatting options // as well. this.formatOptions.delete(textDocument.uri); }, undefined, this._disposables); } public async ensureConfigurationForDocument( document: vscode.TextDocument, token: vscode.CancellationToken ): Promise { const formattingOptions = this.getFormattingOptions(document); if (formattingOptions) { return this.ensureConfigurationOptions(document, formattingOptions, token); } } private getFormattingOptions( document: vscode.TextDocument ): vscode.FormattingOptions | undefined { const editor = vscode.window.visibleTextEditors.find(editor => editor.document.fileName === document.fileName); return editor ? { tabSize: editor.options.tabSize, insertSpaces: editor.options.insertSpaces } as vscode.FormattingOptions : undefined; } public async ensureConfigurationOptions( document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken ): Promise { const file = this.client.toOpenedFilePath(document); if (!file) { return; } const currentOptions = this.getFileOptions(document, options); const cachedOptions = this.formatOptions.get(document.uri); if (cachedOptions) { const cachedOptionsValue = await cachedOptions; if (cachedOptionsValue && areFileConfigurationsEqual(cachedOptionsValue, currentOptions)) { return; } } let resolve: (x: FileConfiguration | undefined) => void; this.formatOptions.set(document.uri, new Promise(r => resolve = r)); const args: Proto.ConfigureRequestArguments = { file, ...currentOptions, }; try { const response = await this.client.execute('configure', args, token); resolve!(response.type === 'response' ? currentOptions : undefined); } finally { resolve!(undefined); } } public async setGlobalConfigurationFromDocument( document: vscode.TextDocument, token: vscode.CancellationToken, ): Promise { const formattingOptions = this.getFormattingOptions(document); if (!formattingOptions) { return; } const args: Proto.ConfigureRequestArguments = { file: undefined /*global*/, ...this.getFileOptions(document, formattingOptions), }; await this.client.execute('configure', args, token); } public reset() { this.formatOptions.clear(); } private getFileOptions( document: vscode.TextDocument, options: vscode.FormattingOptions ): FileConfiguration { return { formatOptions: this.getFormatOptions(document, options), preferences: this.getPreferences(document) }; } private getFormatOptions( document: vscode.TextDocument, options: vscode.FormattingOptions ): Proto.FormatCodeSettings { const config = vscode.workspace.getConfiguration( isTypeScriptDocument(document) ? 'typescript.format' : 'javascript.format', document.uri); return { tabSize: options.tabSize, indentSize: options.tabSize, convertTabsToSpaces: options.insertSpaces, // We can use \n here since the editor normalizes later on to its line endings. newLineCharacter: '\n', insertSpaceAfterCommaDelimiter: config.get('insertSpaceAfterCommaDelimiter'), insertSpaceAfterConstructor: config.get('insertSpaceAfterConstructor'), insertSpaceAfterSemicolonInForStatements: config.get('insertSpaceAfterSemicolonInForStatements'), insertSpaceBeforeAndAfterBinaryOperators: config.get('insertSpaceBeforeAndAfterBinaryOperators'), insertSpaceAfterKeywordsInControlFlowStatements: config.get('insertSpaceAfterKeywordsInControlFlowStatements'), insertSpaceAfterFunctionKeywordForAnonymousFunctions: config.get('insertSpaceAfterFunctionKeywordForAnonymousFunctions'), insertSpaceBeforeFunctionParenthesis: config.get('insertSpaceBeforeFunctionParenthesis'), insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: config.get('insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis'), insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: config.get('insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets'), insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: config.get('insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces'), insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: config.get('insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces'), insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: config.get('insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces'), insertSpaceAfterTypeAssertion: config.get('insertSpaceAfterTypeAssertion'), placeOpenBraceOnNewLineForFunctions: config.get('placeOpenBraceOnNewLineForFunctions'), placeOpenBraceOnNewLineForControlBlocks: config.get('placeOpenBraceOnNewLineForControlBlocks'), semicolons: config.get('semicolons'), }; } private getPreferences(document: vscode.TextDocument): Proto.UserPreferences { if (this.client.apiVersion.lt(API.v290)) { return {}; } const config = vscode.workspace.getConfiguration( isTypeScriptDocument(document) ? 'typescript' : 'javascript', document.uri); const preferencesConfig = vscode.workspace.getConfiguration( isTypeScriptDocument(document) ? 'typescript.preferences' : 'javascript.preferences', document.uri); const preferences: Proto.UserPreferences = { quotePreference: this.getQuoteStylePreference(preferencesConfig), importModuleSpecifierPreference: getImportModuleSpecifierPreference(preferencesConfig), importModuleSpecifierEnding: getImportModuleSpecifierEndingPreference(preferencesConfig), allowTextChangesInNewFiles: document.uri.scheme === fileSchemes.file, providePrefixAndSuffixTextForRename: preferencesConfig.get('renameShorthandProperties', true) === false ? false : preferencesConfig.get('useAliasesForRenames', true), allowRenameOfImportPath: true, includeAutomaticOptionalChainCompletions: config.get('suggest.includeAutomaticOptionalChainCompletions', true), }; return preferences; } private getQuoteStylePreference(config: vscode.WorkspaceConfiguration) { switch (config.get('quoteStyle')) { case 'single': return 'single'; case 'double': return 'double'; default: return this.client.apiVersion.gte(API.v333) ? 'auto' : undefined; } } } function getImportModuleSpecifierPreference(config: vscode.WorkspaceConfiguration) { switch (config.get('importModuleSpecifier')) { case 'relative': return 'relative'; case 'non-relative': return 'non-relative'; default: return undefined; } } function getImportModuleSpecifierEndingPreference(config: vscode.WorkspaceConfiguration) { switch (config.get('importModuleSpecifierEnding')) { case 'minimal': return 'minimal'; case 'index': return 'index'; case 'js': return 'js'; default: return 'auto'; } }