diff --git a/src/activationHelpers/contextAware/commands.ts b/src/activationHelpers/contextAware/commands.ts index a12c95a3a..990b6241f 100644 --- a/src/activationHelpers/contextAware/commands.ts +++ b/src/activationHelpers/contextAware/commands.ts @@ -10,7 +10,7 @@ import { } from "../../utils/codexNotebookUtils"; import { jumpToCellInNotebook } from "../../utils"; import { setTargetFont } from "../../projectManager/projectInitializers"; -import { CodexExportFormat, exportCodexContent } from "../../exportHandler/exportHandler"; +import { CodexExportFormat, exportCodexContent, type ExportOptions } from "../../exportHandler/exportHandler"; import { createEditAnalysisProvider } from "../../providers/EditAnalysisView/EditAnalysisViewProvider"; @@ -158,7 +158,7 @@ export async function registerCommands(context: vscode.ExtensionContext) { format: CodexExportFormat; userSelectedPath: string; filesToExport: string[]; - options?: { skipValidation?: boolean; removeIds?: boolean; }; + options?: ExportOptions; }) => { await exportCodexContent(format, userSelectedPath, filesToExport, options); } diff --git a/src/exportHandler/audioExporter.ts b/src/exportHandler/audioExporter.ts index fabe12a90..c3385f552 100644 --- a/src/exportHandler/audioExporter.ts +++ b/src/exportHandler/audioExporter.ts @@ -9,6 +9,12 @@ import { getFFmpegPath } from "../utils/ffmpegManager"; const execAsync = promisify(exec); +let extensionContext: vscode.ExtensionContext | undefined; + +export const initializeAudioExporter = (context: vscode.ExtensionContext): void => { + extensionContext = context; +}; + // Debug logging for audio export diagnostics const DEBUG = false; function debug(...args: any[]) { @@ -29,11 +35,6 @@ function sanitizeFileComponent(input: string): string { .replace(/_+/g, "_"); } -function formatDateForFolder(d: Date): string { - const pad = (n: number, w = 2) => String(n).padStart(w, "0"); - return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`; -} - // REMOVE: This doesn't seem to be used anywhere /** * Parses a cell reference ID (from globalReferences) to extract book, chapter, and verse. @@ -358,7 +359,7 @@ async function convertToWav( originalExt: string, sampleRate: number = 48000 ): Promise { - const ffmpegBinaryPath = await getFFmpegPath(); + const ffmpegBinaryPath = await getFFmpegPath(extensionContext); if (!ffmpegBinaryPath) { throw new Error("FFmpeg not available"); } @@ -481,17 +482,8 @@ export async function exportAudioAttachments( } const workspaceFolder = workspaceFolders[0]; - // Resolve project name - const projectConfig = vscode.workspace.getConfiguration("codex-project-manager"); - let projectName = projectConfig.get("projectName", ""); - if (!projectName) { - projectName = basename(workspaceFolder.uri.fsPath); - } - - const dateStamp = formatDateForFolder(new Date()); - const exportRoot = vscode.Uri.file(userSelectedPath); - const finalExportDir = vscode.Uri.joinPath(exportRoot, "export", `${sanitizeFileComponent(projectName)}-${dateStamp}`); - await vscode.workspace.fs.createDirectory(finalExportDir); + const exportDir = vscode.Uri.file(userSelectedPath); + await vscode.workspace.fs.createDirectory(exportDir); const includeTimestamps = !!options?.includeTimestamps; const selectedFiles = filesToExport.map((p) => vscode.Uri.file(p)); @@ -516,7 +508,7 @@ export async function exportAudioAttachments( progress.report({ message: `Processing ${basename(file.fsPath)} (${index + 1}/${selectedFiles.length})`, increment }); const bookCode = basename(file.fsPath).split(".")[0] || "BOOK"; - const bookFolder = vscode.Uri.joinPath(finalExportDir, sanitizeFileComponent(bookCode)); + const bookFolder = vscode.Uri.joinPath(exportDir, sanitizeFileComponent(bookCode)); await vscode.workspace.fs.createDirectory(bookFolder); let notebook: CodexNotebookAsJSONData; @@ -629,7 +621,7 @@ export async function exportAudioAttachments( } debug(`Export summary: ${copiedCount} files copied, ${missingCount} skipped`); - vscode.window.showInformationMessage(`Audio export completed: ${copiedCount} files copied${missingCount ? `, ${missingCount} skipped` : ""}. Output: ${finalExportDir.fsPath}`); + vscode.window.showInformationMessage(`Audio export completed: ${copiedCount} files copied${missingCount ? `, ${missingCount} skipped` : ""}. Output: ${exportDir.fsPath}`); } ); } diff --git a/src/exportHandler/exportHandler.ts b/src/exportHandler/exportHandler.ts index aa429f2e0..278dbf649 100644 --- a/src/exportHandler/exportHandler.ts +++ b/src/exportHandler/exportHandler.ts @@ -1,5 +1,4 @@ import * as vscode from "vscode"; -import JSZip from "jszip"; import { CodexCellTypes } from "../../types/enums"; import { basename } from "path"; import * as path from "path"; @@ -8,6 +7,7 @@ import { exec } from "child_process"; import { promisify } from "util"; import { removeHtmlTags, generateSrtData } from "./subtitleUtils"; import { generateVttData } from "./vttUtils"; + // import { exportRtfWithPandoc } from "../../webviews/codex-webviews/src/NewSourceUploader/importers/rtf/pandocNodeBridge"; const execAsync = promisify(exec); @@ -215,6 +215,8 @@ export enum CodexExportFormat { export interface ExportOptions { skipValidation?: boolean; removeIds?: boolean; + includeAudio?: boolean; + includeTimestamps?: boolean; } // IDML Round-trip export: Uses idmlExporter or biblicaExporter based on filename @@ -1608,65 +1610,80 @@ export async function exportCodexContent( filesToExport: string[], options?: ExportOptions ) { - // Check if audio export should also be included alongside the text format export - const includeAudio = (options as any)?.includeAudio === true && format !== CodexExportFormat.AUDIO; + const includeAudio = options?.includeAudio === true && format !== CodexExportFormat.AUDIO; + const isMulti = includeAudio; + + // Always create a wrapper folder + const projectConfig = vscode.workspace.getConfiguration("codex-project-manager"); + const projectName = projectConfig.get("projectName", "") || + basename(vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? "export"); + const dateStamp = new Date().toISOString().slice(0, 10); + const formatLabel = isMulti ? "multi" : format; + const baseName = `${projectName}-${formatLabel}-${dateStamp}`; + let candidate = path.join(userSelectedPath, baseName); + let suffix = 1; + while (fs.existsSync(candidate)) { + candidate = path.join(userSelectedPath, `${baseName}-${suffix}`); + suffix++; + } + const wrapperPath = candidate; + + // In multi mode, each export type gets its own subfolder + const formatPath = isMulti ? path.join(wrapperPath, format) : wrapperPath; + const audioPath = isMulti ? path.join(wrapperPath, "audio") : wrapperPath; - // Prepare export promises const exportPromises: Promise[] = []; - // Add text format export switch (format) { case CodexExportFormat.PLAINTEXT: - exportPromises.push(exportCodexContentAsPlaintext(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsPlaintext(formatPath, filesToExport, options)); break; case CodexExportFormat.USFM: - exportPromises.push(exportCodexContentAsUsfm(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsUsfm(formatPath, filesToExport, options)); break; case CodexExportFormat.HTML: - exportPromises.push(exportCodexContentAsHtml(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsHtml(formatPath, filesToExport, options)); break; case CodexExportFormat.AUDIO: { const { exportAudioAttachments } = await import("./audioExporter"); - exportPromises.push(exportAudioAttachments(userSelectedPath, filesToExport, { includeTimestamps: (options as any)?.includeTimestamps })); + exportPromises.push(exportAudioAttachments(wrapperPath, filesToExport, { includeTimestamps: options?.includeTimestamps })); break; } case CodexExportFormat.SUBTITLES_VTT_WITH_STYLES: - exportPromises.push(exportCodexContentAsSubtitlesVtt(userSelectedPath, filesToExport, options, true)); + exportPromises.push(exportCodexContentAsSubtitlesVtt(formatPath, filesToExport, options, true)); break; case CodexExportFormat.SUBTITLES_VTT_WITHOUT_STYLES: - exportPromises.push(exportCodexContentAsSubtitlesVtt(userSelectedPath, filesToExport, options, false)); + exportPromises.push(exportCodexContentAsSubtitlesVtt(formatPath, filesToExport, options, false)); break; case CodexExportFormat.SUBTITLES_SRT: - exportPromises.push(exportCodexContentAsSubtitlesSrt(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsSubtitlesSrt(formatPath, filesToExport, options)); break; case CodexExportFormat.XLIFF: - exportPromises.push(exportCodexContentAsXliff(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsXliff(formatPath, filesToExport, options)); break; case CodexExportFormat.CSV: - exportPromises.push(exportCodexContentAsCsv(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsCsv(formatPath, filesToExport, options)); break; case CodexExportFormat.TSV: - exportPromises.push(exportCodexContentAsTsv(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsTsv(formatPath, filesToExport, options)); break; case CodexExportFormat.REBUILD_EXPORT: - exportPromises.push(exportCodexContentAsRebuild(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsRebuild(formatPath, filesToExport, options)); break; case CodexExportFormat.BACKTRANSLATIONS: - exportPromises.push(exportCodexContentAsBacktranslations(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsBacktranslations(formatPath, filesToExport, options)); break; } - // Add audio export if requested alongside text format if (includeAudio) { const { exportAudioAttachments } = await import("./audioExporter"); exportPromises.push( - exportAudioAttachments(userSelectedPath, filesToExport, { - includeTimestamps: (options as any)?.includeTimestamps + exportAudioAttachments(audioPath, filesToExport, { + includeTimestamps: options?.includeTimestamps }) ); } - // Execute all exports in parallel await Promise.all(exportPromises); } diff --git a/src/extension.ts b/src/extension.ts index 716bfc949..6b8b7a6fe 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -62,6 +62,7 @@ import { import { initializeAudioProcessor } from "./utils/audioProcessor"; import { initializeAudioMerger } from "./utils/audioMerger"; import { initializeAudioExtractor } from "./utils/audioExtractor"; +import { initializeAudioExporter } from "./exportHandler/audioExporter"; import { checkTools, getUnavailableTools } from "./utils/toolsManager"; import { initToolPreferences, setNativeGitAvailable, getGitToolMode, getSqliteToolMode } from "./utils/toolPreferences"; import { downloadFFmpeg } from "./utils/ffmpegManager"; @@ -322,6 +323,7 @@ export async function activate(context: vscode.ExtensionContext) { initializeAudioProcessor(context); initializeAudioMerger(context); initializeAudioExtractor(context); + initializeAudioExporter(context); // Register and show splash screen immediately before anything else try { diff --git a/src/projectManager/projectExportView.ts b/src/projectManager/projectExportView.ts index b2347afd0..db6aa375b 100644 --- a/src/projectManager/projectExportView.ts +++ b/src/projectManager/projectExportView.ts @@ -1,4 +1,5 @@ import { CodexExportFormat } from "../exportHandler/exportHandler"; +import * as fs from "fs"; import * as vscode from "vscode"; import { safePostMessageToPanel } from "../utils/webviewUtils"; import { @@ -7,6 +8,26 @@ import { type FileGroup, } from "./utils/exportViewUtils"; +const LAST_EXPORT_FOLDER_KEY = "projectExport.lastFolder"; + +function getLastExportFolderUri(context: vscode.ExtensionContext): vscode.Uri | undefined { + const lastPath = context.workspaceState.get(LAST_EXPORT_FOLDER_KEY); + if (!lastPath) { + return undefined; + } + try { + if (!fs.existsSync(lastPath)) { + return undefined; + } + if (!fs.statSync(lastPath).isDirectory()) { + return undefined; + } + return vscode.Uri.file(lastPath); + } catch { + return undefined; + } +} + export async function openProjectExportView(context: vscode.ExtensionContext) { const panel = vscode.window.createWebviewPanel( "projectExportView", @@ -35,27 +56,32 @@ export async function openProjectExportView(context: vscode.ExtensionContext) { const codexFiles = await vscode.workspace.findFiles("**/*.codex"); const fileGroups = await groupCodexFilesByImporterType(codexFiles); + const initialExportFolder = getLastExportFolderUri(context)?.fsPath ?? null; panel.webview.html = getWebviewContent( sourceLanguage, targetLanguage, codiconsUri, - fileGroups + fileGroups, + initialExportFolder ); panel.webview.onDidReceiveMessage(async (message) => { let result: vscode.Uri[] | undefined; switch (message.command) { - case "selectExportPath": + case "selectExportPath": { + const defaultUri = getLastExportFolderUri(context); result = await vscode.window.showOpenDialog({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, title: "Select Export Location", openLabel: "Select Folder", + ...(defaultUri ? { defaultUri } : {}), }); if (result && result[0]) { + await context.workspaceState.update(LAST_EXPORT_FOLDER_KEY, result[0].fsPath); safePostMessageToPanel( panel, { @@ -66,6 +92,7 @@ export async function openProjectExportView(context: vscode.ExtensionContext) { ); } break; + } case "openProjectSettings": await vscode.commands.executeCommand( "codex-project-manager.openProjectSettings" @@ -107,12 +134,14 @@ function getWebviewContent( sourceLanguage: unknown, targetLanguage: unknown, codiconsUri: vscode.Uri, - fileGroups: FileGroup[] + fileGroups: FileGroup[], + initialExportFolder: string | null ) { const hasLanguages = sourceLanguage && targetLanguage; const groupsJson = JSON.stringify(fileGroups); const exportOptionsConfigJson = JSON.stringify(EXPORT_OPTIONS_BY_FILE_TYPE); + const initialExportFolderJson = JSON.stringify(initialExportFolder); return ` @@ -185,7 +214,7 @@ function getWebviewContent( border-radius: 4px; cursor: pointer; display: flex; - align-items: center; + align-items: flex-start; gap: 12px; } .format-option:hover { background-color: var(--vscode-list-hoverBackground); } @@ -269,12 +298,17 @@ function getWebviewContent( background-color: var(--vscode-editor-background); border-top: 1px solid var(--vscode-input-border); } - .format-section-content .format-option { margin-bottom: 8px; padding: 12px; } - .format-section-content .format-option:last-child { margin-bottom: 0; } + .format-section-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + } + .format-section-content .format-option { padding: 12px; } .format-option-row { display: flex; gap: 1rem; } .format-option[data-option].hidden { display: none !important; } .format-section[data-option].hidden { display: none !important; } .format-option-row[data-option].hidden { display: none !important; } + .format-option p, .format-option-content p { line-height: 1.45; margin: 4px 0 0 0; } .format-option-content { display: flex; flex-direction: column; gap: 4px; } .format-tag { display: inline-block; @@ -342,23 +376,50 @@ function getWebviewContent(
-

Audio Export

-
-
- -
- Audio -

Export per-cell audio attachments alongside the selected export format

-
- - +

Select Export Format

+
+ +
+
+ +

Text and Markup Export Options

+
+
+
+
+ Generate Plaintext +

Export as plain text files with minimal formatting

+
+
+
+
+ Generate XLIFF +

Export in XML Localization Interchange File Format (XLIFF) for translation workflows

+ Translation Ready +
+
+
+
+ Generate USFM +

Export in Universal Standard Format Markers

+
+
+
+
+ Generate USFM Without Validation +

Skip USFM validation for a faster export

+ May produce invalid USFM +
+
+
+
+ Generate HTML +

Export as web pages with chapter navigation

+
-
-

Select Export Format

-
- +
@@ -413,43 +474,6 @@ function getWebviewContent(
- -
-
- -
- Generate Plaintext -

Export as plain text files with minimal formatting

-
-
-
- -
- Generate XLIFF -

Export in XML Localization Interchange File Format (XLIFF) for translation workflows

- Translation Ready -
-
-
- -
-
- -
- Generate USFM -

Export in Universal Standard Format Markers

-
-
-
- -
- Generate HTML -

Export as web pages with chapter navigation

-
-
-
- -
@@ -481,12 +505,28 @@ function getWebviewContent(
-
- @@ -505,6 +545,7 @@ function getWebviewContent( Select Location
+
` @@ -525,8 +566,8 @@ function getWebviewContent( const exportOptionsConfig = ${exportOptionsConfigJson}; let currentStep = 1; let selectedFormat = null; - let selectedAudio = false; - let exportPath = null; + let selectedAudioMode = null; // null | 'audio' | 'audio-timestamps' + let exportPath = ${initialExportFolderJson}; let selectedFiles = new Set(); let selectedGroupKey = null; @@ -627,7 +668,7 @@ function getWebviewContent( if (btn) btn.disabled = selectedFiles.size === 0; } - function initStep2Options() { + function initStep2Options(resetFormatSelection) { const key = selectedGroupKey || 'unknown'; const show = (option) => { const allowed = exportOptionsConfig[option]; @@ -639,16 +680,17 @@ function getWebviewContent( const visible = show(opt); el.classList.toggle('hidden', !visible); }); - // Visibility for HTML/USFM is controlled by data-option and exportOptionsConfig - selectedFormat = null; - // Clear previous selected state for format options but keep audio selection intact - document.querySelectorAll('#step2 .format-option').forEach(opt => { - if (opt.id === 'audio-format' || opt.dataset.format === 'audio') return; - opt.classList.remove('selected'); - // remove any inline visual overrides that may have been applied - opt.style.backgroundColor = ''; - opt.style.borderColor = ''; - }); + // Only clear text format when entering step 2 from step 1 (file group may have changed). + // When returning from step 3, keep the user's format choice; audio already behaved this way. + if (resetFormatSelection) { + selectedFormat = null; + document.querySelectorAll('#step2 .format-option:not(.audio-option)').forEach(opt => { + opt.classList.remove('selected'); + opt.style.backgroundColor = ''; + opt.style.borderColor = ''; + }); + + } updateStep2Button(); } @@ -676,6 +718,7 @@ function getWebviewContent( } function goToStep(n) { + const prevStep = currentStep; document.querySelectorAll('.step-panel').forEach(p => p.classList.remove('active')); document.getElementById('step' + n).classList.add('active'); document.querySelectorAll('.step-dot').forEach((dot, i) => { @@ -686,7 +729,7 @@ function getWebviewContent( currentStep = n; updateButtonVisibility(); if (n === 2) { - initStep2Options(); + initStep2Options(prevStep === 1); } else if (n === 3) { updateExportButton(); } @@ -699,13 +742,13 @@ function getWebviewContent( function updateStep2Button() { const btn = document.getElementById('nextStep2'); // Allow moving forward if a format is selected OR audio-only is selected - if (btn) btn.disabled = !(selectedFormat || selectedAudio); + if (btn) btn.disabled = !(selectedFormat || selectedAudioMode); } function updateExportButton() { const btn = document.getElementById('exportButton'); // Export allowed if a format or audio is selected - const hasAnyFormat = !!(selectedFormat || selectedAudio); + const hasAnyFormat = !!(selectedFormat || selectedAudioMode); if (btn) btn.disabled = !hasAnyFormat || !exportPath || selectedFiles.size === 0; } @@ -734,73 +777,72 @@ function getWebviewContent( document.addEventListener('DOMContentLoaded', () => { renderFileGroups(); updateStep1Button(); + if (exportPath) { + const pathEl = document.getElementById('exportPath'); + if (pathEl) pathEl.textContent = exportPath; + } - document.querySelectorAll('#step2 .format-option').forEach(option => { + // Audio option click handler (mutually exclusive toggle) + document.querySelectorAll('#step2 .audio-option').forEach(option => { option.addEventListener('click', (e) => { if (e.target.closest('.format-section-header')) return; - // If this is the audio block, toggle audio selection instead of treating as format - if (option.id === 'audio-format' || option.dataset.format === 'audio') { - const willSelect = !selectedAudio; - if (willSelect) { - option.classList.add('selected'); - try { - const root = document.documentElement; - const bg = getComputedStyle(root).getPropertyValue('--vscode-list-activeSelectionBackground') || ''; - const border = getComputedStyle(root).getPropertyValue('--vscode-focusBorder') || ''; - if (bg) option.style.backgroundColor = bg.trim(); - if (border) option.style.borderColor = border.trim(); - } catch (e) { } - } else { - option.classList.remove('selected'); - option.style.backgroundColor = ''; - option.style.borderColor = ''; - } - selectedAudio = willSelect; - // Refresh button states immediately when audio toggled - try { updateStep2Button(); updateExportButton(); } catch (e) {} - return; + const mode = option.dataset.audioMode; + const wasSelected = selectedAudioMode === mode; + // Clear all audio options + document.querySelectorAll('#step2 .audio-option').forEach(opt => { + opt.classList.remove('selected'); + opt.style.backgroundColor = ''; + opt.style.borderColor = ''; + }); + if (wasSelected) { + selectedAudioMode = null; + } else { + selectedAudioMode = mode; + option.classList.add('selected'); } + try { updateStep2Button(); updateExportButton(); } catch (e) {} + }); + }); + + // Format option click handler (non-audio) + document.querySelectorAll('#step2 .format-option:not(.audio-option)').forEach(option => { + option.addEventListener('click', (e) => { + if (e.target.closest('.format-section-header')) return; // If clicking the already-selected format, deselect it if (option.classList.contains('selected')) { option.classList.remove('selected'); selectedFormat = null; - // hide any USFM-specific options - const usfmOptions = document.getElementById('usfmOptions'); - if (usfmOptions) usfmOptions.style.display = 'none'; updateStep2Button(); return; } // Select this format and clear other non-audio format selections - document.querySelectorAll('#step2 .format-option').forEach(opt => { - if (opt.id === 'audio-format' || opt.dataset.format === 'audio') return; + document.querySelectorAll('#step2 .format-option:not(.audio-option)').forEach(opt => { opt.classList.remove('selected'); opt.style.backgroundColor = ''; opt.style.borderColor = ''; }); option.classList.add('selected'); selectedFormat = option.dataset.format; - const usfmOptions = document.getElementById('usfmOptions'); - if (usfmOptions) usfmOptions.style.display = selectedFormat === 'usfm' ? 'block' : 'none'; updateStep2Button(); }); }); - // audio toggling handled in the format-option click handler above; no separate toggleAudio needed - - // audio-format is handled by the general format-option click handler above - }); function exportProject() { - const formatToSend = selectedFormat || (selectedAudio ? 'audio' : null); + let formatToSend = selectedFormat || (selectedAudioMode ? 'audio' : null); if (!formatToSend || !exportPath || selectedFiles.size === 0) return; const options = {}; - if (formatToSend === 'usfm') options.skipValidation = document.getElementById('skipValidation')?.checked; - // Audio is now a separate toggle that may be combined with other export formats - if (selectedAudio) options.includeAudio = true; - if (selectedAudio) options.includeTimestamps = document.getElementById('audioIncludeTimestamps')?.checked; + if (formatToSend === 'usfm-no-validate') { + formatToSend = 'usfm'; + options.skipValidation = true; + } + if (selectedAudioMode) { + options.includeAudio = true; + options.includeTimestamps = selectedAudioMode === 'audio-timestamps'; + } vscode.postMessage({ command: 'export', format: formatToSend, diff --git a/src/projectManager/utils/projectUtils.ts b/src/projectManager/utils/projectUtils.ts index 1f1aef625..878bc82bb 100644 --- a/src/projectManager/utils/projectUtils.ts +++ b/src/projectManager/utils/projectUtils.ts @@ -1074,7 +1074,41 @@ export async function syncMetadataToConfiguration() { debug("No valid validationCountAudio found in metadata"); } - // Add other metadata properties sync here as needed + // Sync sourceLanguage and targetLanguage from metadata to config + if (Array.isArray(metadata.languages) && metadata.languages.length > 0) { + const metadataSource = metadata.languages.find( + (lang: LanguageMetadata) => lang.projectStatus === LanguageProjectStatus.SOURCE + ); + const metadataTarget = metadata.languages.find( + (lang: LanguageMetadata) => lang.projectStatus === LanguageProjectStatus.TARGET + ); + + if (metadataSource) { + const currentSource = config.get("sourceLanguage") as LanguageMetadata | undefined; + const needsUpdate = !currentSource + || !currentSource.tag + || currentSource.tag !== metadataSource.tag + || currentSource.refName !== metadataSource.refName; + + if (needsUpdate) { + debug(`Syncing sourceLanguage from metadata (${metadataSource.refName}) to configuration`); + await config.update("sourceLanguage", metadataSource, vscode.ConfigurationTarget.Workspace); + } + } + + if (metadataTarget) { + const currentTarget = config.get("targetLanguage") as LanguageMetadata | undefined; + const needsUpdate = !currentTarget + || !currentTarget.tag + || currentTarget.tag !== metadataTarget.tag + || currentTarget.refName !== metadataTarget.refName; + + if (needsUpdate) { + debug(`Syncing targetLanguage from metadata (${metadataTarget.refName}) to configuration`); + await config.update("targetLanguage", metadataTarget, vscode.ConfigurationTarget.Workspace); + } + } + } } catch (error) { console.error("Error syncing metadata to configuration:", error); } diff --git a/types/index.d.ts b/types/index.d.ts index c0250bf17..3519a13b4 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1024,12 +1024,7 @@ type ProjectMetadata = { [lang: string]: string; }; }; - languages: Array<{ - tag: string; - name: { - [lang: string]: string; - }; - }>; + languages: Array; type: { flavorType: { name: string;