diff --git a/package.json b/package.json index 5025ab9..119784c 100644 --- a/package.json +++ b/package.json @@ -66,10 +66,15 @@ "default": 80, "description": "Maximum line length for DeviceTree files. Lines longer than this will be wrapped." }, - "devicetree.enableWarnings": { + "devicetree.diagnostics.enableWarnings": { "type": "boolean", "default": true, "description": "Enable warnings for DeviceTree files. If disabled, only errors will be reported." + }, + "devicetree.diagnostics.lineLengthIncludeComments": { + "type": "boolean", + "default": true, + "markdownDescription": "Include comments when calculating line length for warnings." } } } diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 2cfac7c..8364ccc 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -7,17 +7,29 @@ import * as vscode from 'vscode'; export class DtsDiagnosticsProvider { private diagnosticCollection: vscode.DiagnosticCollection; private maxLineLength: number; + private includeComments: boolean; private tabSize: number; - constructor(maxLineLength: number) { + constructor(maxLineLength: number, includeComments: boolean) { this.diagnosticCollection = vscode.languages.createDiagnosticCollection('devicetree'); this.maxLineLength = maxLineLength + 1; + this.includeComments = includeComments; // Read tabSize from editor configuration const editorConfig = vscode.workspace.getConfiguration('editor'); this.tabSize = editorConfig.get('tabSize', 8); } + /** + * Update configuration settings + * @param maxLineLength The new maximum line length + * @param includeComments Whether to include comments in line length calculation + */ + updateSettings(maxLineLength: number, includeComments: boolean): void { + this.maxLineLength = maxLineLength + 1; + this.includeComments = includeComments; + } + /** * Calculate visual length of a line considering tab stops * @param line The line to calculate the visual length for @@ -36,6 +48,43 @@ export class DtsDiagnosticsProvider { return visualLength + 1; } + /** + * Remove comments from a line for length calculation + * @param line The line to process + * @param inBlockComment Whether we're currently inside a multi-line block comment + * @returns Object with the line with comments removed and whether we're still in a block comment + */ + private removeComments(line: string, inBlockComment: boolean): { line: string; inBlockComment: boolean } { + // Handle multi-line block comment continuation + if (inBlockComment) { + const endComment = line.indexOf('*/'); + if (endComment !== -1) { + line = line.substring(endComment + 2); + inBlockComment = false; + } else { + return { line: '', inBlockComment: true }; + } + } + + // Match strings (double/single quoted) or comments + // Process in order: strings are kept, comments are removed + const regex = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\/\*[\s\S]*?\*\/|\/\*[\s\S]*|\/\/.*/g; + + const result = line.replace(regex, (match) => { + // Keep strings (start with " or ') + if (match[0] === '"' || match[0] === "'") { + return match; + } + // Block comment that doesn't close - set flag and remove rest of line + if (match.startsWith('/*') && !match.endsWith('*/')) { + inBlockComment = true; + } + return ''; + }); + + return { line: result.trimEnd(), inBlockComment }; + } + /** * Check for lines exceeding maximum length * @param document The document to check @@ -45,9 +94,22 @@ export class DtsDiagnosticsProvider { const diagnostics: vscode.Diagnostic[] = []; const text = document.getText(); const lines = text.split('\n'); + let inBlockComment = false; lines.forEach((line, index) => { - const visualLength = this.calculateVisualLength(line); + // Calculate length based on configuration + let lineToCheck: string; + + if (this.includeComments) { + lineToCheck = line; + } else { + const result = this.removeComments(line, inBlockComment); + lineToCheck = result.line; + inBlockComment = result.inBlockComment; + } + + const visualLength = this.calculateVisualLength(lineToCheck); + if (visualLength > this.maxLineLength) { const range = new vscode.Range(index, 0, index, Number.MAX_VALUE); const diagnostic = new vscode.Diagnostic( diff --git a/src/extension.ts b/src/extension.ts index f9f3aa3..013f872 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,8 +7,9 @@ import * as vscode from 'vscode'; import { DtsFormatterProvider } from './formatter'; import { DtsDiagnosticsProvider } from './diagnostics'; -// Global diagnostics provider instance +// Global provider instances let diagnosticsProvider: DtsDiagnosticsProvider | undefined; +let formatterProvider: DtsFormatterProvider | undefined; /** * Activate the extension @@ -20,48 +21,44 @@ export function activate(context: vscode.ExtensionContext) { // Get configuration const config = vscode.workspace.getConfiguration('devicetree'); const maxLineLength = config.get('maxLineLength', 80); - const enableWarnings = config.get('enableWarnings', true); + const enableWarnings = config.get('diagnostics.enableWarnings', true); + const includeComments = config.get('diagnostics.lineLengthIncludeComments', true); - // Create diagnostics provider - diagnosticsProvider = new DtsDiagnosticsProvider(maxLineLength); + // Create providers + diagnosticsProvider = new DtsDiagnosticsProvider(maxLineLength, includeComments); + formatterProvider = new DtsFormatterProvider(maxLineLength); - // Register the formatter provider context.subscriptions.push( - vscode.languages.registerDocumentFormattingEditProvider('dts', new DtsFormatterProvider(maxLineLength)) + vscode.languages.registerDocumentFormattingEditProvider('dts', formatterProvider) ); // Register diagnostics provider context.subscriptions.push(diagnosticsProvider); if (enableWarnings) { + // Helper function to analyze document if it's a DTS file + const analyzeIfDts = (document: vscode.TextDocument): void => { + if (document.languageId === 'dts' && diagnosticsProvider) { + void diagnosticsProvider.analyzeDocument(document); + } + }; + // Listen for document changes context.subscriptions.push( vscode.workspace.onDidChangeTextDocument(event => { - if (event.document.languageId === 'dts' && diagnosticsProvider) { - // Debounce: analyze after a short delay to avoid excessive analysis - setTimeout(() => { - diagnosticsProvider?.analyzeDocument(event.document); - }, 500); - } - }) - ); + // Debounce: analyze after a short delay to avoid excessive analysis + setTimeout(() => analyzeIfDts(event.document), 500); + })); // Listen for text editor options changes (e.g., when tab size changes in the editor) context.subscriptions.push( vscode.window.onDidChangeTextEditorOptions(event => { - const document = event.textEditor.document; - if (document.languageId === 'dts' && diagnosticsProvider) { - diagnosticsProvider.analyzeDocument(document); - } + analyzeIfDts(event.textEditor.document); }) ); // Listen for document opens context.subscriptions.push( - vscode.workspace.onDidOpenTextDocument(document => { - if (document.languageId === 'dts' && diagnosticsProvider) { - diagnosticsProvider.analyzeDocument(document); - } - }) + vscode.workspace.onDidOpenTextDocument(analyzeIfDts) ); // Listen for document closes @@ -72,8 +69,35 @@ export function activate(context: vscode.ExtensionContext) { } }) ); - } + + // Listen for configuration changes + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(event => { + if (event.affectsConfiguration('devicetree')) { + const config = vscode.workspace.getConfiguration('devicetree'); + const maxLineLength = config.get('maxLineLength', 80); + const includeComments = config.get('diagnostics.lineLengthIncludeComments', true); + + // Update formatter settings + if (formatterProvider) { + formatterProvider.updateSettings(maxLineLength); + } + + // Update diagnostics settings + if (diagnosticsProvider) { + diagnosticsProvider.updateSettings(maxLineLength, includeComments); + + // Re-analyze all open DTS documents with new settings + vscode.workspace.textDocuments.forEach(document => { + if (document.languageId === 'dts') { + void diagnosticsProvider?.analyzeDocument(document); + } + }); + } + } + }) + ); } /** @@ -82,6 +106,10 @@ export function activate(context: vscode.ExtensionContext) { */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function deactivate() { + if (formatterProvider) { + formatterProvider.dispose(); + formatterProvider = undefined; + } if (diagnosticsProvider) { diagnosticsProvider.dispose(); diagnosticsProvider = undefined; diff --git a/src/formatter.ts b/src/formatter.ts index e6f7316..0ae8476 100644 --- a/src/formatter.ts +++ b/src/formatter.ts @@ -514,6 +514,14 @@ export class DtsFormatterProvider implements vscode.DocumentFormattingEditProvid this.outputChannel = vscode.window.createOutputChannel('DeviceTree'); } + /** + * Update configuration settings + * @param maxLineLength The new maximum line length + */ + updateSettings(maxLineLength: number): void { + this.maxLineLength = maxLineLength; + } + /** * Provide formatting edits for a document * @param document The document to format diff --git a/src/test/diagnostics.test.ts b/src/test/diagnostics.test.ts index df2a3c7..7ef91cd 100644 --- a/src/test/diagnostics.test.ts +++ b/src/test/diagnostics.test.ts @@ -107,6 +107,95 @@ suite('DTS Diagnostics - Line Length', () => { }); }); +suite('DTS Diagnostics - Comment Handling', () => { + test('Should include comments in line length by default', async () => { + const input = '/ {\n\tmodel = "Test";\t\t\t\t\t/* This is a very long comment that makes the line exceed 80 characters */\n};'; + const diagnostics = await getDiagnostics(input); + assert.ok(diagnostics.length > 0); + assert.ok(diagnostics[0].message.includes('exceeds maximum length')); + }); + + test('Should handle multi-line block comments correctly', async () => { + // When lineLengthIncludeComments is false, lines inside a multi-line block comment + // should be recognized as comment-only lines and not trigger warnings + const config = vscode.workspace.getConfiguration('devicetree'); + const original = config.get('diagnostics.lineLengthIncludeComments', true); + + try { + await config.update('diagnostics.lineLengthIncludeComments', false, vscode.ConfigurationTarget.Global); + await new Promise(resolve => setTimeout(resolve, 300)); + + const input = `/ { +\t/* +\t * This is a very long comment line inside a multi-line block comment that exceeds 80 characters +\t */ +\tmodel = "Test"; +};`; + const diagnostics = await getDiagnostics(input); + // Should NOT warn because the long line is inside a block comment + assert.strictEqual(diagnostics.length, 0); + } finally { + await config.update('diagnostics.lineLengthIncludeComments', original, vscode.ConfigurationTarget.Global); + } + }); + + test('Should not treat comment-like content inside strings as comments', async () => { + const config = vscode.workspace.getConfiguration('devicetree'); + const original = config.get('diagnostics.lineLengthIncludeComments', true); + + try { + await config.update('diagnostics.lineLengthIncludeComments', false, vscode.ConfigurationTarget.Global); + await new Promise(resolve => setTimeout(resolve, 300)); + + // The string contains "// " which looks like a comment but isn't + // A very long URL that makes the line exceed 80 chars + const input = '/ {\n\turl = "https://example.com/very/long/path/that/exceeds/the/maximum/line/length";\n};'; + const diagnostics = await getDiagnostics(input); + // Should warn because the string content is NOT a comment + assert.ok(diagnostics.length > 0); + } finally { + await config.update('diagnostics.lineLengthIncludeComments', original, vscode.ConfigurationTarget.Global); + } + }); + + test('Should exclude comments when lineLengthIncludeComments is false', async () => { + const config = vscode.workspace.getConfiguration('devicetree'); + const original = config.get('diagnostics.lineLengthIncludeComments', true); + + try { + await config.update('diagnostics.lineLengthIncludeComments', false, vscode.ConfigurationTarget.Global); + // Wait for config change to be picked up + await new Promise(resolve => setTimeout(resolve, 300)); + + // Line is short without the comment, but exceeds 80 with the comment + const input = '/ {\n\tmodel = "Test";\t\t\t\t\t/* This is a very long comment that makes the line exceed 80 characters */\n};'; + const diagnostics = await getDiagnostics(input); + // Should NOT warn when comments are excluded + assert.strictEqual(diagnostics.length, 0); + } finally { + await config.update('diagnostics.lineLengthIncludeComments', original, vscode.ConfigurationTarget.Global); + } + }); + + test('Should exclude line comments (//) when lineLengthIncludeComments is false', async () => { + const config = vscode.workspace.getConfiguration('devicetree'); + const original = config.get('diagnostics.lineLengthIncludeComments', true); + + try { + await config.update('diagnostics.lineLengthIncludeComments', false, vscode.ConfigurationTarget.Global); + await new Promise(resolve => setTimeout(resolve, 300)); + + // Line is short without the comment, but exceeds 80 with the comment + const input = '/ {\n\tmodel = "Test";\t\t\t\t\t// This is a very long line comment that makes the line exceed 80 characters\n};'; + const diagnostics = await getDiagnostics(input); + // Should NOT warn when comments are excluded + assert.strictEqual(diagnostics.length, 0); + } finally { + await config.update('diagnostics.lineLengthIncludeComments', original, vscode.ConfigurationTarget.Global); + } + }); +}); + suite('DTS Diagnostics - Configuration', () => { test('Should use configured maxLineLength on startup', async () => { // Note: The maxLineLength is read once when the extension activates @@ -130,11 +219,11 @@ suite('DTS Diagnostics - Configuration', () => { test('Should be disabled when enableWarnings is false', async () => { const config = vscode.workspace.getConfiguration('devicetree'); - const originalEnableWarnings = config.get('enableWarnings', true); + const originalEnableWarnings = config.get('diagnostics.enableWarnings', true); try { // Disable warnings - await config.update('enableWarnings', false, vscode.ConfigurationTarget.Global); + await config.update('diagnostics.enableWarnings', false, vscode.ConfigurationTarget.Global); // Reload extension or wait for config to apply await new Promise(resolve => setTimeout(resolve, 200)); @@ -148,7 +237,7 @@ suite('DTS Diagnostics - Configuration', () => { assert.ok(diagnostics.length >= 0); // Just verify it doesn't crash } finally { // Restore original setting - await config.update('enableWarnings', originalEnableWarnings, vscode.ConfigurationTarget.Global); + await config.update('diagnostics.enableWarnings', originalEnableWarnings, vscode.ConfigurationTarget.Global); } }); });