diff --git a/packages/css/index.ts b/packages/css/index.ts index 1b3323af..752e1760 100644 --- a/packages/css/index.ts +++ b/packages/css/index.ts @@ -1,6 +1,6 @@ import type { CodeAction, Diagnostic, LocationLink, ServicePluginInstance, ServicePlugin } from '@volar/language-service'; import * as css from 'vscode-css-languageservice'; -import type { TextDocument } from 'vscode-languageserver-textdocument'; +import { TextDocument } from 'vscode-languageserver-textdocument'; import { URI, Utils } from 'vscode-uri'; export interface Provide { @@ -180,18 +180,103 @@ export function create(): ServicePlugin { }); }, - async provideDocumentFormattingEdits(document, formatRange, options) { + async provideDocumentFormattingEdits(document, formatRange, options, codeOptions) { return worker(document, async (_stylesheet, cssLs) => { - const options_2 = await context.env.getConfiguration?.(document.languageId + '.format'); - if (options_2?.enable === false) { + const formatSettings = await context.env.getConfiguration?.(document.languageId + '.format'); + if (formatSettings?.enable === false) { return; } - return cssLs.format(document, formatRange, { - ...options_2, + const formatOptions: css.CSSFormatConfiguration = { ...options, - }); + ...formatSettings, + }; + + let formatDocument = document; + let prefixes = []; + let suffixes = []; + + if (codeOptions?.initialIndentLevel) { + for (let i = 0; i < codeOptions.initialIndentLevel; i++) { + if (i === codeOptions.initialIndentLevel - 1) { + prefixes.push('_', '{'); + suffixes.unshift('}'); + } + else { + prefixes.push('_', '{\n'); + suffixes.unshift('\n}'); + } + } + formatDocument = TextDocument.create(document.uri, document.languageId, document.version, prefixes.join('') + document.getText() + suffixes.join('')); + formatRange = { + start: formatDocument.positionAt(0), + end: formatDocument.positionAt(formatDocument.getText().length), + }; + } + + let edits = cssLs.format(formatDocument, formatRange, formatOptions); + + if (codeOptions) { + let newText = TextDocument.applyEdits(formatDocument, edits); + for (const prefix of prefixes) { + newText = newText.trimStart().slice(prefix.trim().length); + } + for (const suffix of suffixes.reverse()) { + newText = newText.trimEnd().slice(0, -suffix.trim().length); + } + if (!codeOptions.initialIndentLevel && codeOptions.level > 0) { + newText = ensureNewLines(newText); + } + edits = [{ + range: { + start: document.positionAt(0), + end: document.positionAt(document.getText().length), + }, + newText, + }]; + } + + return edits; + + function ensureNewLines(newText: string) { + const verifyDocument = TextDocument.create(document.uri, document.languageId, document.version, '_ {' + newText + '}'); + const verifyEdits = cssLs.format(verifyDocument, undefined, formatOptions); + let verifyText = TextDocument.applyEdits(verifyDocument, verifyEdits); + verifyText = verifyText.trimStart().slice('_'.length); + verifyText = verifyText.trim().slice('{'.length, -'}'.length); + if (startWithNewLine(verifyText) !== startWithNewLine(newText)) { + if (startWithNewLine(verifyText)) { + newText = '\n' + newText; + } + else if (newText.startsWith('\n')) { + newText = newText.slice(1); + } + else if (newText.startsWith('\r\n')) { + newText = newText.slice(2); + } + } + if (endWithNewLine(verifyText) !== endWithNewLine(newText)) { + if (endWithNewLine(verifyText)) { + newText = newText + '\n'; + } + else if (newText.endsWith('\n')) { + newText = newText.slice(0, -1); + } + else if (newText.endsWith('\r\n')) { + newText = newText.slice(0, -2); + } + } + return newText; + } + + function startWithNewLine(text: string) { + return text.startsWith('\n') || text.startsWith('\r\n'); + } + + function endWithNewLine(text: string) { + return text.endsWith('\n') || text.endsWith('\r\n'); + } }); }, }; diff --git a/packages/css/package.json b/packages/css/package.json index 9a738cd3..b313f8bc 100644 --- a/packages/css/package.json +++ b/packages/css/package.json @@ -25,11 +25,11 @@ }, "dependencies": { "vscode-css-languageservice": "^6.2.10", + "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "devDependencies": { - "@types/node": "latest", - "vscode-languageserver-textdocument": "^1.0.11" + "@types/node": "latest" }, "peerDependencies": { "@volar/language-service": "~2.0.4" diff --git a/packages/html/index.ts b/packages/html/index.ts index a1976e61..a0339cdf 100644 --- a/packages/html/index.ts +++ b/packages/html/index.ts @@ -1,6 +1,6 @@ import type { ServicePluginInstance, ServicePlugin } from '@volar/language-service'; import * as html from 'vscode-html-languageservice'; -import type { TextDocument } from 'vscode-languageserver-textdocument'; +import { TextDocument } from 'vscode-languageserver-textdocument'; import { URI, Utils } from 'vscode-uri'; const parserLs = html.getLanguageService(); @@ -143,7 +143,7 @@ export function create({ }); }, - async provideDocumentFormattingEdits(document, formatRange, options) { + async provideDocumentFormattingEdits(document, formatRange, options, codeOptions) { return worker(document, async () => { const formatSettings = await context.env.getConfiguration?.('html.format') ?? {}; @@ -174,62 +174,95 @@ export function create({ }; } - return htmlLs.format(document, formatRange, { + const formatOptions: html.HTMLFormatConfiguration = { ...options, ...formatSettings, - }); - }); - }, + endWithNewline: options.insertFinalNewline ? true : options.trimFinalNewlines ? false : undefined, + }; + + let formatDocument = document; + let prefixes = []; + let suffixes = []; + + if (codeOptions?.initialIndentLevel) { + for (let i = 0; i < codeOptions.initialIndentLevel; i++) { + if (i === codeOptions.initialIndentLevel - 1) { + prefixes.push(''); + } + else { + prefixes.push(''); + } + } + formatDocument = TextDocument.create(document.uri, document.languageId, document.version, prefixes.join('') + document.getText() + suffixes.join('')); + formatRange = { + start: formatDocument.positionAt(0), + end: formatDocument.positionAt(formatDocument.getText().length), + }; + } - provideFormattingIndentSensitiveLines(document) { - return worker(document, (htmlDocument) => { - const lines: number[] = []; - /** - * comments - */ - const scanner = htmlLs.createScanner(document.getText()); - let token = scanner.scan(); - let startCommentTagLine: number | undefined; - while (token !== html.TokenType.EOS) { - if (token === html.TokenType.StartCommentTag) { - startCommentTagLine = document.positionAt(scanner.getTokenOffset()).line; + let edits = htmlLs.format(formatDocument, formatRange, formatOptions); + + if (codeOptions) { + let newText = TextDocument.applyEdits(formatDocument, edits); + for (const prefix of prefixes) { + newText = newText.trimStart().slice(prefix.trim().length); } - else if (token === html.TokenType.EndCommentTag) { - const line = document.positionAt(scanner.getTokenOffset()).line; - for (let i = startCommentTagLine! + 1; i <= line; i++) { - lines.push(i); - } - startCommentTagLine = undefined; + for (const suffix of suffixes.reverse()) { + newText = newText.trimEnd().slice(0, -suffix.trim().length); } - else if (token === html.TokenType.AttributeValue) { - const startLine = document.positionAt(scanner.getTokenOffset()).line; - for (let i = 1; i < scanner.getTokenText().split('\n').length; i++) { - lines.push(startLine + i); - } + if (!codeOptions.initialIndentLevel && codeOptions.level > 0) { + newText = ensureNewLines(newText); } - token = scanner.scan(); + edits = [{ + range: { + start: document.positionAt(0), + end: document.positionAt(document.getText().length), + }, + newText, + }]; } - /** - * tags - */ - // https://github.com/beautify-web/js-beautify/blob/686f8c1b265990908ece86ce39291733c75c997c/js/src/html/options.js#L81 - const indentSensitiveTags = new Set(['pre', 'textarea']); - htmlDocument.roots.forEach(function visit(node) { - if ( - node.tag !== undefined - && node.startTagEnd !== undefined - && node.endTagStart !== undefined - && indentSensitiveTags.has(node.tag) - ) { - for (let i = document.positionAt(node.startTagEnd).line + 1; i <= document.positionAt(node.endTagStart).line; i++) { - lines.push(i); + + return edits; + + function ensureNewLines(newText: string) { + const verifyDocument = TextDocument.create(document.uri, document.languageId, document.version, ''); + const verifyEdits = htmlLs.format(verifyDocument, undefined, formatOptions); + let verifyText = TextDocument.applyEdits(verifyDocument, verifyEdits); + verifyText = verifyText.trim().slice(''.length); + if (startWithNewLine(verifyText) !== startWithNewLine(newText)) { + if (startWithNewLine(verifyText)) { + newText = '\n' + newText; + } + else if (newText.startsWith('\n')) { + newText = newText.slice(1); + } + else if (newText.startsWith('\r\n')) { + newText = newText.slice(2); } } - else { - node.children.forEach(visit); + if (endWithNewLine(verifyText) !== endWithNewLine(newText)) { + if (endWithNewLine(verifyText)) { + newText = newText + '\n'; + } + else if (newText.endsWith('\n')) { + newText = newText.slice(0, -1); + } + else if (newText.endsWith('\r\n')) { + newText = newText.slice(0, -2); + } } - }); - return lines; + return newText; + } + + function startWithNewLine(text: string) { + return text.startsWith('\n') || text.startsWith('\r\n'); + } + + function endWithNewLine(text: string) { + return text.endsWith('\n') || text.endsWith('\r\n'); + } }); }, diff --git a/packages/html/package.json b/packages/html/package.json index 46ceb13e..65657925 100644 --- a/packages/html/package.json +++ b/packages/html/package.json @@ -25,11 +25,11 @@ }, "dependencies": { "vscode-html-languageservice": "^5.1.0", + "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "devDependencies": { - "@types/node": "latest", - "vscode-languageserver-textdocument": "^1.0.11" + "@types/node": "latest" }, "peerDependencies": { "@volar/language-service": "~2.0.4" diff --git a/packages/typescript/index.ts b/packages/typescript/index.ts index dd95ef63..a1fdc865 100644 --- a/packages/typescript/index.ts +++ b/packages/typescript/index.ts @@ -3,7 +3,6 @@ import * as semver from 'semver'; import type * as ts from 'typescript'; import type { TextDocument } from 'vscode-languageserver-textdocument'; import { getConfigTitle, isJsonDocument, isTsDocument } from './lib/shared'; -import { URI } from 'vscode-uri'; import { getDocumentRegistry } from '@volar/typescript'; import * as tsFaster from 'typescript-auto-import-cache'; @@ -68,7 +67,7 @@ export function create(ts: typeof import('typescript')): ServicePlugin { getScriptVersion: fileName => fileName === syntacticHostCtx.fileName ? syntacticHostCtx.fileVersion.toString() : '', getScriptSnapshot: fileName => fileName === syntacticHostCtx.fileName ? syntacticHostCtx.snapshot : undefined, getCompilationSettings: () => ({}), - getCurrentDirectory: () => '/', + getCurrentDirectory: () => '', getDefaultLibFileName: () => '', readFile: () => undefined, fileExists: fileName => fileName === syntacticHostCtx.fileName, @@ -148,7 +147,7 @@ export function create(ts: typeof import('typescript')): ServicePlugin { return findDocumentSymbols(document.uri); }, - async provideDocumentFormattingEdits(document, range, options_2) { + async provideDocumentFormattingEdits(document, range, options, codeOptions) { if (!isTsDocument(document)) return; @@ -160,10 +159,10 @@ export function create(ts: typeof import('typescript')): ServicePlugin { prepareSyntacticService(document); - return await doFormatting.onRange(document, range, options_2); + return await doFormatting.onRange(document, range, options, codeOptions); }, - async provideOnTypeFormattingEdits(document, position, key, options_2) { + async provideOnTypeFormattingEdits(document, position, key, options, codeOptions) { if (!isTsDocument(document)) return; @@ -175,43 +174,13 @@ export function create(ts: typeof import('typescript')): ServicePlugin { prepareSyntacticService(document); - return doFormatting.onType(document, options_2, position, key); - }, - - provideFormattingIndentSensitiveLines(document) { - - if (!isTsDocument(document)) - return; - - const ctx = prepareSyntacticService(document); - const sourceFile = ts.createSourceFile(ctx.fileName, document.getText(), ts.ScriptTarget.ESNext); - - if (sourceFile) { - - const lines: number[] = []; - - sourceFile.forEachChild(function walk(node) { - if ( - node.kind === ts.SyntaxKind.FirstTemplateToken - || node.kind === ts.SyntaxKind.LastTemplateToken - || node.kind === ts.SyntaxKind.TemplateHead - ) { - const startLine = document.positionAt(node.getStart(sourceFile)).line; - const endLine = document.positionAt(node.getEnd()).line; - for (let i = startLine + 1; i <= endLine; i++) { - lines.push(i); - } - } - node.forEachChild(walk); - }); - - return lines; - } + return doFormatting.onType(document, options, codeOptions, position, key); }, }; const syntacticHostCtx = { projectVersion: -1, document: undefined as TextDocument | undefined, + documentVersion: undefined as number | undefined, fileName: '', fileVersion: 0, snapshot: ts.ScriptSnapshot.fromString(''), @@ -681,10 +650,14 @@ export function create(ts: typeof import('typescript')): ServicePlugin { } function prepareSyntacticService(document: TextDocument) { - if (syntacticHostCtx.document !== document || syntacticHostCtx.fileVersion !== document.version) { + if (syntacticHostCtx.document !== document || syntacticHostCtx.documentVersion !== document.version) { syntacticHostCtx.document = document; - syntacticHostCtx.fileName = URI.parse(document.uri).fsPath.replace(/\\/g, '/'); - syntacticHostCtx.fileVersion = document.version; + syntacticHostCtx.fileName = '/tmp.' + + document.languageId === 'javascript' ? 'js' : + document.languageId === 'typescriptreact' ? 'tsx' : + document.languageId === 'javascriptreact' ? 'jsx' : + 'ts'; + syntacticHostCtx.fileVersion++; syntacticHostCtx.snapshot = ts.ScriptSnapshot.fromString(document.getText()); syntacticHostCtx.projectVersion++; } diff --git a/packages/typescript/lib/configs/getFormatCodeSettings.ts b/packages/typescript/lib/configs/getFormatCodeSettings.ts index cc13480a..eb26d9f3 100644 --- a/packages/typescript/lib/configs/getFormatCodeSettings.ts +++ b/packages/typescript/lib/configs/getFormatCodeSettings.ts @@ -9,16 +9,12 @@ export async function getFormatCodeSettings( document: TextDocument, options?: FormattingOptions, ): Promise { - - let config = await ctx.env.getConfiguration?.(getConfigTitle(document) + '.format'); - - config = config ?? {}; - + const config = await ctx.env.getConfiguration?.(getConfigTitle(document) + '.format') ?? {}; return { convertTabsToSpaces: options?.insertSpaces, tabSize: options?.tabSize, indentSize: options?.tabSize, - indentStyle: 2 /** ts.IndentStyle.Smart */, + indentStyle: 2 satisfies ts.IndentStyle.Smart, newLineCharacter: '\n', insertSpaceAfterCommaDelimiter: config.insertSpaceAfterCommaDelimiter ?? true, insertSpaceAfterConstructor: config.insertSpaceAfterConstructor ?? false, diff --git a/packages/typescript/lib/features/formatting.ts b/packages/typescript/lib/features/formatting.ts index a98767cf..eb4f0037 100644 --- a/packages/typescript/lib/features/formatting.ts +++ b/packages/typescript/lib/features/formatting.ts @@ -6,16 +6,22 @@ import type { SharedContext } from '../types'; export function register(ctx: SharedContext) { return { - onRange: async (document: TextDocument, range: vscode.Range | undefined, options: vscode.FormattingOptions): Promise => { + onRange: async (document: TextDocument, range: vscode.Range | undefined, options: vscode.FormattingOptions, codeOptions: vscode.EmbeddedCodeFormattingOptions | undefined): Promise => { const fileName = ctx.uriToFileName(document.uri); const tsOptions = await getFormatCodeSettings(ctx, document, options); - if (typeof (tsOptions.indentSize) === "boolean" || typeof (tsOptions.indentSize) === "string") { - tsOptions.indentSize = undefined; + + if (codeOptions) { + tsOptions.baseIndentSize = codeOptions.initialIndentLevel * options.tabSize; } const scriptEdits = range - ? safeCall(() => ctx.languageService.getFormattingEditsForRange(fileName, document.offsetAt(range.start), document.offsetAt(range.end), tsOptions)) + ? safeCall(() => ctx.languageService.getFormattingEditsForRange( + fileName, + document.offsetAt(range.start), + document.offsetAt(range.end), + tsOptions, + )) : safeCall(() => ctx.languageService.getFormattingEditsForDocument(fileName, tsOptions)); if (!scriptEdits) return []; @@ -33,10 +39,15 @@ export function register(ctx: SharedContext) { return result; }, - onType: async (document: TextDocument, options: vscode.FormattingOptions, position: vscode.Position, key: string): Promise => { + onType: async (document: TextDocument, options: vscode.FormattingOptions, codeOptions: vscode.EmbeddedCodeFormattingOptions | undefined, position: vscode.Position, key: string): Promise => { const fileName = ctx.uriToFileName(document.uri); const tsOptions = await getFormatCodeSettings(ctx, document, options); + + if (codeOptions) { + tsOptions.baseIndentSize = codeOptions.initialIndentLevel * options.tabSize; + } + const scriptEdits = safeCall(() => ctx.languageService.getFormattingEditsAfterKeystroke(fileName, document.offsetAt(position), key, tsOptions)); if (!scriptEdits) return []; diff --git a/packages/typescript/package.json b/packages/typescript/package.json index 699e81aa..34793e59 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -33,8 +33,7 @@ "semver": "^7.5.4", "typescript-auto-import-cache": "^0.3.1", "vscode-languageserver-textdocument": "^1.0.11", - "vscode-nls": "^5.2.0", - "vscode-uri": "^3.0.8" + "vscode-nls": "^5.2.0" }, "peerDependencies": { "@volar/language-service": "~2.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18f61b6e..cb12cecd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: vscode-css-languageservice: specifier: ^6.2.10 version: 6.2.12 + vscode-languageserver-textdocument: + specifier: ^1.0.11 + version: 1.0.11 vscode-uri: specifier: ^3.0.8 version: 3.0.8 @@ -42,9 +45,6 @@ importers: '@types/node': specifier: latest version: 20.11.17 - vscode-languageserver-textdocument: - specifier: ^1.0.11 - version: 1.0.11 packages/emmet: dependencies: @@ -66,6 +66,9 @@ importers: vscode-html-languageservice: specifier: ^5.1.0 version: 5.1.2 + vscode-languageserver-textdocument: + specifier: ^1.0.11 + version: 1.0.11 vscode-uri: specifier: ^3.0.8 version: 3.0.8 @@ -73,9 +76,6 @@ importers: '@types/node': specifier: latest version: 20.11.17 - vscode-languageserver-textdocument: - specifier: ^1.0.11 - version: 1.0.11 packages/json: dependencies: @@ -238,9 +238,6 @@ importers: vscode-nls: specifier: ^5.2.0 version: 5.2.0 - vscode-uri: - specifier: ^3.0.8 - version: 3.0.8 devDependencies: '@types/path-browserify': specifier: latest