diff --git a/nostr/commit-signatures.jsonl b/nostr/commit-signatures.jsonl index a56fa82..f39d98d 100644 --- a/nostr/commit-signatures.jsonl +++ b/nostr/commit-signatures.jsonl @@ -111,3 +111,4 @@ {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772188835,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactor 13"]],"content":"Signed commit: refactor 13","id":"f41c8662dcbf1be408c560d11eda0890c40582a8ea8bb3220116e645cc6a2bb5","sig":"2b7b70089cecfa4652fe236fa586a6fe1b05c1c95434a160717cbf5ee2f37382cdd8e8f31d7b3a7576ee5264e9e70c7a8651591caaea0cd311d1be4c561d282f"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772193104,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"02dcdcda1083cffd91dbf8906716c2ae09f06f77ef8590802afecd85f0b3108a","sig":"13d2b30ed37af03fd47dc09536058babb4dc63d1cfc55b8f38651ffd6342abcddc840b543c085b047721e9102b2d07e3dae78ff31d5990c92c04410ef1efcd5b"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772220851,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"d98d2d6a6eb27ba36f19015f7d6969fe3925c40b23187d70ccc9b61141c6b4b7","sig":"8727e3015e38a78d7a6105c26e5b1469dc4d6d701e58d5d6c522ab529b4daa2d39d4353eb6d091f3c1fd28ad0289eae808494c9e2722bf9065dd2b2e9001664f"} +{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772223624,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"99cb543f1e821f1b7df4bbde2b3da3ab3a09cda7a1e9a537fe1b8df79b19e8e8","sig":"762a7ea92457ce81cc5aae9bc644fb9d80f90c7500035fbb506f2f76a5942333b828cc8a59f7656b0e714b15a59158be0a671f51476be2e8eabe9731ced74bcb"} diff --git a/src/lib/components/CodeEditor.svelte b/src/lib/components/CodeEditor.svelte index 9e7c43a..41805b5 100644 --- a/src/lib/components/CodeEditor.svelte +++ b/src/lib/components/CodeEditor.svelte @@ -20,39 +20,43 @@ } let { - content = $bindable(''), - language = $bindable('text'), + content = $bindable(), + language = $bindable(), onChange = () => {}, onSelection = () => {}, readOnly = false, highlights = [], - scrollToLine = $bindable(null) + scrollToLine = $bindable() }: Props = $props(); + // Set default values for bindable props + if (content === undefined) content = ''; + if (language === undefined) language = 'text'; + if (scrollToLine === undefined) scrollToLine = null; + let editorView: EditorView | null = null; let editorElement: HTMLDivElement; let languageCompartment = new Compartment(); + let editableCompartment = new Compartment(); - // Create a highlight decoration (marker style) + // Highlight decoration for persistent markers const highlightMark = Decoration.mark({ class: 'cm-highlight-marker', attributes: { 'data-highlight': 'true' } }); - // Effect to set highlight decorations (DecorationSet) - const setHighlightEffect = StateEffect.define(); + // State effect for updating highlights + const setHighlightEffect = StateEffect.define>(); - // State field to track highlighted ranges - const highlightField = StateField.define({ + // State field to manage highlight decorations + const highlightField = StateField.define>({ create() { return Decoration.none; }, update(decorations, tr) { decorations = decorations.map(tr.changes); - // Apply highlight effects for (const effect of tr.effects) { if (effect.is(setHighlightEffect)) { - // Replace all decorations with the new set decorations = effect.value; } } @@ -61,25 +65,60 @@ provide: f => EditorView.decorations.from(f) }); + // Exported function to scroll to and highlight a range of lines + export function scrollToLines(startLine: number, endLine: number): void { + if (!editorView) return; + + try { + const doc = editorView.state.doc; + const start = Math.max(1, Math.min(startLine, doc.lines)); + const end = Math.max(1, Math.min(endLine, doc.lines)); + + const startLineObj = doc.line(start); + const endLineObj = doc.line(end); + + const from = startLineObj.from; + const to = endLineObj.to; + + // Create highlight decoration + const decorationRange = highlightMark.range(from, to); + const decorationSet = Decoration.set([decorationRange]); + + // Apply highlight + editorView.dispatch({ + effects: setHighlightEffect.of(decorationSet) + }); + + // Scroll to the lines + editorView.dispatch({ + effects: EditorView.scrollIntoView(from, { y: 'center' }) + }); + } catch (err) { + console.error('Error scrolling to lines:', err); + } + } + function getLanguageExtension(): Extension[] { switch (language) { case 'markdown': + // markdown() already includes syntax highlighting - don't add defaultHighlightStyle return [markdown()]; case 'asciidoc': + // StreamLanguage includes its own highlighting - don't use defaultHighlightStyle with it return [StreamLanguage.define(asciidoc)]; default: + // Plain text - no syntax highlighting needed return []; } } function createExtensions(): Extension[] { - const extensions: Extension[] = [ + return [ history(), closeBrackets(), autocompletion(), highlightSelectionMatches(), highlightField, - // Enable line wrapping to prevent horizontal overflow EditorView.lineWrapping, keymap.of([ ...closeBracketsKeymap, @@ -87,17 +126,15 @@ ...searchKeymap, ...historyKeymap, ...completionKeymap - ] as any), - // Add language extensions in a compartment for dynamic updates + ]), languageCompartment.of(getLanguageExtension()), - // Add update listener + editableCompartment.of(EditorView.editable.of(!readOnly)), EditorView.updateListener.of((update) => { if (update.docChanged) { const newContent = update.state.doc.toString(); onChange(newContent); } - // Handle text selection (allow in read-only mode for highlighting) if (update.selectionSet) { const selection = update.state.selection.main; if (!selection.empty) { @@ -114,12 +151,8 @@ ); } } - }), - // Add editable state - EditorView.editable.of(!readOnly) + }) ]; - - return extensions; } onMount(() => { @@ -144,7 +177,10 @@ // Update content when prop changes externally $effect(() => { - if (editorView && content !== editorView.state.doc.toString()) { + if (!editorView) return; + + const currentContent = editorView.state.doc.toString(); + if (content !== currentContent) { editorView.dispatch({ changes: { from: 0, @@ -157,73 +193,51 @@ // Update language when prop changes $effect(() => { - if (editorView) { - // Update language extension using compartment - editorView.dispatch({ - effects: languageCompartment.reconfigure(getLanguageExtension()) - }); - } + if (!editorView) return; + + editorView.dispatch({ + effects: languageCompartment.reconfigure(getLanguageExtension()) + }); }); - // Scroll to and highlight specific lines + // Update editable state when readOnly prop changes $effect(() => { - if (editorView && scrollToLine !== null && scrollToLine > 0) { - try { - const doc = editorView.state.doc; - const line = doc.line(Math.min(scrollToLine, doc.lines)); - const lineStart = line.from; - const lineEnd = line.to; - - // Scroll to the line - editorView.dispatch({ - selection: { anchor: lineStart, head: lineEnd }, - effects: EditorView.scrollIntoView(lineStart, { y: 'center' }) - }); - - // Clear scrollToLine after scrolling - setTimeout(() => { - scrollToLine = null; - }, 100); - } catch (err) { - console.error('Error scrolling to line:', err); - } - } + if (!editorView) return; + + editorView.dispatch({ + effects: editableCompartment.reconfigure(EditorView.editable.of(!readOnly)) + }); }); - // Function to scroll to and highlight a range of lines with a persistent marker - export function scrollToLines(startLine: number, endLine: number) { - if (!editorView) return; + // Scroll to specific line when scrollToLine changes + $effect(() => { + if (!editorView || scrollToLine === null || scrollToLine === undefined || scrollToLine <= 0) return; try { const doc = editorView.state.doc; - const start = Math.min(startLine, doc.lines); - const end = Math.min(endLine, doc.lines); - - const startLineObj = doc.line(start); - const endLineObj = doc.line(end); + const lineNum = Math.min(scrollToLine, doc.lines); + const line = doc.line(lineNum); + const lineStart = line.from; + const lineEnd = line.to; - const from = startLineObj.from; - const to = endLineObj.to; - - // Create a highlight decoration for the range - const decorationRange = highlightMark.range(from, to); - - // Create a DecorationSet with the highlight - const decorationSet = Decoration.set([decorationRange]); - - // Update the highlight field with the new decoration using StateEffect + // Scroll to the line editorView.dispatch({ - effects: setHighlightEffect.of(decorationSet) + selection: { anchor: lineStart, head: lineEnd }, + effects: EditorView.scrollIntoView(lineStart, { y: 'center' }) }); - // Scroll to the lines - editorView.dispatch({ - effects: EditorView.scrollIntoView(from, { y: 'center' }) - }); + // Clear scrollToLine after scrolling + const timeoutId = setTimeout(() => { + scrollToLine = null; + }, 100); + + return () => { + clearTimeout(timeoutId); + }; } catch (err) { - console.error('Error scrolling to lines:', err); + console.error('Error scrolling to line:', err); } - } + });
diff --git a/src/routes/repos/[npub]/[repo]/+page.svelte b/src/routes/repos/[npub]/[repo]/+page.svelte index 30d5c8b..4321c29 100644 --- a/src/routes/repos/[npub]/[repo]/+page.svelte +++ b/src/routes/repos/[npub]/[repo]/+page.svelte @@ -1510,6 +1510,16 @@ state.openDialog = 'createFile'; }} onApplySyntaxHighlighting={applySyntaxHighlighting} + onRenderPreview={async (content: string, ext: string) => { + return new Promise((resolve) => { + const branch = state.git.currentBranch || state.git.defaultBranch || null; + renderFileAsHtmlUtil(content, ext, state.files.currentFile, (html: string) => { + resolve(html); + }, state.npub, state.repo, branch); + }); + }} + npub={state.npub} + repo={state.repo} /> {/if} diff --git a/src/routes/repos/[npub]/[repo]/components/FilesTab.svelte b/src/routes/repos/[npub]/[repo]/components/FilesTab.svelte index 2bee94d..542276b 100644 --- a/src/routes/repos/[npub]/[repo]/components/FilesTab.svelte +++ b/src/routes/repos/[npub]/[repo]/components/FilesTab.svelte @@ -6,6 +6,7 @@ import TabLayout from './TabLayout.svelte'; import FileBrowser from './FileBrowser.svelte'; + // @ts-ignore - Svelte 5 component with named export import CodeEditor from '$lib/components/CodeEditor.svelte'; import NostrHtmlRenderer from '$lib/components/NostrHtmlRenderer.svelte'; @@ -50,6 +51,9 @@ onTabChange?: (tab: string) => void; onCreateFile?: () => void; onApplySyntaxHighlighting?: (content: string, ext: string) => Promise; + onRenderPreview?: (content: string, ext: string) => Promise; + npub?: string; + repo?: string; } let { @@ -92,19 +96,59 @@ tabs = [], onTabChange = () => {}, onCreateFile = () => {}, - onApplySyntaxHighlighting = async () => {} + onApplySyntaxHighlighting = async () => {}, + onRenderPreview = async () => '', + npub = '', + repo = '' }: Props = $props(); - // Apply syntax highlighting when fileContent changes and we're showing raw content - // This ensures highlighting is ALWAYS applied for raw files, regardless of maintainer status + // Live preview HTML generated from editedContent + let livePreviewHtml = $state(null); + let generatingPreview = $state(false); + + // Initialize editedContent when file changes + $effect(() => { + if (currentFile && fileContent !== undefined) { + editedContent = fileContent; + livePreviewHtml = null; // Reset live preview when file changes + } + }); + + // Generate live preview from editedContent when in preview mode $effect(() => { - // Only apply highlighting when: + if (showFilePreview && currentFile && editedContent && supportsPreview((currentFile.split('.').pop() || '').toLowerCase())) { + const ext = currentFile.split('.').pop() || ''; + generatingPreview = true; + + // Debounce preview generation to avoid too many updates + const timeoutId = setTimeout(async () => { + try { + const html = await onRenderPreview(editedContent, ext); + livePreviewHtml = html; + } catch (err) { + console.error('Error generating live preview:', err); + livePreviewHtml = null; + } finally { + generatingPreview = false; + } + }, 300); // 300ms debounce + + return () => clearTimeout(timeoutId); + } else { + livePreviewHtml = null; + } + }); + + // Apply syntax highlighting when fileContent changes + // This ensures highlighting is ALWAYS applied for all files, regardless of maintainer status or preview mode + $effect(() => { + // Apply highlighting when: // 1. We have file content // 2. We have a current file - // 3. We're NOT in preview mode (showing raw) - // 4. It's NOT an image - // 5. Content is not empty - if (fileContent && currentFile && !showFilePreview && !isImageFile && fileContent.trim().length > 0) { + // 3. It's NOT an image + // 4. Content is not empty + // Note: We apply highlighting regardless of preview mode so it's ready when user switches to raw view + if (fileContent && currentFile && !isImageFile && fileContent.trim().length > 0) { const ext = currentFile.split('.').pop() || ''; // Always apply highlighting if we don't have highlighted content or it's empty or doesn't contain hljs const needsHighlighting = !highlightedFileContent || @@ -232,18 +276,27 @@ {:else}
{#if isMaintainer} - {#if currentFile && showFilePreview && fileHtml && supportsPreview((currentFile.split('.').pop() || '').toLowerCase())} + {#if currentFile && showFilePreview && supportsPreview((currentFile.split('.').pop() || '').toLowerCase())}
-
- -
+ {#if generatingPreview} +
Updating preview...
+ {:else if livePreviewHtml} +
+ +
+ {:else if fileHtml} +
+ +
+ {/if}
- {:else} + {:else if fileContent && !isImageFile} + { + onChange={(value: string) => { editedContent = value; hasChanges = value !== fileContent; onContentChange(value); @@ -256,10 +309,16 @@
{currentFile?.split('/').pop()
- {:else if currentFile && showFilePreview && fileHtml && supportsPreview((currentFile.split('.').pop() || '').toLowerCase())} -
- -
+ {:else if currentFile && showFilePreview && supportsPreview((currentFile.split('.').pop() || '').toLowerCase())} + {#if livePreviewHtml} +
+ +
+ {:else if fileHtml} +
+ +
+ {/if} {:else if fileContent}
{#if highlightedFileContent && highlightedFileContent.trim() !== ''} @@ -498,6 +557,13 @@ overflow-wrap: break-word !important; white-space: pre-wrap !important; } + + .loading-preview { + padding: 1rem; + text-align: center; + color: var(--text-secondary, #666); + font-style: italic; + } .file-editor .editor-actions { display: flex !important; diff --git a/src/routes/repos/[npub]/[repo]/utils/file-processing.ts b/src/routes/repos/[npub]/[repo]/utils/file-processing.ts index b6f78e4..a775e93 100644 --- a/src/routes/repos/[npub]/[repo]/utils/file-processing.ts +++ b/src/routes/repos/[npub]/[repo]/utils/file-processing.ts @@ -46,6 +46,7 @@ export function getHighlightLanguage(ext: string): string { 'sass': 'sass', 'less': 'less', 'json': 'json', + 'jsonl': 'json', 'yaml': 'yaml', 'yml': 'yaml', 'toml': 'toml', @@ -318,11 +319,14 @@ export async function applySyntaxHighlighting( // Apply highlighting let highlighted: string; if (lang === 'plaintext') { - highlighted = `
${hljs.highlight(content, { language: 'plaintext' }).value}
`; + highlighted = `
${hljs.highlight(content, { language: 'plaintext' }).value}
`; } else if (hljs.getLanguage(lang)) { highlighted = `
${hljs.highlight(content, { language: lang }).value}
`; } else { - highlighted = `
${hljs.highlightAuto(content).value}
`; + // Use highlightAuto but still add a language class for styling + const autoResult = hljs.highlightAuto(content); + const detectedLang = autoResult.language || 'plaintext'; + highlighted = `
${autoResult.value}
`; } console.log('[applySyntaxHighlighting] Highlighting complete, setting content:', { highlightedLength: highlighted.length }); setHighlightedContent(highlighted);