Browse Source

bug-fixes

Nostr-Signature: 20be97351d2b05fa7ad9e161b2619e9babaaffc6a8090057c1a3ac50a0f08d6a 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc a174c7dd39f613dd88260ef5c111b943df381b0acae20d048596e11ef1a6b0e3c1bfb9a8858af3df0f8858c4c79d1e2d03ad248a0608ac5d5cded6a81e99af77
main
Silberengel 2 weeks ago
parent
commit
30c13763c0
  1. 1
      nostr/commit-signatures.jsonl
  2. 144
      src/lib/components/CodeEditor.svelte
  3. 10
      src/routes/repos/[npub]/[repo]/+page.svelte
  4. 90
      src/routes/repos/[npub]/[repo]/components/FilesTab.svelte
  5. 8
      src/routes/repos/[npub]/[repo]/utils/file-processing.ts

1
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":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":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":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"}

144
src/lib/components/CodeEditor.svelte

@ -20,39 +20,43 @@
} }
let { let {
content = $bindable(''), content = $bindable(),
language = $bindable('text'), language = $bindable(),
onChange = () => {}, onChange = () => {},
onSelection = () => {}, onSelection = () => {},
readOnly = false, readOnly = false,
highlights = [], highlights = [],
scrollToLine = $bindable(null) scrollToLine = $bindable()
}: Props = $props(); }: 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 editorView: EditorView | null = null;
let editorElement: HTMLDivElement; let editorElement: HTMLDivElement;
let languageCompartment = new Compartment(); let languageCompartment = new Compartment();
let editableCompartment = new Compartment();
// Create a highlight decoration (marker style) // Highlight decoration for persistent markers
const highlightMark = Decoration.mark({ const highlightMark = Decoration.mark({
class: 'cm-highlight-marker', class: 'cm-highlight-marker',
attributes: { 'data-highlight': 'true' } attributes: { 'data-highlight': 'true' }
}); });
// Effect to set highlight decorations (DecorationSet) // State effect for updating highlights
const setHighlightEffect = StateEffect.define<typeof Decoration.none>(); const setHighlightEffect = StateEffect.define<ReturnType<typeof Decoration.set>>();
// State field to track highlighted ranges // State field to manage highlight decorations
const highlightField = StateField.define({ const highlightField = StateField.define<ReturnType<typeof Decoration.set>>({
create() { create() {
return Decoration.none; return Decoration.none;
}, },
update(decorations, tr) { update(decorations, tr) {
decorations = decorations.map(tr.changes); decorations = decorations.map(tr.changes);
// Apply highlight effects
for (const effect of tr.effects) { for (const effect of tr.effects) {
if (effect.is(setHighlightEffect)) { if (effect.is(setHighlightEffect)) {
// Replace all decorations with the new set
decorations = effect.value; decorations = effect.value;
} }
} }
@ -61,25 +65,60 @@
provide: f => EditorView.decorations.from(f) 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[] { function getLanguageExtension(): Extension[] {
switch (language) { switch (language) {
case 'markdown': case 'markdown':
// markdown() already includes syntax highlighting - don't add defaultHighlightStyle
return [markdown()]; return [markdown()];
case 'asciidoc': case 'asciidoc':
// StreamLanguage includes its own highlighting - don't use defaultHighlightStyle with it
return [StreamLanguage.define(asciidoc)]; return [StreamLanguage.define(asciidoc)];
default: default:
// Plain text - no syntax highlighting needed
return []; return [];
} }
} }
function createExtensions(): Extension[] { function createExtensions(): Extension[] {
const extensions: Extension[] = [ return [
history(), history(),
closeBrackets(), closeBrackets(),
autocompletion(), autocompletion(),
highlightSelectionMatches(), highlightSelectionMatches(),
highlightField, highlightField,
// Enable line wrapping to prevent horizontal overflow
EditorView.lineWrapping, EditorView.lineWrapping,
keymap.of([ keymap.of([
...closeBracketsKeymap, ...closeBracketsKeymap,
@ -87,17 +126,15 @@
...searchKeymap, ...searchKeymap,
...historyKeymap, ...historyKeymap,
...completionKeymap ...completionKeymap
] as any), ]),
// Add language extensions in a compartment for dynamic updates
languageCompartment.of(getLanguageExtension()), languageCompartment.of(getLanguageExtension()),
// Add update listener editableCompartment.of(EditorView.editable.of(!readOnly)),
EditorView.updateListener.of((update) => { EditorView.updateListener.of((update) => {
if (update.docChanged) { if (update.docChanged) {
const newContent = update.state.doc.toString(); const newContent = update.state.doc.toString();
onChange(newContent); onChange(newContent);
} }
// Handle text selection (allow in read-only mode for highlighting)
if (update.selectionSet) { if (update.selectionSet) {
const selection = update.state.selection.main; const selection = update.state.selection.main;
if (!selection.empty) { if (!selection.empty) {
@ -114,12 +151,8 @@
); );
} }
} }
}), })
// Add editable state
EditorView.editable.of(!readOnly)
]; ];
return extensions;
} }
onMount(() => { onMount(() => {
@ -144,7 +177,10 @@
// Update content when prop changes externally // Update content when prop changes externally
$effect(() => { $effect(() => {
if (editorView && content !== editorView.state.doc.toString()) { if (!editorView) return;
const currentContent = editorView.state.doc.toString();
if (content !== currentContent) {
editorView.dispatch({ editorView.dispatch({
changes: { changes: {
from: 0, from: 0,
@ -157,20 +193,30 @@
// Update language when prop changes // Update language when prop changes
$effect(() => { $effect(() => {
if (editorView) { if (!editorView) return;
// Update language extension using compartment
editorView.dispatch({ editorView.dispatch({
effects: languageCompartment.reconfigure(getLanguageExtension()) effects: languageCompartment.reconfigure(getLanguageExtension())
}); });
}
}); });
// Scroll to and highlight specific lines // Update editable state when readOnly prop changes
$effect(() => {
if (!editorView) return;
editorView.dispatch({
effects: editableCompartment.reconfigure(EditorView.editable.of(!readOnly))
});
});
// Scroll to specific line when scrollToLine changes
$effect(() => { $effect(() => {
if (editorView && scrollToLine !== null && scrollToLine > 0) { if (!editorView || scrollToLine === null || scrollToLine === undefined || scrollToLine <= 0) return;
try { try {
const doc = editorView.state.doc; const doc = editorView.state.doc;
const line = doc.line(Math.min(scrollToLine, doc.lines)); const lineNum = Math.min(scrollToLine, doc.lines);
const line = doc.line(lineNum);
const lineStart = line.from; const lineStart = line.from;
const lineEnd = line.to; const lineEnd = line.to;
@ -181,49 +227,17 @@
}); });
// Clear scrollToLine after scrolling // Clear scrollToLine after scrolling
setTimeout(() => { const timeoutId = setTimeout(() => {
scrollToLine = null; scrollToLine = null;
}, 100); }, 100);
return () => {
clearTimeout(timeoutId);
};
} catch (err) { } catch (err) {
console.error('Error scrolling to line:', err); console.error('Error scrolling to line:', err);
} }
}
}); });
// Function to scroll to and highlight a range of lines with a persistent marker
export function scrollToLines(startLine: number, endLine: number) {
if (!editorView) 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 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
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);
}
}
</script> </script>
<div bind:this={editorElement} class="code-editor"></div> <div bind:this={editorElement} class="code-editor"></div>

10
src/routes/repos/[npub]/[repo]/+page.svelte

@ -1510,6 +1510,16 @@
state.openDialog = 'createFile'; state.openDialog = 'createFile';
}} }}
onApplySyntaxHighlighting={applySyntaxHighlighting} onApplySyntaxHighlighting={applySyntaxHighlighting}
onRenderPreview={async (content: string, ext: string) => {
return new Promise<string>((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} {/if}

90
src/routes/repos/[npub]/[repo]/components/FilesTab.svelte

@ -6,6 +6,7 @@
import TabLayout from './TabLayout.svelte'; import TabLayout from './TabLayout.svelte';
import FileBrowser from './FileBrowser.svelte'; import FileBrowser from './FileBrowser.svelte';
// @ts-ignore - Svelte 5 component with named export
import CodeEditor from '$lib/components/CodeEditor.svelte'; import CodeEditor from '$lib/components/CodeEditor.svelte';
import NostrHtmlRenderer from '$lib/components/NostrHtmlRenderer.svelte'; import NostrHtmlRenderer from '$lib/components/NostrHtmlRenderer.svelte';
@ -50,6 +51,9 @@
onTabChange?: (tab: string) => void; onTabChange?: (tab: string) => void;
onCreateFile?: () => void; onCreateFile?: () => void;
onApplySyntaxHighlighting?: (content: string, ext: string) => Promise<void>; onApplySyntaxHighlighting?: (content: string, ext: string) => Promise<void>;
onRenderPreview?: (content: string, ext: string) => Promise<string>;
npub?: string;
repo?: string;
} }
let { let {
@ -92,19 +96,59 @@
tabs = [], tabs = [],
onTabChange = () => {}, onTabChange = () => {},
onCreateFile = () => {}, onCreateFile = () => {},
onApplySyntaxHighlighting = async () => {} onApplySyntaxHighlighting = async () => {},
onRenderPreview = async () => '',
npub = '',
repo = ''
}: Props = $props(); }: Props = $props();
// Apply syntax highlighting when fileContent changes and we're showing raw content // Live preview HTML generated from editedContent
// This ensures highlighting is ALWAYS applied for raw files, regardless of maintainer status let livePreviewHtml = $state<string | null>(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(() => { $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 // 1. We have file content
// 2. We have a current file // 2. We have a current file
// 3. We're NOT in preview mode (showing raw) // 3. It's NOT an image
// 4. It's NOT an image // 4. Content is not empty
// 5. 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 && !showFilePreview && !isImageFile && fileContent.trim().length > 0) { if (fileContent && currentFile && !isImageFile && fileContent.trim().length > 0) {
const ext = currentFile.split('.').pop() || ''; const ext = currentFile.split('.').pop() || '';
// Always apply highlighting if we don't have highlighted content or it's empty or doesn't contain hljs // Always apply highlighting if we don't have highlighted content or it's empty or doesn't contain hljs
const needsHighlighting = !highlightedFileContent || const needsHighlighting = !highlightedFileContent ||
@ -232,18 +276,27 @@
{:else} {:else}
<div class="editor-container"> <div class="editor-container">
{#if isMaintainer} {#if isMaintainer}
{#if currentFile && showFilePreview && fileHtml && supportsPreview((currentFile.split('.').pop() || '').toLowerCase())} {#if currentFile && showFilePreview && supportsPreview((currentFile.split('.').pop() || '').toLowerCase())}
<div class="read-only-editor" class:word-wrap={wordWrap}> <div class="read-only-editor" class:word-wrap={wordWrap}>
{#if generatingPreview}
<div class="loading-preview">Updating preview...</div>
{:else if livePreviewHtml}
<div class="file-preview markdown">
<NostrHtmlRenderer html={livePreviewHtml} />
</div>
{:else if fileHtml}
<div class="file-preview markdown"> <div class="file-preview markdown">
<NostrHtmlRenderer html={fileHtml} /> <NostrHtmlRenderer html={fileHtml} />
</div> </div>
{/if}
</div> </div>
{:else} {:else if fileContent && !isImageFile}
<!-- Always use CodeEditor for maintainers (editable by default) -->
<CodeEditor <CodeEditor
content={editedContent || fileContent} content={editedContent || fileContent}
language={fileLanguage} language={fileLanguage}
readOnly={needsClone} readOnly={needsClone}
onChange={(value) => { onChange={(value: string) => {
editedContent = value; editedContent = value;
hasChanges = value !== fileContent; hasChanges = value !== fileContent;
onContentChange(value); onContentChange(value);
@ -256,10 +309,16 @@
<div class="file-preview image-preview"> <div class="file-preview image-preview">
<img src={imageUrl} alt={currentFile?.split('/').pop() || 'Image'} class="file-image" /> <img src={imageUrl} alt={currentFile?.split('/').pop() || 'Image'} class="file-image" />
</div> </div>
{:else if currentFile && showFilePreview && fileHtml && supportsPreview((currentFile.split('.').pop() || '').toLowerCase())} {:else if currentFile && showFilePreview && supportsPreview((currentFile.split('.').pop() || '').toLowerCase())}
{#if livePreviewHtml}
<div class="file-preview markdown">
<NostrHtmlRenderer html={livePreviewHtml} />
</div>
{:else if fileHtml}
<div class="file-preview markdown"> <div class="file-preview markdown">
<NostrHtmlRenderer html={fileHtml} /> <NostrHtmlRenderer html={fileHtml} />
</div> </div>
{/if}
{:else if fileContent} {:else if fileContent}
<div class="raw-content"> <div class="raw-content">
{#if highlightedFileContent && highlightedFileContent.trim() !== ''} {#if highlightedFileContent && highlightedFileContent.trim() !== ''}
@ -499,6 +558,13 @@
white-space: pre-wrap !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 { .file-editor .editor-actions {
display: flex !important; display: flex !important;
align-items: center; align-items: center;

8
src/routes/repos/[npub]/[repo]/utils/file-processing.ts

@ -46,6 +46,7 @@ export function getHighlightLanguage(ext: string): string {
'sass': 'sass', 'sass': 'sass',
'less': 'less', 'less': 'less',
'json': 'json', 'json': 'json',
'jsonl': 'json',
'yaml': 'yaml', 'yaml': 'yaml',
'yml': 'yaml', 'yml': 'yaml',
'toml': 'toml', 'toml': 'toml',
@ -318,11 +319,14 @@ export async function applySyntaxHighlighting(
// Apply highlighting // Apply highlighting
let highlighted: string; let highlighted: string;
if (lang === 'plaintext') { if (lang === 'plaintext') {
highlighted = `<pre><code class="hljs">${hljs.highlight(content, { language: 'plaintext' }).value}</code></pre>`; highlighted = `<pre><code class="hljs language-plaintext">${hljs.highlight(content, { language: 'plaintext' }).value}</code></pre>`;
} else if (hljs.getLanguage(lang)) { } else if (hljs.getLanguage(lang)) {
highlighted = `<pre><code class="hljs language-${lang}">${hljs.highlight(content, { language: lang }).value}</code></pre>`; highlighted = `<pre><code class="hljs language-${lang}">${hljs.highlight(content, { language: lang }).value}</code></pre>`;
} else { } else {
highlighted = `<pre><code class="hljs">${hljs.highlightAuto(content).value}</code></pre>`; // Use highlightAuto but still add a language class for styling
const autoResult = hljs.highlightAuto(content);
const detectedLang = autoResult.language || 'plaintext';
highlighted = `<pre><code class="hljs language-${detectedLang}">${autoResult.value}</code></pre>`;
} }
console.log('[applySyntaxHighlighting] Highlighting complete, setting content:', { highlightedLength: highlighted.length }); console.log('[applySyntaxHighlighting] Highlighting complete, setting content:', { highlightedLength: highlighted.length });
setHighlightedContent(highlighted); setHighlightedContent(highlighted);

Loading…
Cancel
Save