Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 57 additions & 24 deletions packages/typescript/lib/configs/getFormatCodeSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,68 @@ export async function getFormatCodeSettings(
options: FormattingOptions | undefined,
): Promise<ts.FormatCodeSettings> {
const config = await ctx.env.getConfiguration<any>?.(getConfigTitle(document) + '.format') ?? {};
// The built-in TS extension reads the unified `js/ts.format.*` settings
// first and only falls back to the deprecated language-specific
// `typescript.format.*` / `javascript.format.*` settings when the unified
// value is not configured. Mirror that behavior here.
const unifiedConfig = await ctx.env.getConfiguration<any>?.('js/ts.format') ?? {};

function formatOption(key: string, defaultValue: any): any {
const unified = unifiedConfig[key];
// `getConfiguration()` returns the registered default even for unset
// settings, so we can only tell a user-configured unified value from an
// unset one by comparing it against its default.
return unified !== undefined && unified !== defaultValue
? unified
: config[key] ?? defaultValue;
}

return {
convertTabsToSpaces: options?.insertSpaces,
tabSize: options?.tabSize,
indentSize: options?.tabSize,
indentStyle: 2 satisfies ts.IndentStyle.Smart,
newLineCharacter: '\n',
insertSpaceAfterCommaDelimiter: config.insertSpaceAfterCommaDelimiter ?? true,
insertSpaceAfterConstructor: config.insertSpaceAfterConstructor ?? false,
insertSpaceAfterSemicolonInForStatements: config.insertSpaceAfterSemicolonInForStatements ?? true,
insertSpaceBeforeAndAfterBinaryOperators: config.insertSpaceBeforeAndAfterBinaryOperators ?? true,
insertSpaceAfterKeywordsInControlFlowStatements: config.insertSpaceAfterKeywordsInControlFlowStatements ?? true,
insertSpaceAfterFunctionKeywordForAnonymousFunctions: config.insertSpaceAfterFunctionKeywordForAnonymousFunctions
?? true,
insertSpaceBeforeFunctionParenthesis: config.insertSpaceBeforeFunctionParenthesis ?? false,
insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis:
config.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis ?? false,
insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets:
config.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets ?? false,
insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: config.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces
?? true,
insertSpaceAfterOpeningAndBeforeClosingEmptyBraces: config.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces
?? true,
insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces:
config.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces ?? false,
insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces:
config.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces ?? false,
insertSpaceAfterTypeAssertion: config.insertSpaceAfterTypeAssertion ?? false,
placeOpenBraceOnNewLineForFunctions: config.placeOpenBraceOnNewLineForFunctions ?? false,
placeOpenBraceOnNewLineForControlBlocks: config.placeOpenBraceOnNewLineForControlBlocks ?? false,
semicolons: config.semicolons ?? 'ignore',
insertSpaceAfterCommaDelimiter: formatOption('insertSpaceAfterCommaDelimiter', true),
insertSpaceAfterConstructor: formatOption('insertSpaceAfterConstructor', false),
insertSpaceAfterSemicolonInForStatements: formatOption('insertSpaceAfterSemicolonInForStatements', true),
insertSpaceBeforeAndAfterBinaryOperators: formatOption('insertSpaceBeforeAndAfterBinaryOperators', true),
insertSpaceAfterKeywordsInControlFlowStatements: formatOption(
'insertSpaceAfterKeywordsInControlFlowStatements',
true,
),
insertSpaceAfterFunctionKeywordForAnonymousFunctions: formatOption(
'insertSpaceAfterFunctionKeywordForAnonymousFunctions',
true,
),
insertSpaceBeforeFunctionParenthesis: formatOption('insertSpaceBeforeFunctionParenthesis', false),
insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: formatOption(
'insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis',
false,
),
insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: formatOption(
'insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets',
false,
),
insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: formatOption(
'insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces',
true,
),
insertSpaceAfterOpeningAndBeforeClosingEmptyBraces: formatOption(
'insertSpaceAfterOpeningAndBeforeClosingEmptyBraces',
true,
),
insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: formatOption(
'insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces',
false,
),
insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: formatOption(
'insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces',
false,
),
insertSpaceAfterTypeAssertion: formatOption('insertSpaceAfterTypeAssertion', false),
placeOpenBraceOnNewLineForFunctions: formatOption('placeOpenBraceOnNewLineForFunctions', false),
placeOpenBraceOnNewLineForControlBlocks: formatOption('placeOpenBraceOnNewLineForControlBlocks', false),
semicolons: formatOption('semicolons', 'ignore'),
};
}
93 changes: 93 additions & 0 deletions packages/typescript/tests/getFormatCodeSettings.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { LanguageServiceContext } from '@volar/language-service';
import { describe, expect, test } from 'vitest';
import type { TextDocument } from 'vscode-languageserver-textdocument';
import { getFormatCodeSettings } from '../lib/configs/getFormatCodeSettings';

// Mirrors how @volar/kit's createServiceEnvironment resolves flat settings.
function getConfiguration(settings: Record<string, any>) {
return (section: string) => {
if (section in settings) {
return settings[section];
}
let result: any;
for (const settingKey in settings) {
if (settingKey.startsWith(`${section}.`)) {
const value = settings[settingKey];
const props = settingKey.substring(section.length + 1).split('.');
result ??= {};
let current = result;
while (props.length > 1) {
const prop = props.shift()!;
if (typeof current[prop] !== 'object') {
current[prop] = {};
}
current = current[prop];
}
current[props.shift()!] = value;
}
}
return result;
};
}

function createContext(settings: Record<string, any>): LanguageServiceContext {
return {
env: {
getConfiguration: getConfiguration(settings),
},
} as any;
}

const document = { languageId: 'typescript' } as TextDocument;

describe('getFormatCodeSettings', () => {
test('uses unified js/ts.format.* settings when configured', async () => {
const ctx = createContext({
'js/ts.format.semicolons': 'insert',
'js/ts.format.placeOpenBraceOnNewLineForControlBlocks': true,
'js/ts.format.placeOpenBraceOnNewLineForFunctions': true,
});
const settings = await getFormatCodeSettings(ctx, document, undefined);
expect(settings.semicolons).toBe('insert');
expect(settings.placeOpenBraceOnNewLineForControlBlocks).toBe(true);
expect(settings.placeOpenBraceOnNewLineForFunctions).toBe(true);
});

test('falls back to language-specific format settings', async () => {
const ctx = createContext({
'typescript.format.semicolons': 'insert',
'typescript.format.placeOpenBraceOnNewLineForControlBlocks': true,
'typescript.format.placeOpenBraceOnNewLineForFunctions': true,
});
const settings = await getFormatCodeSettings(ctx, document, undefined);
expect(settings.semicolons).toBe('insert');
expect(settings.placeOpenBraceOnNewLineForControlBlocks).toBe(true);
expect(settings.placeOpenBraceOnNewLineForFunctions).toBe(true);
});

test('unified settings take precedence over language-specific settings', async () => {
const ctx = createContext({
'js/ts.format.semicolons': 'remove',
'typescript.format.semicolons': 'insert',
});
const settings = await getFormatCodeSettings(ctx, document, undefined);
expect(settings.semicolons).toBe('remove');
});

test('uses defaults when nothing is configured', async () => {
const ctx = createContext({});
const settings = await getFormatCodeSettings(ctx, document, undefined);
expect(settings.semicolons).toBe('ignore');
expect(settings.placeOpenBraceOnNewLineForControlBlocks).toBe(false);
expect(settings.placeOpenBraceOnNewLineForFunctions).toBe(false);
expect(settings.insertSpaceAfterCommaDelimiter).toBe(true);
});

test('applies formatting options', async () => {
const ctx = createContext({});
const settings = await getFormatCodeSettings(ctx, document, { insertSpaces: true, tabSize: 2 });
expect(settings.convertTabsToSpaces).toBe(true);
expect(settings.tabSize).toBe(2);
expect(settings.indentSize).toBe(2);
});
});