diff --git a/javascript/packages/language-server/src/formatting_service.ts b/javascript/packages/language-server/src/formatting_service.ts index 49be5cbe1..6b9f3e3f5 100644 --- a/javascript/packages/language-server/src/formatting_service.ts +++ b/javascript/packages/language-server/src/formatting_service.ts @@ -8,6 +8,8 @@ import { Settings } from "./settings" import { Config } from "@herb-tools/config" import { version } from "../package.json" +const OPEN_CONFIG_ACTION = 'Open .herb.yml' + export class FormattingService { private connection: Connection private documents: TextDocuments @@ -16,6 +18,7 @@ export class FormattingService { private config?: Config private preRewriters: ASTRewriter[] = [] private postRewriters: StringRewriter[] = [] + private failedRewriters: Map = new Map() constructor(connection: Connection, documents: TextDocuments, project: Project, settings: Settings) { this.connection = connection @@ -27,6 +30,7 @@ export class FormattingService { async initialize() { try { this.config = await Config.loadForEditor(this.project.projectPath, version) + this.connection.console.log(`[Formatting] Config loaded, formatter config: ${JSON.stringify(this.config?.formatter || 'none')}`) await this.loadConfiguredRewriters() this.connection.console.log("Herb formatter initialized successfully") } catch (error) { @@ -49,7 +53,12 @@ export class FormattingService { } private async loadConfiguredRewriters() { + this.connection.console.log(`[Rewriter] Loading rewriters from config: ${JSON.stringify(this.config?.formatter?.rewriter || 'none')}`) + + this.failedRewriters.clear() + if (!this.config?.formatter?.rewriter) { + this.connection.console.log(`[Rewriter] No rewriter config found, clearing rewriters`) this.preRewriters = [] this.postRewriters = [] @@ -58,6 +67,10 @@ export class FormattingService { try { const baseDir = this.config.projectPath || this.project.projectPath + this.connection.console.log(`[Rewriter] Using baseDir: ${baseDir}`) + this.connection.console.log(`[Rewriter] config.projectPath: ${this.config.projectPath}`) + this.connection.console.log(`[Rewriter] project.projectPath: ${this.project.projectPath}`) + const preNames = this.config.formatter.rewriter.pre || [] const postNames = this.config.formatter.rewriter.post || [] const warnings: string[] = [] @@ -105,7 +118,9 @@ export class FormattingService { preRewriters.push(instance) } catch (error) { - warnings.push(`Failed to initialize pre-format rewriter "${name}": ${error}`) + const errorMessage = error instanceof Error ? error.message : String(error) + warnings.push(`Failed to initialize pre-format rewriter "${name}": ${errorMessage}`) + this.failedRewriters.set(name, errorMessage) } } @@ -130,15 +145,39 @@ export class FormattingService { postRewriters.push(instance) } catch (error) { - warnings.push(`Failed to initialize post-format rewriter "${name}": ${error}`) + const errorMessage = error instanceof Error ? error.message : String(error) + warnings.push(`Failed to initialize post-format rewriter "${name}": ${errorMessage}`) + this.failedRewriters.set(name, errorMessage) } } this.preRewriters = preRewriters this.postRewriters = postRewriters + this.connection.console.log(`[Rewriter] Loaded ${preRewriters.length} pre-rewriters: ${preRewriters.map(r => r.name).join(', ')}`) + this.connection.console.log(`[Rewriter] Loaded ${postRewriters.length} post-rewriters: ${postRewriters.map(r => r.name).join(', ')}`) + if (warnings.length > 0) { - warnings.forEach(warning => this.connection.console.warn(`Rewriter: ${warning}`)) + // Log each warning individually + warnings.forEach(warning => { + this.connection.console.warn(`Rewriter: ${warning}`) + }) + + // Show a single combined warning message + const message = warnings.length === 1 + ? `Herb Rewriter: ${warnings[0]}` + : `Herb Rewriters (${warnings.length} failed):\n${warnings.map((w, i) => `${i + 1}. ${w}`).join('\n')}` + + if (this.settings.hasShowDocumentCapability) { + this.connection.window.showWarningMessage(message, { title: OPEN_CONFIG_ACTION }).then(action => { + if (action?.title === OPEN_CONFIG_ACTION) { + const configPath = `${this.project.projectPath}/.herb.yml` + this.connection.window.showDocument({ uri: `file://${configPath}`, takeFocus: true }) + } + }) + } else { + this.connection.window.showWarningMessage(message) + } } } catch (error) { this.connection.console.error(`Failed to load rewriters: ${error}`) @@ -203,6 +242,27 @@ export class FormattingService { try { const text = document.getText() const config = await this.getConfigWithSettings(params.textDocument.uri) + + this.connection.console.log(`[Formatting] Creating formatter with ${this.preRewriters.length} pre-rewriters, ${this.postRewriters.length} post-rewriters`) + + if (this.failedRewriters.size > 0) { + const failedList = Array.from(this.failedRewriters.entries()) + const message = failedList.length === 1 + ? `Herb Rewriter "${failedList[0][0]}" is not available: ${failedList[0][1]}` + : `Herb Rewriters (${failedList.length} not available):\n${failedList.map(([name, error], i) => `${i + 1}. ${name}: ${error}`).join('\n')}` + + if (this.settings.hasShowDocumentCapability) { + this.connection.window.showWarningMessage(message, { title: OPEN_CONFIG_ACTION }).then(action => { + if (action?.title === OPEN_CONFIG_ACTION) { + const configPath = `${this.project.projectPath}/.herb.yml` + this.connection.window.showDocument({ uri: `file://${configPath}`, takeFocus: true }) + } + }) + } else { + this.connection.window.showWarningMessage(message) + } + } + const formatter = Formatter.from(this.project.herbBackend, config, { preRewriters: this.preRewriters, postRewriters: this.postRewriters diff --git a/javascript/packages/language-server/src/service.ts b/javascript/packages/language-server/src/service.ts index fdeff70dc..0ee567081 100644 --- a/javascript/packages/language-server/src/service.ts +++ b/javascript/packages/language-server/src/service.ts @@ -93,6 +93,7 @@ export class Service { async refresh() { await this.project.refresh() + await this.formattingService.refreshConfig(this.config) await this.diagnostics.refreshAllDocuments() } diff --git a/javascript/packages/language-server/src/settings.ts b/javascript/packages/language-server/src/settings.ts index f010deeab..8be334f84 100644 --- a/javascript/packages/language-server/src/settings.ts +++ b/javascript/packages/language-server/src/settings.ts @@ -39,6 +39,7 @@ export class Settings { hasConfigurationCapability = false hasWorkspaceFolderCapability = false hasDiagnosticRelatedInformationCapability = false + hasShowDocumentCapability = false params: InitializeParams capabilities: ClientCapabilities @@ -50,6 +51,7 @@ export class Settings { this.connection = connection this.hasConfigurationCapability = !!(this.capabilities.workspace && !!this.capabilities.workspace.configuration) + this.hasShowDocumentCapability = !!(this.capabilities.window?.showDocument) this.hasWorkspaceFolderCapability = !!( this.capabilities.workspace && !!this.capabilities.workspace.workspaceFolders diff --git a/javascript/packages/rewriter/package.json b/javascript/packages/rewriter/package.json index 2bf2a7181..0541dac57 100644 --- a/javascript/packages/rewriter/package.json +++ b/javascript/packages/rewriter/package.json @@ -39,6 +39,7 @@ }, "dependencies": { "@herb-tools/core": "0.7.5", + "@herb-tools/tailwind-class-sorter": "0.7.5", "glob": "^11.0.3" }, "devDependencies": { diff --git a/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts b/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts index e1262a964..156710bf0 100644 --- a/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts +++ b/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts @@ -243,9 +243,25 @@ export class TailwindClassSorterRewriter extends ASTRewriter { } async initialize(context: RewriteContext): Promise { - this.sorter = await TailwindClassSorter.fromConfig({ - baseDir: context.baseDir - }) + try { + this.sorter = await TailwindClassSorter.fromConfig({ + baseDir: context.baseDir + }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + + if (errorMessage.includes('Cannot find module') || errorMessage.includes('ENOENT')) { + throw new Error( + `Tailwind CSS is not installed in this project. ` + + `To use the Tailwind class sorter, install Tailwind CSS itself using: npm install -D tailwindcss, ` + + `or remove the "tailwind-class-sorter" rewriter from your .herb.yml config file.\n` + + `If "tailwindcss" is already part of your package.json, make sure your NPM dependencies are installed.\n` + + `Original error: ${errorMessage}.` + ) + } + + throw error + } } rewrite(node: T, _context: RewriteContext): T { diff --git a/javascript/packages/vscode/src/client.ts b/javascript/packages/vscode/src/client.ts index bccabfb71..a3d49669a 100644 --- a/javascript/packages/vscode/src/client.ts +++ b/javascript/packages/vscode/src/client.ts @@ -83,6 +83,7 @@ export class Client { indentWidth: projectConfig.formatter?.indentWidth ?? 2, maxLineLength: projectConfig.formatter?.maxLineLength ?? 80, exclude: projectConfig.formatter?.exclude, + rewriter: projectConfig.formatter?.rewriter, }, trace: { server: vscodeConfig.get('trace.server', 'verbose'), @@ -175,6 +176,7 @@ export class Client { indentWidth: projectConfig.formatter?.indentWidth ?? 2, maxLineLength: projectConfig.formatter?.maxLineLength ?? 80, exclude: projectConfig.formatter?.exclude, + rewriter: projectConfig.formatter?.rewriter, }, trace: { server: vscodeConfig.get('trace.server', 'verbose'), // Trace is always from VS Code diff --git a/yarn.lock b/yarn.lock index a3d9d2d63..1e3927a80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4276,7 +4276,7 @@ esbuild-plugin-copy@^2.1.1: fs-extra "^10.0.1" globby "^11.0.3" -esbuild@^0.25.0, esbuild@^0.25.11, esbuild@~0.25.0: +esbuild@^0.25.0, esbuild@~0.25.0: version "0.25.11" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz" integrity sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==