diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..435a87f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "studio", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/test/vscode/extension.test.ts b/test/vscode/extension.test.ts index 558ebe7..2a2a2ba 100644 --- a/test/vscode/extension.test.ts +++ b/test/vscode/extension.test.ts @@ -198,7 +198,7 @@ suite('Extension Test Suite', () => { const vscodeJsonDiagnostics = diagnostics.filter(diagnostic => diagnostic.source === 'json' || diagnostic.source === 'JSON'); - assert.strictEqual(vscodeJsonDiagnostics.length, 0, + assert.strictEqual(vscodeJsonDiagnostics.length, 0, 'VS Code built-in JSON validation should be disabled'); const sourcemetaDiagnostics = diagnostics.filter(diagnostic => @@ -207,4 +207,50 @@ suite('Extension Test Suite', () => { assert.ok(sourcemetaDiagnostics.length > 0, 'Sourcemeta Studio should still report metaschema errors'); }); + + test("Lint diagnostics should be ordered by line number", async function () { + this.timeout(15000); + + const extension = vscode.extensions.getExtension( + "sourcemeta.sourcemeta-studio" + ); + if (extension && !extension.isActive) { + await extension.activate(); + } + const fixtureDir = path.join( + __dirname, + "..", + "..", + "..", + "test", + "vscode", + "fixtures" + ); + const schemaPath = path.join(fixtureDir, "lint-order.schema.json"); + + const document = await vscode.workspace.openTextDocument( + vscode.Uri.file(schemaPath) + ); + await vscode.window.showTextDocument(document); + + await vscode.commands.executeCommand("sourcemeta-studio.openPanel"); + + await new Promise((resolve) => setTimeout(resolve, 5000)); + + const diagnostics = vscode.languages + .getDiagnostics(document.uri) + .filter((d) => d.source === "Sourcemeta Studio (Lint)"); + + assert.ok(diagnostics.length > 1, "Expected multiple lint diagnostics"); + + const lineNumbers = diagnostics.map((d) => d.range.start.line); + + const sorted = [...lineNumbers].sort((a, b) => a - b); + + assert.deepStrictEqual( + lineNumbers, + sorted, + "Lint diagnostics should be sorted by line number" + ); + }); }); diff --git a/test/vscode/fixtures/lint-order.schema.json b/test/vscode/fixtures/lint-order.schema.json new file mode 100644 index 0000000..cba505d --- /dev/null +++ b/test/vscode/fixtures/lint-order.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + + "type": "object", + + "properties": { + "role": { + "enum": ["admin", "user"], + "type": "string" + }, + + "preferences": { + "type": "object", + "properties": { + "theme": { + "enum": ["dark", "light"], + "type": "string" + } + } + } + }, + + "required": ["id", "role"] +} diff --git a/test/vscode/package-lock.json b/test/vscode/package-lock.json index 9ffaf01..ae17db0 100644 --- a/test/vscode/package-lock.json +++ b/test/vscode/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@sourcemeta/studio-tests", + "name": "@sourcemeta/studio-test-vscode", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@sourcemeta/studio-tests", + "name": "@sourcemeta/studio-test-vscode", "version": "0.0.0", "devDependencies": { "@types/chai": "^5.2.3", diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 6797de2..8fd7b34 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -5,7 +5,7 @@ import { PanelManager } from './panel/PanelManager'; import { CommandExecutor } from './commands/CommandExecutor'; import { DiagnosticManager } from './diagnostics/DiagnosticManager'; import { getFileInfo, parseLintResult, parseMetaschemaResult, errorPositionToRange, parseCliError, hasJsonParseErrors } from './utils/fileUtils'; -import { PanelState, WebviewToExtensionMessage } from '../../protocol/types'; +import { LintError, PanelState, WebviewToExtensionMessage } from '../../protocol/types'; import { DiagnosticType } from './types'; let panelManager: PanelManager; @@ -35,8 +35,8 @@ export async function activate(context: vscode.ExtensionContext): Promise // Only disable validation if a workspace is open to avoid changing global user settings if (vscode.workspace.workspaceFolders) { await vscode.workspace.getConfiguration('json').update( - 'validate.enable', - false, + 'validate.enable', + false, vscode.ConfigurationTarget.Workspace ); } @@ -105,7 +105,7 @@ function handleWebviewMessage(message: WebviewToExtensionMessage): void { } vscode.window.showTextDocument(lastActiveTextEditor.document, showOptions).then((editor) => { editor.selection = new vscode.Selection(range.start, range.end); - + editor.revealRange(range, vscode.TextEditorRevealType.InCenter); }); } else if (message.command === 'openExternal' && message.url) { @@ -113,7 +113,7 @@ function handleWebviewMessage(message: WebviewToExtensionMessage): void { } else if (message.command === 'formatSchema' && lastActiveTextEditor) { const filePath = lastActiveTextEditor.document.uri.fsPath; const fileInfo = getFileInfo(filePath); - + if (!fileInfo || !panelManager.exists() || !currentPanelState) { return; } @@ -138,14 +138,14 @@ function handleWebviewMessage(message: WebviewToExtensionMessage): void { if (lastActiveTextEditor) { await vscode.window.showTextDocument(lastActiveTextEditor.document, lastActiveTextEditor.viewColumn); } - + // Wait for Huge schemas to reload after formatting await new Promise(resolve => setTimeout(resolve, 300)); await updatePanelContent(); }).catch((error) => { let errorMessage = error.message; - + // Try to parse JSON error from CLI const cliError = parseCliError(error.message); if (cliError) { @@ -157,7 +157,7 @@ function handleWebviewMessage(message: WebviewToExtensionMessage): void { } } } - + vscode.window.showErrorMessage(`Format failed: ${errorMessage}`); if (currentPanelState) { const updatedState = { @@ -217,7 +217,7 @@ function handleActiveEditorChange(editor: vscode.TextEditor | undefined): void { * Handle document save events */ function handleDocumentSave(document: vscode.TextDocument): void { - if (panelManager.exists() && lastActiveTextEditor && + if (panelManager.exists() && lastActiveTextEditor && document.uri.fsPath === lastActiveTextEditor.document.uri.fsPath) { const fileInfo = getFileInfo(document.uri.fsPath); // Only refresh if it's a JSON/YAML file @@ -227,6 +227,26 @@ function handleDocumentSave(document: vscode.TextDocument): void { } } +/** + * Sort the Linting Errors by line location + */ +function sortLintErrorsByLocation(errors: LintError[]): LintError[] { + return [...errors].sort((a, b) => { + if (!a.position && !b.position) return 0; + if (!a.position) return 1; + if (!b.position) return -1; + + const [aLine, aColumn] = a.position; + const [bLine, bColumn] = b.position; + + if (aLine !== bLine) { + return aLine - bLine; + } + + return aColumn - bColumn; + }); +} + /** * Update the panel content with current file analysis */ @@ -313,6 +333,11 @@ async function updatePanelContent(): Promise { const lintResult = parseLintResult(lintOutput); + if (lintResult.errors && lintResult.errors.length > 0) { + // TODO: Consider moving lint diagnostic ordering to the jsonschema CLI + lintResult.errors = sortLintErrorsByLocation(lintResult.errors); + } + const parseErrors = hasJsonParseErrors(lintResult, metaschemaResult); const finalState: PanelState = {