From 7df95b8be67fccd41a1e1f0f7f507b1a3fc5929d Mon Sep 17 00:00:00 2001 From: Ulysse Bouchet Date: Wed, 22 Apr 2026 14:50:36 +0200 Subject: [PATCH 1/4] fix: support base-type R/DC/RC entries in export files Parse, format, and syntax-highlight lines headed by R (base directories) with DC/RC status flags alongside the existing F/D/R format, so export files using base entries round-trip correctly. --- src/ExportEditor.ts | 6 +++--- src/ExportFormatter.ts | 9 +++++++-- syntaxes/export.tmLanguage.json | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/ExportEditor.ts b/src/ExportEditor.ts index 1989c79..010ac1a 100644 --- a/src/ExportEditor.ts +++ b/src/ExportEditor.ts @@ -325,16 +325,16 @@ export class ExportEditor implements vscode.Disposable { } } - if (tokens[0] === 'F' && tokens.length === 5) { + if ((tokens[0] === 'F' || tokens[0] === 'R') && tokens.length === 5) { const [, type, name, ioFlag, unit] = tokens; const fileObj: FileDescriptor = { type: type, name: name, unit: unit, }; - if (ioFlag === 'D') { + if (ioFlag === 'D' || ioFlag === 'DC') { formData.inputFiles.push(fileObj); - } else if (ioFlag === 'R') { + } else if (ioFlag === 'R' || ioFlag === 'RC') { formData.outputFiles.push(fileObj); } } diff --git a/src/ExportFormatter.ts b/src/ExportFormatter.ts index e473235..ddf1d3b 100644 --- a/src/ExportFormatter.ts +++ b/src/ExportFormatter.ts @@ -4,6 +4,7 @@ interface Entry { comments: string[]; line: string; type: string; + head?: 'F' | 'R'; direction?: 'D' | 'R'; } @@ -29,6 +30,9 @@ function typeRank(type: string): number { } function byType(a: Entry, b: Entry): number { + if (a.head !== b.head) { + return a.head === 'F' ? -1 : 1; + } const pa = typeRank(a.type); const pb = typeRank(b.type); if (pa !== pb) { @@ -90,12 +94,13 @@ export function formatExportContent(text: string, filename?: string): string { if (head === 'P') { pEntries.push({ comments: pendingComments, line: trimmed, type: '' }); pendingComments = []; - } else if (head === 'F') { - const direction: 'D' | 'R' = tokens[3] === 'D' ? 'D' : 'R'; + } else if (head === 'F' || head === 'R') { + const direction: 'D' | 'R' = tokens[3] === 'D' || tokens[3] === 'DC' ? 'D' : 'R'; fEntries.push({ comments: pendingComments, line: trimmed, type: tokens[1] ?? '', + head: head as 'F' | 'R', direction, }); pendingComments = []; diff --git a/syntaxes/export.tmLanguage.json b/syntaxes/export.tmLanguage.json index c5ea6a4..e347f2a 100644 --- a/syntaxes/export.tmLanguage.json +++ b/syntaxes/export.tmLanguage.json @@ -30,7 +30,7 @@ ] }, "file-line": { - "begin": "^\\s*(F)\\s+(\\S+)\\s+(\\S+)\\s+(?:(D)|(RC?))\\s+(\\S+)", + "begin": "^\\s*([FR])\\s+(\\S+)\\s+(\\S+)\\s+(?:(DC?)|(RC?))\\s+(\\S+)", "end": "$", "beginCaptures": { "1": { "name": "keyword.control.file.export" }, From b36913d976047018d95eb3f5bbec20fa87517da7 Mon Sep 17 00:00:00 2001 From: Ulysse Bouchet Date: Wed, 22 Apr 2026 14:50:53 +0200 Subject: [PATCH 2/4] feat: rework export webview form UX Split the file name column into separate basename and extension inputs so the extension is prefilled from the chosen type and stays independently editable. Default a new file's unit to the next multiple of ten above the highest existing unit when the type is not yet present. Show per-field errors with clickable rows in a unified error panel, make warnings clickable by jumping to the most relevant input, and add hover tints to the submit-bar pills. --- webviews/export/src/components/App.svelte | 208 ++++++++++++------ webviews/export/src/components/FileRow.svelte | 61 ++++- .../export/src/components/FileSection.svelte | 1 + .../export/src/components/SubmitBar.svelte | 23 +- webviews/export/src/lib/types.ts | 55 +++-- 5 files changed, 259 insertions(+), 89 deletions(-) diff --git a/webviews/export/src/components/App.svelte b/webviews/export/src/components/App.svelte index e1a3199..0694b02 100644 --- a/webviews/export/src/components/App.svelte +++ b/webviews/export/src/components/App.svelte @@ -58,28 +58,36 @@ const errors: Record = {}; if (formData.name.trim() === '') { - errors['envName'] = 'File name is required.'; + errors['envName'] = 'The export file name is required.'; } for (const [key, value] of Object.entries(formData.parameters)) { if (!isInteger(value)) { - errors[key] = `"${key}" must be an integer.`; + errors[key] = `"${key}" must be a whole number.`; } } const checkFiles = (files: FileDescriptor[], label: 'Input' | 'Output') => { - files.forEach((f) => { + files.forEach((f, idx) => { if (isEmptyFile(f)) { return; } - if (!f.name.trim()) { - errors[`name-${f.id}`] = `${label} file: missing file name.`; - } + const prefix = `${label} file #${idx + 1}`; + const trimmedName = f.name.trim(); + const dotIdx = trimmedName.lastIndexOf('.'); + const base = dotIdx >= 0 ? trimmedName.slice(0, dotIdx) : trimmedName; + const ext = dotIdx >= 0 ? trimmedName.slice(dotIdx + 1) : ''; if (!f.type.trim()) { - errors[`type-${f.id}`] = `${label} file: missing type.`; + errors[`type-${f.id}`] = `${prefix}: file type is required.`; + } + if (!base) { + errors[`name-${f.id}`] = `${prefix}: file name is required.`; + } + if (!ext) { + errors[`ext-${f.id}`] = `${prefix}: file extension is required.`; } if (!isInteger(f.unit)) { - errors[`unit-${f.id}`] = 'Unit must be an integer.'; + errors[`unit-${f.id}`] = `${prefix}: unit must be a whole number.`; } }); }; @@ -98,8 +106,22 @@ return out; }); + let allErrors = $derived<{ targetId?: string; message: string }[]>([ + ...Object.entries(errorFor).map(([targetId, message]) => ({ targetId, message })), + ...formErrors.map((message) => ({ message })), + ]); + + function focusTarget(id: string) { + const el = document.getElementById(id); + if (!el) { + return; + } + el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + (el as HTMLElement).focus({ preventScroll: true }); + } + let warnings = $derived.by(() => { - const out: string[] = []; + const out: { targetId?: string; message: string }[] = []; if ( mode === 'edit' && @@ -107,9 +129,10 @@ formData.name.trim() && formData.name.trim() !== originalName ) { - out.push( - `Saving will rename ${originalName}.export to ${formData.name.trim()}.export. The original file will be deleted.` - ); + out.push({ + targetId: 'envName', + message: `Saving will rename ${originalName}.export to ${formData.name.trim()}.export. The original file will be deleted.`, + }); } const meshTypes = new Set(['mmed', 'mail', 'msh']); @@ -117,15 +140,21 @@ (f) => !isEmptyFile(f) && meshTypes.has(f.type) ); if (!hasMesh) { - out.push('No mesh file is set (mmed, mail, or msh).'); + out.push({ + targetId: 'add-input', + message: 'No mesh file is set (mmed, mail, or msh).', + }); } const hasRmed = formData.outputFiles.some((f) => !isEmptyFile(f) && f.type === 'rmed'); if (!hasRmed) { - out.push('No rmed output file is set.'); + out.push({ + targetId: 'add-output', + message: 'No rmed output file is set.', + }); } - const byUnit = new Map(); + const byUnit = new Map(); for (const f of [...formData.inputFiles, ...formData.outputFiles]) { if (isEmptyFile(f)) { continue; @@ -138,13 +167,19 @@ continue; } const label = f.name.trim() || `(unnamed ${f.type || 'file'})`; - const existing = byUnit.get(unitStr) ?? []; - existing.push(label); - byUnit.set(unitStr, existing); + const existing = byUnit.get(unitStr); + if (existing) { + existing.names.push(label); + } else { + byUnit.set(unitStr, { names: [label], firstId: f.id }); + } } - for (const [unit, names] of byUnit) { + for (const [unit, { names, firstId }] of byUnit) { if (names.length > 1) { - out.push(`Multiple files share unit ${unit}: ${names.join(', ')}`); + out.push({ + targetId: `unit-${firstId}`, + message: `Multiple files share unit ${unit}: ${names.join(', ')}`, + }); } } return out; @@ -205,13 +240,17 @@ if (isEmptyFile(f)) { continue; } - lines.push(`F ${f.type} ${f.name.trim()} D ${f.unit.trim()}`); + const head = f.type === 'base' ? 'R' : 'F'; + const status = f.type === 'base' ? 'DC' : 'D'; + lines.push(`${head} ${f.type} ${f.name.trim()} ${status} ${f.unit.trim()}`); } for (const f of formData.outputFiles) { if (isEmptyFile(f)) { continue; } - lines.push(`F ${f.type} ${f.name.trim()} R ${f.unit.trim()}`); + const head = f.type === 'base' ? 'R' : 'F'; + const status = f.type === 'base' ? 'RC' : 'R'; + lines.push(`${head} ${f.type} ${f.name.trim()} ${status} ${f.unit.trim()}`); } vscode?.postMessage({ command: 'result', value: lines.join('\n') }); } @@ -445,34 +484,45 @@ onRemove={removeFile} /> - {#if formErrors.length > 0} + {#if allErrors.length > 0} +
    + {#each allErrors as e} +
  • +
  • {/each}
@@ -482,31 +532,44 @@ {#if warnings.length > 0} - {#if nameError || unitError || typeError} -

- {nameError || typeError || unitError} -

+ {#if rowErrors.length > 0} +
    + {#each rowErrors as message} +
  • {message}
  • + {/each} +
{/if} diff --git a/webviews/export/src/components/FileSection.svelte b/webviews/export/src/components/FileSection.svelte index 2ac1b77..9b2cf46 100644 --- a/webviews/export/src/components/FileSection.svelte +++ b/webviews/export/src/components/FileSection.svelte @@ -80,6 +80,7 @@ + + ({ value: v, label: v })), + ]} + value={testlist.category || null} + onSelect={(v) => (testlist.category = v as Category)} + > + + + + + +