Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 118 additions & 5 deletions src/renderer/components/editor/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { useRef, useEffect } from 'react'
import { useCodeMirror } from '@/hooks/use-codemirror'
import { useRef, useEffect, useMemo, useState, useCallback } from 'react'
import type { ImperativePanelGroupHandle, PanelOnResize } from 'react-resizable-panels'
import { useCodeMirror, type VisibleLineRange } from '@/hooks/use-codemirror'
import { TocPanel } from './TocPanel'
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable'
import { useTocIsVisible, useTocWidth, useTocSettingsStore } from '@/stores/toc-settings-store'
import { TOC_MAX_WIDTH, TOC_MIN_WIDTH } from '@/types/settings'

interface CodeEditorProps {
filePath: string
Expand All @@ -12,6 +17,7 @@ interface CodeEditorProps {
onScrollChange: (scrollTop: number) => void
}


export function CodeEditor({
content,
language,
Expand All @@ -22,15 +28,44 @@ export function CodeEditor({
onScrollChange
}: CodeEditorProps): React.JSX.Element {
const containerRef = useRef<HTMLDivElement>(null)
const { view, setContent } = useCodeMirror(containerRef, {
const layoutRef = useRef<HTMLDivElement>(null)
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null)
const [visibleRange, setVisibleRange] = useState<VisibleLineRange | undefined>()
const [layoutWidth, setLayoutWidth] = useState(0)
const isTocVisible = useTocIsVisible()
const tocWidth = useTocWidth()
const setTocWidth = useTocSettingsStore((state) => state.setWidth)

const { view, setContent, scrollToLine } = useCodeMirror(containerRef, {
content,
language,
readOnly,
onChange,
onCursorChange,
onScrollChange
onScrollChange,
onVisibleRangeChange: setVisibleRange
})

const getTocPanelSizePercent = useCallback((): number => {
const panelWidth = layoutWidth || layoutRef.current?.clientWidth || 1000
const widthRatio = panelWidth > 0 ? tocWidth / panelWidth : 0
return Math.min(40, Math.max(10, widthRatio * 100))
}, [layoutWidth, tocWidth])

const tocPanelDefaultSize = useMemo(() => getTocPanelSizePercent(), [getTocPanelSizePercent])

const handleTocResize = useCallback<PanelOnResize>(
(size, prevSize): void => {
const layoutWidth = layoutRef.current?.clientWidth ?? 1000
const nextPixels = Math.round((size / 100) * layoutWidth)

if (prevSize !== size) {
setTocWidth(nextPixels)
}
},
[setTocWidth]
)

// Update content when it changes from external source (file reload)
const prevContentRef = useRef(content)
useEffect(() => {
Expand All @@ -40,14 +75,92 @@ export function CodeEditor({
}
}, [content, setContent])

useEffect(() => {
const element = layoutRef.current
if (!element) {
return
}

const updateLayoutWidth = (): void => {
setLayoutWidth(element.clientWidth)
}

updateLayoutWidth()

const observer = new ResizeObserver(() => {
updateLayoutWidth()
})

observer.observe(element)

return () => observer.disconnect()
}, [])

// Focus when becoming visible
useEffect(() => {
if (isVisible && view) {
view.focus()
}
}, [isVisible, view])

useEffect(() => {
if (!isTocVisible || language !== 'markdown') {
return
}

const group = panelGroupRef.current
if (!group) {
return
}

const tocSize = getTocPanelSizePercent()
const currentTocSize = group.getLayout()[1]

if (currentTocSize !== undefined && Math.abs(currentTocSize - tocSize) < 0.5) {
return
}

group.setLayout([100 - tocSize, tocSize])
}, [getTocPanelSizePercent, isTocVisible, language])

return (
<div ref={containerRef} className="w-full h-full overflow-hidden" />
<div className="h-full w-full" style={{ display: isVisible ? 'block' : 'none' }}>
<div ref={layoutRef} className="h-full w-full">
<ResizablePanelGroup ref={panelGroupRef} direction="horizontal">
<ResizablePanel
defaultSize={isTocVisible && language === 'markdown' ? 100 - tocPanelDefaultSize : 100}
minSize={60}
>
<div ref={containerRef} className="w-full h-full overflow-hidden" />
</ResizablePanel>

{isTocVisible && language === 'markdown' && (
<>
<ResizableHandle />
<ResizablePanel
defaultSize={tocPanelDefaultSize}
minSize={10}
maxSize={40}
onResize={handleTocResize}
>
<div
className="h-full"
style={{ minWidth: TOC_MIN_WIDTH, maxWidth: TOC_MAX_WIDTH, width: '100%' }}
>
<TocPanel
editorMode="codemirror"
codemirror={{
content,
scrollToLine,
visibleRange
}}
/>
</div>
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
</div>
</div>
)
}
11 changes: 7 additions & 4 deletions src/renderer/components/editor/EditorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MarkdownEditor } from './MarkdownEditor'
import { EditorToolbar } from './EditorToolbar'
import { useEditorStore } from '@/stores/editor-store'
import type { EditorFileState } from '@/stores/editor-store'
import { useTocSettings } from '@/hooks/use-toc-settings'

interface EditorPanelProps {
filePath: string
Expand All @@ -14,6 +15,8 @@ export function EditorPanel({
filePath,
isVisible
}: EditorPanelProps): React.JSX.Element {
useTocSettings()

const fileState = useEditorStore(
(state) => state.openFiles.get(filePath)
) as EditorFileState | undefined
Expand All @@ -25,28 +28,28 @@ export function EditorPanel({
(content: string) => {
updateContent(filePath, content)
},
[filePath]
[filePath, updateContent]
)

const handleCursorChange = useCallback(
(line: number, col: number) => {
updateCursorPosition(filePath, line, col)
},
[filePath]
[filePath, updateCursorPosition]
)

const handleScrollChange = useCallback(
(scrollTop: number) => {
updateScrollTop(filePath, scrollTop)
},
[filePath]
[filePath, updateScrollTop]
)

const handleToggleViewMode = useCallback(() => {
if (!fileState) return
const newMode = fileState.viewMode === 'markdown' ? 'code' : 'markdown'
setViewMode(filePath, newMode)
}, [filePath, fileState?.viewMode])
}, [filePath, fileState, setViewMode])

if (!fileState) {
return (
Expand Down
65 changes: 43 additions & 22 deletions src/renderer/components/editor/EditorToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Code2, Eye } from 'lucide-react'
import { Code2, Eye, List } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { useTocIsVisible, useTocSettingsStore } from '@/stores/toc-settings-store'

interface EditorToolbarProps {
viewMode: 'code' | 'markdown'
Expand All @@ -12,31 +14,50 @@ export function EditorToolbar({
onToggleViewMode,
filePath
}: EditorToolbarProps): React.JSX.Element {
const fileName = filePath.split('/').pop() || filePath
const fileName = filePath.split(/[\\/]/).pop() || filePath
const isTocVisible = useTocIsVisible()
const toggleTocVisibility = useTocSettingsStore((state) => state.toggleVisibility)

return (
<div className="flex items-center justify-between px-3 h-8 border-b border-border bg-card flex-shrink-0">
<span className="text-xs text-muted-foreground truncate">{fileName}</span>
<button
onClick={onToggleViewMode}
className={cn(
'flex items-center gap-1 px-2 py-0.5 text-xs rounded transition-colors',
'text-muted-foreground hover:text-foreground hover:bg-secondary'
)}
title={viewMode === 'markdown' ? 'Switch to source mode' : 'Switch to WYSIWYG mode'}
>
{viewMode === 'markdown' ? (
<>
<Code2 size={12} />
<span>Source</span>
</>
) : (
<>
<Eye size={12} />
<span>Preview</span>
</>
)}
</button>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
className={cn(
'h-6 gap-1 px-2 text-xs text-muted-foreground hover:text-foreground',
isTocVisible && 'bg-accent text-accent-foreground'
)}
onClick={toggleTocVisibility}
title="Toggle Table of Contents"
aria-pressed={isTocVisible}
>
<List size={12} />
<span>TOC</span>
</Button>

<button
onClick={onToggleViewMode}
className={cn(
'flex items-center gap-1 px-2 py-0.5 text-xs rounded transition-colors',
'text-muted-foreground hover:text-foreground hover:bg-secondary'
)}
title={viewMode === 'markdown' ? 'Switch to source mode' : 'Switch to WYSIWYG mode'}
>
{viewMode === 'markdown' ? (
<>
<Code2 size={12} />
<span>Source</span>
</>
) : (
<>
<Eye size={12} />
<span>Preview</span>
</>
)}
</button>
</div>
</div>
)
}
Loading
Loading