/*--------------------------------------------------------------------------------------------- * 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 { equals } from '../utils/objects'; import { ResourceMap } from '../utils/resourceMap'; interface FileConfiguration { readonly formatOptions: Proto.FormatCodeSettings; readonly preferences: Proto.UserPreferences; } function areFileConfigurationsEqual(a: FileConfiguration, b: FileConfiguration): boolean { return equals(a, b); } export default class FileConfigurationManager extends Disposable { private readonly formatOptions: ResourceMap>; public constructor( private readonly client: ITypeScriptServiceClient, onCaseInsenitiveFileSystem: boolean ) { super(); this.formatOptions = new ResourceMap(undefined, { onCaseInsenitiveFileSystem }); 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'), insertSpaceAfterOpeningAndBeforeClosingEmptyBraces: config.get('insertSpaceAfterOpeningAndBeforeClosingEmptyBraces'), 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 & { displayPartsForJSDoc: true } = { 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), provideRefactorNotApplicableReason: true, generateReturnInDocTemplate: config.get('suggest.jsdoc.generateReturns', true), // @ts-expect-error until 4.3 protocol update includeCompletionsForImportStatements: config.get('suggest.includeCompletionsForImportStatements', true), includeCompletionsWithSnippetText: config.get('suggest.includeCompletionsWithSnippetText', true), displayPartsForJSDoc: 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 'project-relative': return 'project-relative'; 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'; } }