11import { EventRef , Menu , Notice , Platform , Plugin , TAbstractFile , TFile , FileSystemAdapter } from "obsidian" ;
22import { AutoGitSettings , AutoGitSettingTab , DEFAULT_SETTINGS } from "./settings" ;
3- import { getChangedFiles , commitAll , push , pull , getFileStatuses , getConflictFiles , markConflictsResolved , revertAll , revertFile , FileStatus , getChangedFilesSync , commitAndPushSync } from "./git" ;
3+ import { getChangedFiles , commitAll , push , pull , getFileStatuses , getConflictFiles , markConflictsResolved , revertAll , revertFile , FileStatus , getChangedFilesSync , commitSyncAndPushDetached } from "./git" ;
44import { renderTemplate } from "./template" ;
55import { t } from "./i18n" ;
66import { RevertConfirmModal } from "./modals" ;
@@ -15,11 +15,13 @@ export default class AutoGitPlugin extends Plugin {
1515 private statusRefreshInterval : number | null = null ;
1616 private statusRefreshTimeout : number | null = null ;
1717 private currentStatuses : Map < string , FileStatus > = new Map ( ) ;
18+ private previousStatuses : Map < string , FileStatus > = new Map ( ) ;
1819 private conflictFiles : Set < string > = new Set ( ) ;
1920 private _hasConflicts = false ;
2021 private resolveConflictCommand : { id : string } | null = null ;
2122 private ribbonIconEl : HTMLElement | null = null ;
2223 private beforeUnloadHandler : ( ( ) => void ) | null = null ;
24+ private mutationObserver : MutationObserver | null = null ;
2325
2426 async onload ( ) {
2527 await this . loadSettings ( ) ;
@@ -56,9 +58,13 @@ export default class AutoGitPlugin extends Plugin {
5658 } ) ;
5759
5860 this . setupVaultListeners ( ) ;
59- this . updateStatusBadges ( ) ;
6061 this . setupFileContextMenu ( ) ;
6162
63+ // Wait for layout ready before initializing badges
64+ this . app . workspace . onLayoutReady ( ( ) => {
65+ this . updateStatusBadges ( ) ;
66+ } ) ;
67+
6268 // Auto pull on open
6369 if ( this . settings . autoPullOnOpen && ! Platform . isMobileApp ) {
6470 // Delay to ensure vault is ready
@@ -84,7 +90,8 @@ export default class AutoGitPlugin extends Plugin {
8490 if ( this . settings . includeFileList ) {
8591 message += "\n\n" + changedFiles . join ( "\n" ) ;
8692 }
87- commitAndPushSync ( cwd , this . settings . gitPath , message ) ;
93+ // Sync commit + detached push (push runs in background after app closes)
94+ commitSyncAndPushDetached ( cwd , this . settings . gitPath , message ) ;
8895 }
8996 }
9097 }
@@ -108,6 +115,10 @@ export default class AutoGitPlugin extends Plugin {
108115 window . clearTimeout ( this . statusRefreshTimeout ) ;
109116 this . statusRefreshTimeout = null ;
110117 }
118+ if ( this . mutationObserver ) {
119+ this . mutationObserver . disconnect ( ) ;
120+ this . mutationObserver = null ;
121+ }
111122 if ( this . ribbonIconEl ) {
112123 this . ribbonIconEl . remove ( ) ;
113124 this . ribbonIconEl = null ;
@@ -138,8 +149,6 @@ export default class AutoGitPlugin extends Plugin {
138149 }
139150
140151 private setupVaultListeners ( ) {
141- if ( ! this . settings . autoCommit ) return ;
142-
143152 if ( Platform . isMobileApp ) {
144153 new Notice ( t ( ) . noticeMobileNotSupported ) ;
145154 return ;
@@ -157,8 +166,13 @@ export default class AutoGitPlugin extends Plugin {
157166 if ( ! ( file instanceof TFile ) ) return ;
158167 if ( this . shouldIgnore ( file . path ) ) return ;
159168
160- this . scheduleCommit ( ) ;
169+ // Always refresh badges on file change
161170 this . scheduleStatusRefresh ( ) ;
171+
172+ // Only schedule commit if autoCommit is enabled
173+ if ( this . settings . autoCommit ) {
174+ this . scheduleCommit ( ) ;
175+ }
162176 }
163177
164178 private shouldIgnore ( path : string ) : boolean {
@@ -450,6 +464,11 @@ export default class AutoGitPlugin extends Plugin {
450464 window . clearTimeout ( this . statusRefreshTimeout ) ;
451465 this . statusRefreshTimeout = null ;
452466 }
467+ // Disconnect existing observer
468+ if ( this . mutationObserver ) {
469+ this . mutationObserver . disconnect ( ) ;
470+ this . mutationObserver = null ;
471+ }
453472
454473 if ( Platform . isMobileApp || ! this . settings . showStatusBadge ) {
455474 this . clearStatusBadges ( ) ;
@@ -459,10 +478,59 @@ export default class AutoGitPlugin extends Plugin {
459478 // Initial refresh
460479 this . refreshStatusBadges ( ) ;
461480
462- // Refresh every 5 seconds
463- this . statusRefreshInterval = window . setInterval ( ( ) => {
464- this . refreshStatusBadges ( ) ;
465- } , 5000 ) ;
481+ // Setup polling if interval > 0
482+ const interval = this . settings . badgeRefreshInterval ;
483+ if ( interval > 0 ) {
484+ this . statusRefreshInterval = window . setInterval ( ( ) => {
485+ this . refreshStatusBadges ( ) ;
486+ } , interval * 1000 ) ;
487+ }
488+
489+ // Setup MutationObserver for virtualized file list
490+ this . setupMutationObserver ( ) ;
491+ }
492+
493+ private setupMutationObserver ( ) {
494+ // Find file explorer container
495+ const container = document . querySelector ( ".nav-files-container" ) ;
496+ if ( ! container ) return ;
497+
498+ this . mutationObserver = new MutationObserver ( ( mutations ) => {
499+ for ( const mutation of mutations ) {
500+ mutation . addedNodes . forEach ( ( node ) => {
501+ if ( node instanceof HTMLElement ) {
502+ this . applyBadgesToNewNodes ( node ) ;
503+ }
504+ } ) ;
505+ }
506+ } ) ;
507+
508+ this . mutationObserver . observe ( container , {
509+ childList : true ,
510+ subtree : true ,
511+ } ) ;
512+ }
513+
514+ private applyBadgesToNewNodes ( node : HTMLElement ) {
515+ // Check if node itself is a file/folder title
516+ const items = node . matches ( ".nav-file-title, .nav-folder-title" )
517+ ? [ node ]
518+ : Array . from ( node . querySelectorAll ( ".nav-file-title, .nav-folder-title" ) ) ;
519+
520+ const allStatuses = this . getMergedStatuses ( ) ;
521+
522+ for ( const item of items ) {
523+ const path = item . getAttribute ( "data-path" ) ;
524+ if ( ! path ) continue ;
525+
526+ // Skip if already has badge
527+ if ( item . querySelector ( ".git-status-badge" ) ) continue ;
528+
529+ const status = allStatuses . get ( path ) ;
530+ if ( status ) {
531+ this . addBadgeToElement ( item , status ) ;
532+ }
533+ }
466534 }
467535
468536 private scheduleStatusRefresh ( ) {
@@ -488,7 +556,12 @@ export default class AutoGitPlugin extends Plugin {
488556
489557 getFileStatuses ( cwd , this . settings . gitPath ) . then ( ( statuses ) => {
490558 this . currentStatuses = statuses ;
491- this . updateBadgesInDOM ( ) ;
559+ // Use full update if first time, otherwise use diff
560+ if ( this . previousStatuses . size === 0 ) {
561+ this . updateBadgesInDOM ( ) ;
562+ } else {
563+ this . updateBadgesDiff ( ) ;
564+ }
492565 } ) . catch ( ( ) => {
493566 // Ignore errors
494567 } ) ;
@@ -497,6 +570,67 @@ export default class AutoGitPlugin extends Plugin {
497570 private clearStatusBadges ( ) {
498571 document . querySelectorAll ( ".git-status-badge" ) . forEach ( ( el ) => el . remove ( ) ) ;
499572 this . currentStatuses . clear ( ) ;
573+ this . previousStatuses . clear ( ) ;
574+ }
575+
576+ private getMergedStatuses ( ) : Map < string , FileStatus > {
577+ const merged = new Map ( this . currentStatuses ) ;
578+ this . conflictFiles . forEach ( ( file ) => {
579+ merged . set ( file , "U" as FileStatus ) ;
580+ } ) ;
581+
582+ // Add folder statuses
583+ const folderStatuses = this . calculateFolderStatuses ( merged ) ;
584+ folderStatuses . forEach ( ( status , path ) => {
585+ merged . set ( path , status ) ;
586+ } ) ;
587+
588+ return merged ;
589+ }
590+
591+ private updateBadgesDiff ( ) {
592+ const newStatuses = this . getMergedStatuses ( ) ;
593+
594+ // Find paths to remove (in previous but not in new, or status changed)
595+ for ( const [ path , oldStatus ] of this . previousStatuses ) {
596+ const newStatus = newStatuses . get ( path ) ;
597+ if ( ! newStatus || newStatus !== oldStatus ) {
598+ this . removeBadgeFromPath ( path ) ;
599+ }
600+ }
601+
602+ // Find paths to add/update (in new but not in previous, or status changed)
603+ for ( const [ path , status ] of newStatuses ) {
604+ const oldStatus = this . previousStatuses . get ( path ) ;
605+ if ( oldStatus !== status ) {
606+ this . updateBadgeForPath ( path , status ) ;
607+ }
608+ }
609+
610+ this . previousStatuses = newStatuses ;
611+ }
612+
613+ private removeBadgeFromPath ( path : string ) {
614+ const escapedPath = CSS . escape ( path ) ;
615+ const selector = `.nav-file-title[data-path="${ escapedPath } "], .nav-folder-title[data-path="${ escapedPath } "]` ;
616+ const el = document . querySelector ( selector ) ;
617+ if ( el ) {
618+ const badge = el . querySelector ( ".git-status-badge" ) ;
619+ if ( badge ) badge . remove ( ) ;
620+ }
621+ }
622+
623+ private updateBadgeForPath ( path : string , status : FileStatus ) {
624+ const escapedPath = CSS . escape ( path ) ;
625+ const selector = `.nav-file-title[data-path="${ escapedPath } "], .nav-folder-title[data-path="${ escapedPath } "]` ;
626+ const el = document . querySelector ( selector ) ;
627+ if ( el ) {
628+ // Remove existing badge if any
629+ const existingBadge = el . querySelector ( ".git-status-badge" ) ;
630+ if ( existingBadge ) existingBadge . remove ( ) ;
631+ // Add new badge
632+ this . addBadgeToElement ( el , status ) ;
633+ }
500634 }
501635
502636 private updateBadgesInDOM ( ) {
@@ -533,6 +667,9 @@ export default class AutoGitPlugin extends Plugin {
533667 this . addBadgeToElement ( item , status ) ;
534668 }
535669 } ) ;
670+
671+ // Update previous statuses for diff tracking
672+ this . previousStatuses = this . getMergedStatuses ( ) ;
536673 }
537674
538675 private calculateFolderStatuses ( statuses : Map < string , FileStatus > ) : Map < string , FileStatus > {
0 commit comments