From 8e79b61cf9861dcc6bf30ee3d9487e7591fe2309 Mon Sep 17 00:00:00 2001 From: TJ Date: Thu, 11 Sep 2025 16:15:53 -0400 Subject: [PATCH 01/10] Working integration of expanded line formatting for exp:channel tags --- package.json | 6 + src/extension.ts | 8 +- src/providers/EEFormatterProvider.ts | 25 +++ src/services/EEFormatterService.ts | 235 +++++++++++++++++++++++++++ 4 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 src/providers/EEFormatterProvider.ts create mode 100644 src/services/EEFormatterService.ts diff --git a/package.json b/package.json index aafddc9..7a67a8e 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,12 @@ "type": "boolean", "default": true, "description": "Determines if auto-completion and IntelliSense suggestions will show" + }, + "ee.format.wrapAttributes": { + "type": "string", + "enum": ["auto", "force", "force-expand-multiline", "preserve"], + "default": "auto", + "description": "Controls how EE tag attributes are wrapped. 'auto' wraps when tag exceeds 80 characters, 'force' always wraps, 'force-expand-multiline' wraps when multiple attributes exist, 'preserve' keeps original formatting." } } }, diff --git a/src/extension.ts b/src/extension.ts index 27486ca..89241a7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,13 +8,14 @@ import CompletionProvider from './providers/CompletionProvider'; import ModifierCompletionProvider from './providers/ModifierCompletionProvider'; import ParametersProvider from './providers/ParameterProvider'; import GlobalVariableProvider from './providers/GlobalVariableProvider'; +import EEFormatterProvider from './providers/EEFormatterProvider'; /** * Activate - * + * * This method is called when your extension is activated - * - * @param context + * + * @param context */ export function activate(context: vscode.ExtensionContext) { CompletionProvider.register(context); @@ -23,6 +24,7 @@ export function activate(context: vscode.ExtensionContext) { GlobalVariableProvider.register(context); IndentRulesProvider.register(context); CommandsProvider.register(context); + EEFormatterProvider.register(context); } // this method is called when your extension is deactivated diff --git a/src/providers/EEFormatterProvider.ts b/src/providers/EEFormatterProvider.ts new file mode 100644 index 0000000..da7685d --- /dev/null +++ b/src/providers/EEFormatterProvider.ts @@ -0,0 +1,25 @@ +import * as vscode from 'vscode'; +import EEFormatterService from '../services/EEFormatterService'; + +export default class EEFormatterProvider { + + private static formatter: EEFormatterService; + + /** + * Register the EE formatter provider + * @param context VS Code extension context + */ + public static register(context: vscode.ExtensionContext) { + // Create formatter instance + this.formatter = new EEFormatterService(); + + // Register document formatting provider for HTML language + const disposable = vscode.languages.registerDocumentFormattingEditProvider( + 'html', + this.formatter + ); + + // Add to context subscriptions + context.subscriptions.push(disposable); + } +} diff --git a/src/services/EEFormatterService.ts b/src/services/EEFormatterService.ts new file mode 100644 index 0000000..4d0fb0a --- /dev/null +++ b/src/services/EEFormatterService.ts @@ -0,0 +1,235 @@ +import * as vscode from 'vscode'; + +export default class EEFormatterService implements vscode.DocumentFormattingEditProvider { + + /** + * Format EE tags in the document + * @param document The document to format + * @param options Formatting options + * @param token Cancellation token + * @returns Array of text edits + */ + public provideDocumentFormattingEdits( + document: vscode.TextDocument, + options: vscode.FormattingOptions, + token: vscode.CancellationToken + ): vscode.ProviderResult { + + const edits: vscode.TextEdit[] = []; + const text = document.getText(); + const lines = text.split('\n'); + + // Get EE formatting configuration + const config = vscode.workspace.getConfiguration('ee.format'); + const wrapAttributes = config.get('wrapAttributes', 'auto'); + const indentSize = options.insertSpaces ? options.tabSize : 1; + const indentChar = options.insertSpaces ? ' '.repeat(indentSize) : '\t'; + + let i = 0; + while (i < lines.length) { + const line = lines[i]; + const eeTagMatch = this.findEETag(line); + + if (eeTagMatch) { + const formattedLines = this.formatEETag( + eeTagMatch, + lines, + i, + indentChar, + wrapAttributes + ); + + if (formattedLines.length > 1) { + // Replace the original line(s) with formatted lines + const startLine = i; + const endLine = i + eeTagMatch.lineSpan - 1; + + edits.push( + vscode.TextEdit.replace( + new vscode.Range( + document.positionAt(document.offsetAt(new vscode.Position(startLine, 0))), + document.positionAt(document.offsetAt(new vscode.Position(endLine + 1, 0))) + ), + formattedLines.join('\n') + '\n' + ) + ); + + i = endLine + 1; + } else { + i++; + } + } else { + i++; + } + } + + return edits; + } + + /** + * Find EE tag in a line + * @param line The line to search + * @returns EE tag match or null + */ + private findEETag(line: string): EETagMatch | null { + // Match EE tags: {exp:...} or {variable} or {/exp:...} + const eeTagRegex = /\{([^}]+)\}/g; + const match = eeTagRegex.exec(line); + + if (match) { + const fullMatch = match[0]; + const content = match[1]; + + // Check if it's a tag that can have attributes (exp: tags) + if (content.includes(':') && !content.startsWith('/')) { + return { + fullMatch, + content, + isClosing: false, + isPair: true, + lineSpan: 1, + startPos: match.index, + endPos: match.index + fullMatch.length + }; + } + } + + return null; + } + + /** + * Format an EE tag with proper attribute wrapping + * @param tagMatch The EE tag match + * @param lines All lines in the document + * @param startLineIndex Starting line index + * @param indentChar Character(s) to use for indentation + * @param wrapAttributes Wrap attributes setting + * @returns Array of formatted lines + */ + private formatEETag( + tagMatch: EETagMatch, + lines: string[], + startLineIndex: number, + indentChar: string, + wrapAttributes: string + ): string[] { + + const line = lines[startLineIndex]; + const beforeTag = line.substring(0, tagMatch.startPos); + const afterTag = line.substring(tagMatch.endPos); + + // Extract attributes from the tag content + const tagContent = tagMatch.content; + const attributes = this.parseEETagAttributes(tagContent); + + if (wrapAttributes === 'preserve') { + // Don't format if preserve is set + return [line]; + } + + // Determine if we should wrap attributes + const shouldWrap = wrapAttributes === 'force' || + (wrapAttributes === 'force-expand-multiline' && attributes.length > 1) || + (wrapAttributes === 'auto' && this.shouldWrapAttributes(tagContent)); + + if (!shouldWrap || attributes.length <= 1) { + return [line]; + } + + // Format with wrapped attributes + const formattedLines: string[] = []; + + // Opening tag with first attribute + formattedLines.push(`${beforeTag}{${attributes[0]}`); + + // Additional attributes on separate lines + for (let i = 1; i < attributes.length; i++) { + formattedLines.push(`${beforeTag}${indentChar}${attributes[i]}`); + } + + // Closing brace + formattedLines.push(`${beforeTag}}${afterTag}`); + + return formattedLines; + } + + /** + * Parse EE tag attributes + * @param tagContent The content inside the EE tag braces + * @returns Array of attribute strings + */ + private parseEETagAttributes(tagContent: string): string[] { + const attributes: string[] = []; + + // Split by spaces but preserve quoted values + const parts = this.splitPreservingQuotes(tagContent); + + for (const part of parts) { + const trimmed = part.trim(); + if (trimmed) { + attributes.push(trimmed); + } + } + + return attributes; + } + + /** + * Split string preserving quoted values + * @param str String to split + * @returns Array of parts + */ + private splitPreservingQuotes(str: string): string[] { + const parts: string[] = []; + let current = ''; + let inQuotes = false; + let quoteChar = ''; + + for (let i = 0; i < str.length; i++) { + const char = str[i]; + + if (!inQuotes && (char === '"' || char === "'")) { + inQuotes = true; + quoteChar = char; + current += char; + } else if (inQuotes && char === quoteChar) { + inQuotes = false; + quoteChar = ''; + current += char; + } else if (!inQuotes && char === ' ') { + if (current.trim()) { + parts.push(current.trim()); + current = ''; + } + } else { + current += char; + } + } + + if (current.trim()) { + parts.push(current.trim()); + } + + return parts; + } + + /** + * Determine if attributes should be wrapped + * @param tagContent The tag content + * @returns True if attributes should be wrapped + */ + private shouldWrapAttributes(tagContent: string): boolean { + // Simple heuristic: wrap if tag is longer than 80 characters + return tagContent.length > 80; + } +} + +interface EETagMatch { + fullMatch: string; + content: string; + isClosing: boolean; + isPair: boolean; + lineSpan: number; + startPos: number; + endPos: number; +} From 20ca30e3c2f99d02e709d344e2c1b9c5405acac3 Mon Sep 17 00:00:00 2001 From: TJ Date: Thu, 11 Sep 2025 16:21:17 -0400 Subject: [PATCH 02/10] Added indentation enforcement for elements inside expanded tags --- src/services/EEFormatterService.ts | 166 ++++++++++++++++++++++++++--- 1 file changed, 150 insertions(+), 16 deletions(-) diff --git a/src/services/EEFormatterService.ts b/src/services/EEFormatterService.ts index 4d0fb0a..f050b6b 100644 --- a/src/services/EEFormatterService.ts +++ b/src/services/EEFormatterService.ts @@ -40,21 +40,39 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit ); if (formattedLines.length > 1) { - // Replace the original line(s) with formatted lines - const startLine = i; - const endLine = i + eeTagMatch.lineSpan - 1; - - edits.push( - vscode.TextEdit.replace( - new vscode.Range( - document.positionAt(document.offsetAt(new vscode.Position(startLine, 0))), - document.positionAt(document.offsetAt(new vscode.Position(endLine + 1, 0))) - ), - formattedLines.join('\n') + '\n' - ) - ); - - i = endLine + 1; + // Check if this is a paired tag that spans multiple lines + const closingTagInfo = this.findClosingTag(eeTagMatch.content, lines, i); + + if (closingTagInfo) { + // Replace the entire paired tag section + const startLine = i; + const endLine = closingTagInfo.lineIndex; + + edits.push( + vscode.TextEdit.replace( + new vscode.Range( + document.positionAt(document.offsetAt(new vscode.Position(startLine, 0))), + document.positionAt(document.offsetAt(new vscode.Position(endLine + 1, 0))) + ), + formattedLines.join('\n') + '\n' + ) + ); + + i = endLine + 1; + } else { + // Single-line tag formatting + edits.push( + vscode.TextEdit.replace( + new vscode.Range( + document.positionAt(document.offsetAt(new vscode.Position(i, 0))), + document.positionAt(document.offsetAt(new vscode.Position(i + 1, 0))) + ), + formattedLines.join('\n') + '\n' + ) + ); + + i++; + } } else { i++; } @@ -136,7 +154,41 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit return [line]; } - // Format with wrapped attributes + // Check if this is a paired tag that spans multiple lines + const closingTagInfo = this.findClosingTag(tagContent, lines, startLineIndex); + + if (closingTagInfo) { + // Format paired tag with content indentation + return this.formatPairedEETag( + tagMatch, + lines, + startLineIndex, + closingTagInfo, + indentChar, + attributes, + beforeTag, + afterTag + ); + } else { + // Format single-line tag + return this.formatSingleLineEETag( + attributes, + beforeTag, + afterTag, + indentChar + ); + } + } + + /** + * Format a single-line EE tag + */ + private formatSingleLineEETag( + attributes: string[], + beforeTag: string, + afterTag: string, + indentChar: string + ): string[] { const formattedLines: string[] = []; // Opening tag with first attribute @@ -153,6 +205,81 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit return formattedLines; } + /** + * Format a paired EE tag with content indentation + */ + private formatPairedEETag( + tagMatch: EETagMatch, + lines: string[], + startLineIndex: number, + closingTagInfo: ClosingTagInfo, + indentChar: string, + attributes: string[], + beforeTag: string, + afterTag: string + ): string[] { + const formattedLines: string[] = []; + + // Opening tag with first attribute + formattedLines.push(`${beforeTag}{${attributes[0]}`); + + // Additional attributes on separate lines + for (let i = 1; i < attributes.length; i++) { + formattedLines.push(`${beforeTag}${indentChar}${attributes[i]}`); + } + + // Closing brace of opening tag + formattedLines.push(`${beforeTag}}${afterTag}`); + + // Add indented content between tags + for (let i = startLineIndex + 1; i < closingTagInfo.lineIndex; i++) { + const contentLine = lines[i]; + if (contentLine.trim()) { + formattedLines.push(`${beforeTag}${indentChar}${contentLine}`); + } else { + formattedLines.push(contentLine); // Preserve empty lines + } + } + + // Add closing tag with proper indentation + const closingLine = lines[closingTagInfo.lineIndex]; + const closingBeforeTag = closingLine.substring(0, closingTagInfo.startPos); + const closingAfterTag = closingLine.substring(closingTagInfo.endPos); + formattedLines.push(`${closingBeforeTag}{${closingTagInfo.content}}${closingAfterTag}`); + + return formattedLines; + } + + /** + * Find the closing tag for a paired EE tag + */ + private findClosingTag( + openingContent: string, + lines: string[], + startLineIndex: number + ): ClosingTagInfo | null { + // Extract the tag name (e.g., "exp:channel:entries" from "exp:channel:entries channel=...") + const tagName = openingContent.split(' ')[0]; + const closingTagPattern = `{/${tagName}}`; + + // Search for closing tag in subsequent lines + for (let i = startLineIndex + 1; i < lines.length; i++) { + const line = lines[i]; + const closingMatch = line.match(new RegExp(`(\\{[^}]*/${tagName}\\})`)); + + if (closingMatch) { + return { + lineIndex: i, + startPos: closingMatch.index!, + endPos: closingMatch.index! + closingMatch[0].length, + content: closingMatch[0].slice(1, -1) // Remove { and } + }; + } + } + + return null; + } + /** * Parse EE tag attributes * @param tagContent The content inside the EE tag braces @@ -233,3 +360,10 @@ interface EETagMatch { startPos: number; endPos: number; } + +interface ClosingTagInfo { + lineIndex: number; + startPos: number; + endPos: number; + content: string; +} From 3e06642326d21e07723735787ffaa2b773cb4c3b Mon Sep 17 00:00:00 2001 From: TJ Date: Wed, 1 Oct 2025 20:45:51 -0400 Subject: [PATCH 03/10] Introduced 'nest scanning' to identify and apply formatting to nested EE tags, worked on preserving nested html --- src/services/EEFormatterService.ts | 255 ++++++++++++++++++++++------- 1 file changed, 200 insertions(+), 55 deletions(-) diff --git a/src/services/EEFormatterService.ts b/src/services/EEFormatterService.ts index f050b6b..0e762e5 100644 --- a/src/services/EEFormatterService.ts +++ b/src/services/EEFormatterService.ts @@ -31,7 +31,7 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit const eeTagMatch = this.findEETag(line); if (eeTagMatch) { - const formattedLines = this.formatEETag( + const result = this.formatEETag( eeTagMatch, lines, i, @@ -39,9 +39,10 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit wrapAttributes ); - if (formattedLines.length > 1) { + if (result.formattedLines.length > 1) { // Check if this is a paired tag that spans multiple lines - const closingTagInfo = this.findClosingTag(eeTagMatch.content, lines, i); + const { tagName } = this.parseEETagAttributes(eeTagMatch.content); + const closingTagInfo = this.findClosingTag(tagName, lines, i); if (closingTagInfo) { // Replace the entire paired tag section @@ -54,7 +55,7 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit document.positionAt(document.offsetAt(new vscode.Position(startLine, 0))), document.positionAt(document.offsetAt(new vscode.Position(endLine + 1, 0))) ), - formattedLines.join('\n') + '\n' + result.formattedLines.join('\n') + '\n' ) ); @@ -67,7 +68,7 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit document.positionAt(document.offsetAt(new vscode.Position(i, 0))), document.positionAt(document.offsetAt(new vscode.Position(i + 1, 0))) ), - formattedLines.join('\n') + '\n' + result.formattedLines.join('\n') + '\n' ) ); @@ -90,13 +91,29 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit * @returns EE tag match or null */ private findEETag(line: string): EETagMatch | null { - // Match EE tags: {exp:...} or {variable} or {/exp:...} - const eeTagRegex = /\{([^}]+)\}/g; - const match = eeTagRegex.exec(line); + // Handle nested braces in EE tags + let braceCount = 0; + let start = -1; + let end = -1; + + for (let i = 0; i < line.length; i++) { + if (line[i] === '{') { + if (start === -1) { + start = i; + } + braceCount++; + } else if (line[i] === '}') { + braceCount--; + if (braceCount === 0 && start !== -1) { + end = i; + break; + } + } + } - if (match) { - const fullMatch = match[0]; - const content = match[1]; + if (start !== -1 && end !== -1) { + const fullMatch = line.substring(start, end + 1); + const content = line.substring(start + 1, end); // Check if it's a tag that can have attributes (exp: tags) if (content.includes(':') && !content.startsWith('/')) { @@ -106,8 +123,8 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit isClosing: false, isPair: true, lineSpan: 1, - startPos: match.index, - endPos: match.index + fullMatch.length + startPos: start, + endPos: end + 1 }; } } @@ -122,7 +139,7 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit * @param startLineIndex Starting line index * @param indentChar Character(s) to use for indentation * @param wrapAttributes Wrap attributes setting - * @returns Array of formatted lines + * @returns Object with formatted lines and skip count */ private formatEETag( tagMatch: EETagMatch, @@ -130,53 +147,67 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit startLineIndex: number, indentChar: string, wrapAttributes: string - ): string[] { + ): { formattedLines: string[]; skipLines: number } { const line = lines[startLineIndex]; const beforeTag = line.substring(0, tagMatch.startPos); const afterTag = line.substring(tagMatch.endPos); + // Calculate proper indentation from surrounding context + const contextIndent = this.calculateContextIndentation(lines, startLineIndex, indentChar); + // Extract attributes from the tag content const tagContent = tagMatch.content; - const attributes = this.parseEETagAttributes(tagContent); + const { tagName, attributes } = this.parseEETagAttributes(tagContent); if (wrapAttributes === 'preserve') { // Don't format if preserve is set - return [line]; + return { formattedLines: [line], skipLines: 0 }; } - // Determine if we should wrap attributes - const shouldWrap = wrapAttributes === 'force' || - (wrapAttributes === 'force-expand-multiline' && attributes.length > 1) || - (wrapAttributes === 'auto' && this.shouldWrapAttributes(tagContent)); - - if (!shouldWrap || attributes.length <= 1) { - return [line]; + // For exp: tags, always format if they have attributes + // For other tags, use the wrap attributes setting + const isExpTag = tagName.startsWith('exp:'); + const shouldWrap = isExpTag ? + (attributes.length > 0) : // Always format exp tags with attributes + (wrapAttributes === 'force' || + (wrapAttributes === 'force-expand-multiline' && attributes.length > 0) || + (wrapAttributes === 'auto' && this.shouldWrapAttributes(tagContent))); + + if (!shouldWrap || attributes.length === 0) { + return { formattedLines: [line], skipLines: 0 }; } // Check if this is a paired tag that spans multiple lines - const closingTagInfo = this.findClosingTag(tagContent, lines, startLineIndex); + const closingTagInfo = this.findClosingTag(tagName, lines, startLineIndex); if (closingTagInfo) { // Format paired tag with content indentation - return this.formatPairedEETag( + const formattedLines = this.formatPairedEETag( tagMatch, lines, startLineIndex, closingTagInfo, indentChar, + tagName, attributes, beforeTag, - afterTag + afterTag, + contextIndent ); + + // Skip the lines that were processed + return { formattedLines, skipLines: closingTagInfo.lineIndex - startLineIndex }; } else { // Format single-line tag - return this.formatSingleLineEETag( + const formattedLines = this.formatSingleLineEETag( + tagName, attributes, beforeTag, afterTag, indentChar ); + return { formattedLines, skipLines: 0 }; } } @@ -184,6 +215,7 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit * Format a single-line EE tag */ private formatSingleLineEETag( + tagName: string, attributes: string[], beforeTag: string, afterTag: string, @@ -191,12 +223,12 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit ): string[] { const formattedLines: string[] = []; - // Opening tag with first attribute - formattedLines.push(`${beforeTag}{${attributes[0]}`); + // Opening tag with tag name + formattedLines.push(`${beforeTag}{${tagName}`); - // Additional attributes on separate lines - for (let i = 1; i < attributes.length; i++) { - formattedLines.push(`${beforeTag}${indentChar}${attributes[i]}`); + // Attributes on separate lines + for (const attribute of attributes) { + formattedLines.push(`${beforeTag}${indentChar}${attribute}`); } // Closing brace @@ -214,38 +246,45 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit startLineIndex: number, closingTagInfo: ClosingTagInfo, indentChar: string, + tagName: string, attributes: string[], beforeTag: string, - afterTag: string + afterTag: string, + contextIndent: string ): string[] { const formattedLines: string[] = []; - // Opening tag with first attribute - formattedLines.push(`${beforeTag}{${attributes[0]}`); + // Use context-aware indentation for the opening tag + const openingIndent = contextIndent; - // Additional attributes on separate lines - for (let i = 1; i < attributes.length; i++) { - formattedLines.push(`${beforeTag}${indentChar}${attributes[i]}`); + // Opening tag with tag name + formattedLines.push(`${openingIndent}{${tagName}`); + + // Attributes on separate lines + for (const attribute of attributes) { + formattedLines.push(`${openingIndent}${indentChar}${attribute}`); } // Closing brace of opening tag - formattedLines.push(`${beforeTag}}${afterTag}`); + formattedLines.push(`${openingIndent}}${afterTag}`); - // Add indented content between tags + // Add content between tags, indenting the whole block as one unit for (let i = startLineIndex + 1; i < closingTagInfo.lineIndex; i++) { const contentLine = lines[i]; if (contentLine.trim()) { - formattedLines.push(`${beforeTag}${indentChar}${contentLine}`); + // Remove original indentation and add proper indentation relative to EE tag + const trimmedContent = contentLine.trim(); + formattedLines.push(`${openingIndent}${indentChar}${trimmedContent}`); } else { - formattedLines.push(contentLine); // Preserve empty lines + // Preserve empty lines + formattedLines.push(contentLine); } } // Add closing tag with proper indentation const closingLine = lines[closingTagInfo.lineIndex]; - const closingBeforeTag = closingLine.substring(0, closingTagInfo.startPos); const closingAfterTag = closingLine.substring(closingTagInfo.endPos); - formattedLines.push(`${closingBeforeTag}{${closingTagInfo.content}}${closingAfterTag}`); + formattedLines.push(`${openingIndent}{${closingTagInfo.content}}${closingAfterTag}`); return formattedLines; } @@ -254,14 +293,10 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit * Find the closing tag for a paired EE tag */ private findClosingTag( - openingContent: string, + tagName: string, lines: string[], startLineIndex: number ): ClosingTagInfo | null { - // Extract the tag name (e.g., "exp:channel:entries" from "exp:channel:entries channel=...") - const tagName = openingContent.split(' ')[0]; - const closingTagPattern = `{/${tagName}}`; - // Search for closing tag in subsequent lines for (let i = startLineIndex + 1; i < lines.length; i++) { const line = lines[i]; @@ -283,22 +318,25 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit /** * Parse EE tag attributes * @param tagContent The content inside the EE tag braces - * @returns Array of attribute strings + * @returns Object with tagName and attributes array */ - private parseEETagAttributes(tagContent: string): string[] { + private parseEETagAttributes(tagContent: string): { tagName: string; attributes: string[] } { const attributes: string[] = []; // Split by spaces but preserve quoted values const parts = this.splitPreservingQuotes(tagContent); - for (const part of parts) { - const trimmed = part.trim(); + // First part is the tag name, rest are attributes + const tagName = parts[0] || ''; + + for (let i = 1; i < parts.length; i++) { + const trimmed = parts[i].trim(); if (trimmed) { attributes.push(trimmed); } } - return attributes; + return { tagName, attributes }; } /** @@ -349,6 +387,113 @@ export default class EEFormatterService implements vscode.DocumentFormattingEdit // Simple heuristic: wrap if tag is longer than 80 characters return tagContent.length > 80; } + + /** + * Calculate proper indentation based on surrounding context + * @param lines All lines in the document + * @param currentLineIndex Current line index + * @param indentChar Character(s) to use for indentation + * @returns Proper indentation string + */ + private calculateContextIndentation(lines: string[], currentLineIndex: number, indentChar: string): string { + // Look backwards to find the nearest opening block + for (let i = currentLineIndex - 1; i >= 0; i--) { + const line = lines[i]; + const trimmedLine = line.trim(); + + // Skip empty lines and comments + if (!trimmedLine || trimmedLine.startsWith('