Skip to content

Commit 2112bfa

Browse files
authored
Merge pull request #119 from beNative/codex/fix-local-plantuml-renderer-issue
Improve local PlantUML renderer error UX
2 parents 2109ab4 + 89a6efe commit 2112bfa

4 files changed

Lines changed: 105 additions & 6 deletions

File tree

electron/main.ts

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -483,11 +483,11 @@ ipcMain.handle('plantuml:render-svg', async (_, diagram: string, format: 'svg' =
483483
return;
484484
}
485485

486-
const exitDetails = errorOutput.trim() || `Renderer exited with code ${code}.`;
486+
const exitDetails = errorOutput.trim();
487487
finalize({
488488
success: false,
489-
error: 'Local PlantUML renderer failed to produce output.',
490-
details: exitDetails,
489+
error: derivePlantumlFriendlyError(exitDetails, code ?? undefined),
490+
details: exitDetails || (typeof code === 'number' ? `Renderer exited with code ${code}.` : undefined),
491491
});
492492
});
493493

@@ -514,6 +514,88 @@ let plantumlJarLookupPromise: Promise<string> | null = null;
514514

515515
const PLANTUML_JAR_RELATIVE_PATH = path.join('assets', 'plantuml', 'plantuml.jar');
516516

517+
function derivePlantumlFriendlyError(details?: string | null, exitCode?: number): string {
518+
if (details) {
519+
const normalized = details.toLowerCase();
520+
521+
if (normalized.includes('cannot run program') && normalized.includes('"dot"')) {
522+
return 'Graphviz (the "dot" executable) is required for the local PlantUML renderer. Install Graphviz or add it to your PATH.';
523+
}
524+
525+
if (normalized.includes('graphviz') && normalized.includes('not found')) {
526+
return 'Graphviz binaries were not found. Install Graphviz to enable the local PlantUML renderer.';
527+
}
528+
529+
if (normalized.includes('unsupportedclassversionerror')) {
530+
return 'The bundled PlantUML renderer requires a newer Java runtime. Update Java and try again.';
531+
}
532+
533+
if (normalized.includes('could not find or load main class') || normalized.includes('classnotfoundexception')) {
534+
return 'The PlantUML renderer could not start. Ensure assets/plantuml/plantuml.jar is present and accessible.';
535+
}
536+
537+
if (normalized.includes('permission denied')) {
538+
return 'The PlantUML renderer could not be executed because of missing file permissions.';
539+
}
540+
}
541+
542+
if (typeof exitCode === 'number' && exitCode !== 0) {
543+
return `Local PlantUML renderer exited with code ${exitCode}.`;
544+
}
545+
546+
return 'Local PlantUML renderer failed to produce output.';
547+
}
548+
549+
function isPackagedAsarPath(filePath: string): boolean {
550+
return /\.asar($|[\\/])/.test(filePath);
551+
}
552+
553+
async function ensurePlantumlJarExtracted(sourcePath: string): Promise<string> {
554+
if (!isPackagedAsarPath(sourcePath)) {
555+
return sourcePath;
556+
}
557+
558+
const tempDir = path.join(app.getPath('temp'), 'docforge-plantuml');
559+
const destinationPath = path.join(tempDir, 'plantuml.jar');
560+
561+
await fs.mkdir(tempDir, { recursive: true });
562+
563+
let needsExtraction = true;
564+
try {
565+
const [sourceStats, destStats] = await Promise.all([fs.stat(sourcePath), fs.stat(destinationPath)]);
566+
if (sourceStats.size === destStats.size) {
567+
needsExtraction = false;
568+
}
569+
} catch {
570+
// Either the destination does not exist yet or we could not stat one of the files.
571+
needsExtraction = true;
572+
}
573+
574+
if (!needsExtraction) {
575+
return destinationPath;
576+
}
577+
578+
const pipeline = promisify(stream.pipeline);
579+
580+
try {
581+
await pipeline(createReadStream(sourcePath), createWriteStream(destinationPath));
582+
} catch (error) {
583+
try {
584+
await fs.unlink(destinationPath);
585+
} catch {
586+
// Ignore cleanup failures; we'll retry extraction later if needed.
587+
}
588+
589+
const message =
590+
error instanceof Error
591+
? error.message
592+
: 'Failed to extract bundled PlantUML renderer from application archive.';
593+
throw new Error(`Unable to prepare PlantUML renderer: ${message}`);
594+
}
595+
596+
return destinationPath;
597+
}
598+
517599
async function resolvePlantUmlJar(): Promise<string> {
518600
if (cachedPlantumlJarPath) {
519601
return cachedPlantumlJarPath;
@@ -539,8 +621,9 @@ async function resolvePlantUmlJar(): Promise<string> {
539621
for (const candidate of candidates) {
540622
try {
541623
await fs.access(candidate);
542-
cachedPlantumlJarPath = candidate;
543-
return candidate;
624+
const usablePath = await ensurePlantumlJarExtracted(candidate);
625+
cachedPlantumlJarPath = usablePath;
626+
return usablePath;
544627
} catch {
545628
// Continue searching
546629
}

services/preview/markdownRenderer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,7 @@ const MarkdownViewer = forwardRef<HTMLDivElement, MarkdownViewerProps>(({ conten
621621
gap: 0.5rem;
622622
align-items: flex-start;
623623
text-align: left;
624+
user-select: text;
624625
}
625626
626627
.df-mermaid-error__message,
@@ -636,6 +637,7 @@ const MarkdownViewer = forwardRef<HTMLDivElement, MarkdownViewerProps>(({ conten
636637
padding: 0.75rem 0.85rem;
637638
background: rgba(var(--color-background), 0.6);
638639
color: rgba(var(--color-text-secondary), 0.95);
640+
user-select: text;
639641
}
640642
641643
.df-mermaid-error__details > summary,

services/preview/plantumlDiagram.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,21 @@ interface PlantUMLErrorProps {
1515
details?: string | null;
1616
}
1717

18+
const stopPointerEvent = (event: React.SyntheticEvent) => {
19+
event.stopPropagation();
20+
};
21+
1822
const PlantUMLError: React.FC<PlantUMLErrorProps> = ({ message, details }) => (
1923
<div className="df-plantuml" role="alert">
20-
<div className="df-plantuml-error">
24+
<div
25+
className="df-plantuml-error"
26+
onPointerDown={stopPointerEvent}
27+
onPointerMove={stopPointerEvent}
28+
onPointerUp={stopPointerEvent}
29+
onDoubleClick={stopPointerEvent}
30+
onClick={stopPointerEvent}
31+
onWheel={stopPointerEvent}
32+
>
2133
<div className="df-plantuml-error__message">{message}</div>
2234
{details && details.trim() && (
2335
<details className="df-plantuml-error__details">

services/preview/plantumlRenderer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,12 @@ const PlantUMLPreview: React.FC<PlantUMLPreviewProps> = ({ content, settings })
7373
gap: 0.5rem;
7474
align-items: flex-start;
7575
text-align: left;
76+
user-select: text;
7677
}
7778
7879
.df-plantuml-error__details {
7980
font-size: 0.85rem;
81+
user-select: text;
8082
}
8183
`}</style>
8284
</ZoomPanContainer>

0 commit comments

Comments
 (0)