1- import { useCallback , useEffect , useRef , useState } from 'react' ;
1+ import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
22
3+ import type { OutlineItem } from '@/components/editor/editor-breadcrumbs' ;
34import type { GotoPosition , ViewedFile } from '@/stores/file-viewer-store' ;
45import type { FC } from 'react' ;
56
67import { FileDiffViewer } from '@/components/activity/file-diff-viewer' ;
78import { CodeMirrorEditor } from '@/components/editor/CodeMirrorEditor' ;
9+ import { EditorBreadcrumbs , extractMarkdownOutline } from '@/components/editor/editor-breadcrumbs' ;
810import { writeFile , lspDidChange , lspDidSave } from '@/lib/backend' ;
9- import { useFileViewerStore } from '@/stores/file-viewer-store' ;
11+ import { useCursorPosition , useFileViewerStore } from '@/stores/file-viewer-store' ;
1012
1113// Hook to detect theme from DOM
1214function useDetectTheme ( ) : 'dark' | 'light' {
@@ -39,6 +41,7 @@ interface FileViewerContentProps {
3941
4042export const FileViewerContent : FC < FileViewerContentProps > = ( { file } ) => {
4143 const theme = useDetectTheme ( ) ;
44+ const cursorPosition = useCursorPosition ( ) ;
4245 const searchOpen = useFileViewerStore ( ( state ) => state . searchOpen ) ;
4346 const searchQuery = useFileViewerStore ( ( state ) => state . searchQuery ) ;
4447 const setSearchQuery = useFileViewerStore ( ( state ) => state . setSearchQuery ) ;
@@ -47,12 +50,50 @@ export const FileViewerContent: FC<FileViewerContentProps> = ({ file }) => {
4750 const markSaved = useFileViewerStore ( ( state ) => state . markSaved ) ;
4851 const pendingGoto = useFileViewerStore ( ( state ) => state . pendingGoto ) ;
4952 const clearPendingGoto = useFileViewerStore ( ( state ) => state . clearPendingGoto ) ;
53+ const gotoPosition = useFileViewerStore ( ( state ) => state . gotoPosition ) ;
5054 const inputRef = useRef < HTMLInputElement > ( null ) ;
5155 const documentVersionRef = useRef ( 1 ) ; // Track document version for LSP
5256
5357 // Only apply goto if it's for the current file
5458 const gotoForThisFile : GotoPosition | null = pendingGoto ;
5559
60+ // Extract outline for markdown files
61+ const outline = useMemo ( ( ) : OutlineItem [ ] => {
62+ if ( file . language === 'markdown' ) {
63+ return extractMarkdownOutline ( file . content ) ;
64+ }
65+ return [ ] ;
66+ } , [ file . content , file . language ] ) ;
67+
68+ // Find active outline index based on cursor position
69+ const activeOutlineIndex = useMemo ( ( ) : number => {
70+ if ( outline . length === 0 ) return - 1 ;
71+
72+ // Find the last heading that is at or before the current cursor line
73+ // cursorPosition.line is 1-indexed, outline.line is 0-indexed
74+ const cursorLine = cursorPosition . line - 1 ;
75+ let activeIndex = - 1 ;
76+
77+ for ( let i = 0 ; i < outline . length ; i ++ ) {
78+ const item = outline [ i ] ;
79+ if ( item && item . line <= cursorLine ) {
80+ activeIndex = i ;
81+ } else if ( item ) {
82+ break ;
83+ }
84+ }
85+
86+ return activeIndex ;
87+ } , [ outline , cursorPosition . line ] ) ;
88+
89+ // Handle outline item click - navigate to that line
90+ const handleOutlineClick = useCallback (
91+ ( item : OutlineItem ) : void => {
92+ gotoPosition ( file . path , item . line + 1 , 1 ) ; // Convert 0-indexed to 1-indexed
93+ } ,
94+ [ gotoPosition , file . path ]
95+ ) ;
96+
5697 // Focus search input when opened
5798 useEffect ( ( ) => {
5899 if ( searchOpen && inputRef . current ) {
@@ -95,39 +136,50 @@ export const FileViewerContent: FC<FileViewerContentProps> = ({ file }) => {
95136 }
96137
97138 return (
98- < div className = "h-full w-full relative" >
99- < CodeMirrorEditor
100- value = { file . content }
101- language = { file . language }
139+ < div className = "h-full w-full flex flex-col" >
140+ { /* Breadcrumbs */ }
141+ < EditorBreadcrumbs
102142 filePath = { file . path }
103- onChange = { handleChange }
104- onSave = { handleSave }
105- theme = { theme }
106- gotoPosition = { gotoForThisFile }
107- onGotoComplete = { clearPendingGoto }
143+ outline = { outline }
144+ activeOutlineIndex = { activeOutlineIndex }
145+ onOutlineClick = { handleOutlineClick }
108146 />
109147
110- { /* Search overlay */ }
111- { searchOpen ? (
112- < div className = "absolute top-2 right-4 flex items-center gap-2 bg-card border border-border rounded-md px-2 py-1 shadow-lg z-10" >
113- < input
114- ref = { inputRef }
115- type = "text"
116- value = { searchQuery }
117- onChange = { ( e ) : void => {
118- setSearchQuery ( e . target . value ) ;
119- } }
120- onKeyDown = { ( e ) : void => {
121- if ( e . key === 'Escape' ) closeSearch ( ) ;
122- } }
123- placeholder = "Search..."
124- className = "w-48 text-sm bg-transparent border-none outline-none"
125- />
126- < span className = "text-xs text-muted-foreground" >
127- { searchQuery . length > 0 ? 'Esc to close' : '' }
128- </ span >
129- </ div >
130- ) : null }
148+ { /* Editor */ }
149+ < div className = "flex-1 relative min-h-0" >
150+ < CodeMirrorEditor
151+ value = { file . content }
152+ language = { file . language }
153+ filePath = { file . path }
154+ onChange = { handleChange }
155+ onSave = { handleSave }
156+ theme = { theme }
157+ gotoPosition = { gotoForThisFile }
158+ onGotoComplete = { clearPendingGoto }
159+ />
160+
161+ { /* Search overlay */ }
162+ { searchOpen ? (
163+ < div className = "absolute top-2 right-4 flex items-center gap-2 bg-card border border-border rounded-md px-2 py-1 shadow-lg z-10" >
164+ < input
165+ ref = { inputRef }
166+ type = "text"
167+ value = { searchQuery }
168+ onChange = { ( e ) : void => {
169+ setSearchQuery ( e . target . value ) ;
170+ } }
171+ onKeyDown = { ( e ) : void => {
172+ if ( e . key === 'Escape' ) closeSearch ( ) ;
173+ } }
174+ placeholder = "Search..."
175+ className = "w-48 text-sm bg-transparent border-none outline-none"
176+ />
177+ < span className = "text-xs text-muted-foreground" >
178+ { searchQuery . length > 0 ? 'Esc to close' : '' }
179+ </ span >
180+ </ div >
181+ ) : null }
182+ </ div >
131183 </ div >
132184 ) ;
133185} ;
0 commit comments