import { App , Plugin , PluginSettingTab , Setting , MarkdownView , TAbstractFile } from 'obsidian' ; import * as CodeMirror from "codemirror" ; interface PluginSettings { dbFileName : string ; delayAfterFileOpening : number ; } const DEFAULT_SETTINGS = { dbFileName : '.obsidian/plugins/remember-cursor-position/cursor-positions.json' , delayAfterFileOpening : 100 } ; interface EphemeralState { cursor ? : { from : { ch : number ; line : number ; } ; to : { ch : number ; line : number ; } ; } ; scroll ? : number ; } export default class RememberCursorPosition extends Plugin { settings : PluginSettings ; db : { [ file_path ] : EphemeralState ; } ; lastEphemeralState : EphemeralState ; lastLoadedFileName : string ; loadingFile = false ; async onload ( ) { await this . loadSettings ( ) ; try { this . db = await this . readDb ( ) ; } catch ( e ) { console . error ( "Remember Cursor Position plugin can't read database: " + e ) ; this . db = { } ; } this . addSettingTab ( new SettingTab ( this . app , this ) ) ; this . registerEvent ( this . app . workspace . on ( 'file-open' , ( file ) => this . restoreEphemeralState ( ) ) ) ; this . registerEvent ( this . app . workspace . on ( 'quit' , ( ) => { this . writeDb ( this . db ) ; } ) ) ; this . registerEvent ( this . app . vault . on ( 'rename' , ( file , oldPath ) => this . renameFile ( file , oldPath ) ) ) ; this . registerEvent ( this . app . vault . on ( 'delete' , ( file ) => this . deleteFile ( file ) ) ) ; this . registerInterval ( window . setInterval ( ( ) => this . checkEphemeralStateChanged ( ) , 100 ) ) ; this . restoreEphemeralState ( ) ; } renameFile ( file , oldPath ) { let newName = file . path ; let oldName = oldPath ; this . db [ newName ] = this . db [ oldName ] ; delete this . db [ oldName ] ; } deleteFile ( file ) { let fileName = file . path ; delete this . db [ fileName ] ; } checkEphemeralStateChanged ( ) { let fileName = this . app . workspace . getActiveFile ( ) ?. path ; if ( ! fileName || ! this . lastLoadedFileName || fileName != this . lastLoadedFileName || this . loadingFile ) return ; let st = this . getEphemeralState ( ) ; if ( ! this . lastEphemeralState ) this . lastEphemeralState = st ; if ( ! this . isEphemeralStatesEquals ( st , this . lastEphemeralState ) ) { this . saveEphemeralState ( st ) ; this . lastEphemeralState = st ; } } isEphemeralStatesEquals ( state1 , state2 ) { if ( state1 . cursor && ! state2 . cursor ) return false ; if ( ! state1 . cursor && state2 . cursor ) return false ; if ( state1 . cursor ) { if ( state1 . cursor . from . ch != state2 . cursor . from . ch ) return false ; if ( state1 . cursor . from . line != state2 . cursor . from . line ) return false ; if ( state1 . cursor . to . ch != state2 . cursor . to . ch ) return false ; if ( state1 . cursor . to . line != state2 . cursor . to . line ) return false ; } if ( state1 . scroll && ! state2 . scroll ) return false ; if ( ! state1 . scroll && state2 . scroll ) return false ; if ( state1 . scroll ) { if ( state1 . scroll != state2 . scroll ) return false ; } return true ; } async saveEphemeralState ( st ) { let fileName = this . app . workspace . getActiveFile ( ) ?. path ; if ( fileName && fileName == this . lastLoadedFileName ) { this . db [ fileName ] = st ; } } async restoreEphemeralState ( ) { let fileName = this . app . workspace . getActiveFile ( ) ?. path ; if ( fileName && this . loadingFile && this . lastLoadedFileName == fileName ) return ; this . loadingFile = true ; if ( this . lastLoadedFileName != fileName ) { this . lastEphemeralState = { } ; this . lastLoadedFileName = fileName ; if ( fileName ) { let st = this . db [ fileName ] ; if ( st ) { await this . delay ( this . settings . delayAfterFileOpening ) ; let scroll ; for ( let i = 0 ; i < 20 ; i ++ ) { scroll = this . app . workspace . getActiveViewOfType ( MarkdownView ) ?. currentMode ?. getScroll ( ) ; if ( scroll !== null ) break ; await this . delay ( 10 ) ; } if ( scroll === 0 ) { for ( let i = 0 ; i < 20 ; i ++ ) { this . setEphemeralState ( st ) ; await this . delay ( 10 ) ; } } } this . lastEphemeralState = st ; } this . loadingFile = false ; } } async readDb ( ) { let db = { } ; if ( await this . app . vault . adapter . exists ( this . settings . dbFileName ) ) { let data = await this . app . vault . adapter . read ( this . settings . dbFileName ) ; db = JSON . parse ( data ) ; } return db ; } async writeDb ( db ) { let newParentFolder = this . settings . dbFileName . substring ( 0 , this . settings . dbFileName . lastIndexOf ( "/" ) ) ; if ( ! await this . app . vault . adapter . exists ( newParentFolder ) ) this . app . vault . adapter . mkdir ( newParentFolder ) ; this . app . vault . adapter . write ( this . settings . dbFileName , JSON . stringify ( db ) ) ; } getEphemeralState ( ) { let state = { } ; state . scroll = Number ( this . app . workspace . getActiveViewOfType ( MarkdownView ) ?. currentMode ?. getScroll ( ) ?. toFixed ( 4 ) ) ; let editor = this . getEditor ( ) ; if ( editor ) { let from = editor . getCursor ( "anchor" ) ; let to = editor . getCursor ( "head" ) ; if ( from && to ) { state . cursor = { from : { ch : from . ch , line : from . line } , to : { ch : to . ch , line : to . line } } ; } } return state ; } setEphemeralState ( state ) { const view = this . app . workspace . getActiveViewOfType ( MarkdownView ) ; if ( state . cursor ) { let editor = this . getEditor ( ) ; if ( editor ) { editor . setSelection ( state . cursor . from , state . cursor . to , { scroll : false } ) ; } } if ( view && state . scroll ) { view . setEphemeralState ( state ) ; } } private getEditor ( ) { return this . app . workspace . getActiveViewOfType ( MarkdownView ) ?. sourceMode . cmEditor ; } async loadSettings ( ) { this . settings = Object . assign ( { } , DEFAULT_SETTINGS , await this . loadData ( ) ) ; } async saveSettings ( ) { await this . saveData ( this . settings ) ; } async delay ( ms ) { return new Promise ( resolve => setTimeout ( resolve , ms ) ) ; } } class SettingTab extends PluginSettingTab { plugin : RememberCursorPosition ; constructor ( app , plugin ) { super ( app , plugin ) ; this . plugin = plugin ; } display ( ) { let { containerEl } = this ; containerEl . empty ( ) ; containerEl . createEl ( 'h2' , { text : 'Remember cursor position - Settings' } ) ; new Setting ( containerEl ) . setName ( 'Data file name' ) . setDesc ( 'Save positions to this file' ) . addText ( text => text . setPlaceholder ( 'Example: cursor-positions.json' ) . setValue ( this . plugin . settings . dbFileName ) . onChange ( async ( value ) => { this . plugin . settings . dbFileName = value ; await this . plugin . saveSettings ( ) ; } ) ) ; new Setting ( containerEl ) . setName ( 'Delay after opening a new note' ) . setDesc ( 'This plugin shouldn\'t scroll if you used a link to the note header like [link](note.md#header). If it did, then increase the delay until everything works. If you are not using links to page sections, set the delay to zero (slider to the left). Slider values: 0-300 ms (default value: 100 ms).' ) . addSlider ( text => text . setLimits ( 0 , 300 , 10 ) . setValue ( this . plugin . settings . delayAfterFileOpening ) . onChange ( async ( value ) => { this . plugin . settings . delayAfterFileOpening = value ; await this . plugin . saveSettings ( ) ; } ) ) ; } }