/*--------------------------------------------------------------------------------------------- * 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 * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { isTypeScriptDocument } from '../utils/languageModeIds'; import { ResourceMap } from '../utils/resourceMap'; function objsAreEqual(a: T, b: T): boolean { let keys = Object.keys(a); for (let i = 0; i < keys.length; i++) { let key = keys[i]; 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 { private onDidCloseTextDocumentSub: vscode.Disposable | undefined; private formatOptions = new ResourceMap(); public constructor( private readonly client: ITypeScriptServiceClient ) { this.onDidCloseTextDocumentSub = 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); }); } public dispose() { if (this.onDidCloseTextDocumentSub) { this.onDidCloseTextDocumentSub.dispose(); this.onDidCloseTextDocumentSub = undefined; } } 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.toPath(document.uri); if (!file) { return; } const cachedOptions = this.formatOptions.get(document.uri); const currentOptions = this.getFileOptions(document, options); if (cachedOptions && areFileConfigurationsEqual(cachedOptions, currentOptions)) { return; } this.formatOptions.set(document.uri, currentOptions); const args: Proto.ConfigureRequestArguments = { file, ...currentOptions, }; await this.client.execute('configure', args, token); } 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'), }; } private getPreferences(document: vscode.TextDocument): Proto.UserPreferences { if (!this.client.apiVersion.gte(API.v290)) { return {}; } const preferences = vscode.workspace.getConfiguration( isTypeScriptDocument(document) ? 'typescript.preferences' : 'javascript.preferences', document.uri); return { quotePreference: getQuoteStylePreference(preferences), importModuleSpecifierPreference: getImportModuleSpecifierPreference(preferences), allowTextChangesInNewFiles: document.uri.scheme === 'file' }; } } function getQuoteStylePreference(config: vscode.WorkspaceConfiguration) { switch (config.get('quoteStyle')) { case 'single': return 'single'; case 'double': return 'double'; default: return undefined; } } function getImportModuleSpecifierPreference(config: vscode.WorkspaceConfiguration) { switch (config.get('importModuleSpecifier')) { case 'relative': return 'relative'; case 'non-relative': return 'non-relative'; default: return undefined; } }