@@ -15,12 +15,19 @@ export type Tab = {
1515 id : string ;
1616 label : string ; // Display name (filename)
1717 filePath : string | null ;
18+ browserHandle ?: FileSystemFileHandle | null ;
1819 content : string ;
1920 dirty : boolean ;
2021} ;
2122
22- function makeTab ( id : string , label = "Untitled" , content = "" , filePath : string | null = null ) : Tab {
23- return { id, label, content, filePath, dirty : false } ;
23+ function makeTab (
24+ id : string ,
25+ label = "Untitled" ,
26+ content = "" ,
27+ filePath : string | null = null ,
28+ browserHandle : FileSystemFileHandle | null = null
29+ ) : Tab {
30+ return { id, label, content, filePath, browserHandle, dirty : false } ;
2431}
2532
2633let _tabCounter = 0 ;
@@ -67,10 +74,11 @@ export function useEditor() {
6774 const handleContentChange = useCallback (
6875 ( value : string | undefined ) => {
6976 const v = value ?? "" ;
77+ if ( v === activeTab . content ) return ;
7078 updateTab ( activeTabId , { content : v , dirty : true } ) ;
7179 refreshPreview ( v ) ;
7280 } ,
73- [ activeTabId , updateTab , refreshPreview ]
81+ [ activeTab , activeTabId , updateTab , refreshPreview ]
7482 ) ;
7583
7684 // ── file ops ───────────────────────────────────────────────────────────────
@@ -87,7 +95,7 @@ export function useEditor() {
8795 const { content } = await Files . read ( filePath ) ;
8896 const label = filePath . split ( / [ / \\ ] / ) . pop ( ) ?? filePath ;
8997 const id = nextTabId ( ) ;
90- const newTab = makeTab ( id , label , content , filePath ) ;
98+ const newTab = makeTab ( id , label , content , filePath , null ) ;
9199 setTabs ( ( prev ) => [ ...prev , newTab ] ) ;
92100 setActiveTabId ( id ) ;
93101 refreshPreview ( content , filePath ) ;
@@ -103,10 +111,15 @@ export function useEditor() {
103111 ) ;
104112
105113 const openTextAsTab = useCallback (
106- ( label : string , content : string , filePath : string | null = null ) => {
114+ (
115+ label : string ,
116+ content : string ,
117+ filePath : string | null = null ,
118+ browserHandle : FileSystemFileHandle | null = null
119+ ) => {
107120 const id = nextTabId ( ) ;
108121 const tabLabel = label . trim ( ) || "Untitled" ;
109- const newTab = makeTab ( id , tabLabel , content , filePath ) ;
122+ const newTab = makeTab ( id , tabLabel , content , filePath , browserHandle ) ;
110123 setTabs ( ( prev ) => [ ...prev , newTab ] ) ;
111124 setActiveTabId ( id ) ;
112125 refreshPreview ( content , filePath ?? undefined ) ;
@@ -116,83 +129,75 @@ export function useEditor() {
116129
117130 const saveFile = useCallback (
118131 async ( filePath ?: string ) => {
119- const path = filePath ?? activeTab . filePath ;
132+ let path = filePath ?? activeTab . filePath ;
133+
134+ // For tabs opened without absolute path, try to resolve an existing path
135+ // without opening any dialog.
136+ if ( ! path ) {
137+ const byName = recentFiles . filter ( ( p ) => ( p . split ( / [ / \\ ] / ) . pop ( ) ?? "" ) === activeTab . label ) ;
138+ if ( byName . length === 1 ) {
139+ path = byName [ 0 ] ;
140+ } else {
141+ const candidates = [ activeTab . label , `./${ activeTab . label } ` ] ;
142+ for ( const candidate of candidates ) {
143+ try {
144+ await Files . read ( candidate ) ;
145+ path = candidate ;
146+ break ;
147+ } catch {
148+ // Keep trying other candidates.
149+ }
150+ }
151+ }
152+ }
153+
120154 if ( path ) {
121155 await Files . write ( path , activeTab . content ) ;
122- updateTab ( activeTabId , { dirty : false , filePath : path , label : path . split ( / [ / \\ ] / ) . pop ( ) ?? path } ) ;
156+ updateTab ( activeTabId , {
157+ dirty : false ,
158+ filePath : path ,
159+ label : path . split ( / [ / \\ ] / ) . pop ( ) ?? path ,
160+ } ) ;
161+ Files . addRecent ( path )
162+ . then ( ( { entries } ) => setRecentFiles ( entries ) )
163+ . catch ( console . error ) ;
123164 return ;
124165 }
125166
126167 // Save As flow: ask user for a destination when the tab has no path yet.
127168 const rawLabel = activeTab . label . trim ( ) || "Untitled" ;
128169 const suggestedName = / \. [ A - Z a - z 0 - 9 ] + $ / . test ( rawLabel ) ? rawLabel : `${ rawLabel } .md` ;
129170
130- // In Tauri desktop mode, use native save dialog and persist via backend API.
131- const isTauri = typeof window !== "undefined" && "__TAURI__" in window ;
132- if ( isTauri ) {
133- try {
134- const { save } = await import ( "@tauri-apps/plugin-dialog" ) ;
135- const target = await save ( {
136- defaultPath : suggestedName ,
137- filters : [ { name : "Markdown" , extensions : [ "md" , "markdown" , "txt" ] } ] ,
138- } ) ;
139-
140- if ( ! target ) return ;
141-
142- const resolvedPath = Array . isArray ( target ) ? target [ 0 ] : target ;
143- await Files . write ( resolvedPath , activeTab . content ) ;
144- updateTab ( activeTabId , {
145- dirty : false ,
146- filePath : resolvedPath ,
147- label : resolvedPath . split ( / [ / \\ ] / ) . pop ( ) ?? resolvedPath ,
148- } ) ;
149- Files . addRecent ( resolvedPath )
150- . then ( ( { entries } ) => setRecentFiles ( entries ) )
151- . catch ( console . error ) ;
152- } catch {
153- // No dialog capability in runtime: silently no-op.
154- }
155- return ;
156- }
157-
158- // In browser mode, use the native file save picker when available.
159- const maybePicker = ( window as Window & {
160- showSaveFilePicker ?: ( options ?: {
161- suggestedName ?: string ;
162- types ?: Array < { description ?: string ; accept : Record < string , string [ ] > } > ;
163- } ) => Promise < {
164- createWritable : ( ) => Promise < {
165- write : ( data : string ) => Promise < void > ;
166- close : ( ) => Promise < void > ;
167- } > ;
168- name ?: string ;
169- } > ;
170- } ) . showSaveFilePicker ;
171+ // Try Tauri native save dialog first (desktop mode).
172+ try {
173+ const { save } = await import ( "@tauri-apps/plugin-dialog" ) ;
174+ const target = await save ( {
175+ defaultPath : suggestedName ,
176+ filters : [ { name : "Markdown" , extensions : [ "md" , "markdown" , "txt" ] } ] ,
177+ } ) ;
171178
172- if ( ! maybePicker ) return ;
179+ if ( ! target ) return ;
173180
174- try {
175- const handle = await maybePicker ( {
176- suggestedName,
177- types : [
178- {
179- description : "Markdown files" ,
180- accept : {
181- "text/markdown" : [ ".md" , ".markdown" ] ,
182- "text/plain" : [ ".txt" ] ,
183- } ,
184- } ,
185- ] ,
181+ const resolvedPath = Array . isArray ( target ) ? target [ 0 ] : target ;
182+ await Files . write ( resolvedPath , activeTab . content ) ;
183+ updateTab ( activeTabId , {
184+ dirty : false ,
185+ filePath : resolvedPath ,
186+ browserHandle : null ,
187+ label : resolvedPath . split ( / [ / \\ ] / ) . pop ( ) ?? resolvedPath ,
186188 } ) ;
187- const writable = await handle . createWritable ( ) ;
188- await writable . write ( activeTab . content ) ;
189- await writable . close ( ) ;
190- updateTab ( activeTabId , { dirty : false , label : handle . name || suggestedName } ) ;
189+ Files . addRecent ( resolvedPath )
190+ . then ( ( { entries } ) => setRecentFiles ( entries ) )
191+ . catch ( console . error ) ;
192+ return ;
191193 } catch {
192- // No picker capability (or picker cancelled): silently no-op .
194+ // Not running with Tauri dialog capability .
193195 }
196+
197+ // Outside Tauri dialog capability, do nothing silently.
198+ return ;
194199 } ,
195- [ activeTab , activeTabId , updateTab ]
200+ [ activeTab , activeTabId , recentFiles , updateTab ]
196201 ) ;
197202
198203 const newTab = useCallback ( ( ) => {
0 commit comments