2025-08-02 12:09:34 +08:00
/ *
THIS IS A GENERATED / BUNDLED FILE BY ROLLUP
if you want to view the source visit the plugins github repository
* /
'use strict' ;
var obsidian = require ( 'obsidian' ) ;
2026-05-06 17:32:44 +08:00
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Copyright ( c ) Microsoft Corporation .
Permission to use , copy , modify , and / or distribute this software for any
purpose with or without fee is hereby granted .
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS . IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL , DIRECT ,
INDIRECT , OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE , DATA OR PROFITS , WHETHER IN AN ACTION OF CONTRACT , NEGLIGENCE OR
OTHER TORTIOUS ACTION , ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE .
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
function _ _awaiter ( thisArg , _arguments , P , generator ) {
function adopt ( value ) { return value instanceof P ? value : new P ( function ( resolve ) { resolve ( value ) ; } ) ; }
return new ( P || ( P = Promise ) ) ( function ( resolve , reject ) {
function fulfilled ( value ) { try { step ( generator . next ( value ) ) ; } catch ( e ) { reject ( e ) ; } }
function rejected ( value ) { try { step ( generator [ "throw" ] ( value ) ) ; } catch ( e ) { reject ( e ) ; } }
function step ( result ) { result . done ? resolve ( result . value ) : adopt ( result . value ) . then ( fulfilled , rejected ) ; }
step ( ( generator = generator . apply ( thisArg , _arguments || [ ] ) ) . next ( ) ) ;
} ) ;
}
typeof SuppressedError === "function" ? SuppressedError : function ( error , suppressed , message ) {
var e = new Error ( message ) ;
return e . name = "SuppressedError" , e . error = error , e . suppressed = suppressed , e ;
2025-08-02 12:09:34 +08:00
} ;
const SAFE _DB _FLUSH _INTERVAL = 5000 ;
2026-05-06 17:32:44 +08:00
const DEFAULT _DB _FILENAME _LEGACY = '.obsidian/plugins/remember-cursor-position/cursor-positions.json' ;
2025-08-02 12:09:34 +08:00
const DEFAULT _SETTINGS = {
2026-05-06 17:32:44 +08:00
dbFileName : '' ,
2025-08-02 12:09:34 +08:00
delayAfterFileOpening : 100 ,
saveTimer : SAFE _DB _FLUSH _INTERVAL ,
2026-05-06 17:32:44 +08:00
pruneOrphans : false ,
maxAgeDays : 0 ,
maxCount : 0 ,
2025-08-02 12:09:34 +08:00
} ;
class RememberCursorPosition extends obsidian . Plugin {
constructor ( ) {
super ( ... arguments ) ;
2026-05-06 17:32:44 +08:00
this . loadedLeafIdList = [ ] ;
2025-08-02 12:09:34 +08:00
this . loadingFile = false ;
}
onload ( ) {
return _ _awaiter ( this , void 0 , void 0 , function * ( ) {
yield this . loadSettings ( ) ;
try {
this . db = yield this . readDb ( ) ;
2026-05-06 17:32:44 +08:00
this . pruneDb ( ) ;
2025-08-02 12:09:34 +08:00
this . lastSavedDb = yield this . readDb ( ) ;
}
catch ( e ) {
console . error ( "Remember Cursor Position plugin can\'t read database: " + e ) ;
this . db = { } ;
this . lastSavedDb = { } ;
}
this . addSettingTab ( new SettingTab ( this . app , this ) ) ;
2026-05-06 17:32:44 +08:00
this . registerEvent ( this . app . workspace . on ( 'file-open' , ( file ) => this . restoreEphemeralState ( file ) ) ) ;
2025-08-02 12:09:34 +08:00
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 ) ) ) ;
//todo: replace by scroll and mouse cursor move events
this . registerInterval ( window . setInterval ( ( ) => this . checkEphemeralStateChanged ( ) , 100 ) ) ;
2026-05-06 17:32:44 +08:00
this . saveTimerIntervalId = this . registerInterval ( window . setInterval ( ( ) => this . writeDb ( this . db ) , this . settings . saveTimer ) ) ;
2025-08-02 12:09:34 +08:00
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 ( ) {
var _a ;
let fileName = ( _a = this . app . workspace . getActiveFile ( ) ) === null || _a === void 0 ? void 0 : _a . path ;
//waiting for load new file
if ( ! fileName || ! this . lastLoadedFileName || fileName != this . lastLoadedFileName || this . loadingFile )
return ;
let st = this . getEphemeralState ( ) ;
if ( ! this . lastEphemeralState )
this . lastEphemeralState = st ;
if ( ! isNaN ( st . scroll ) && ! 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 && state1 . scroll != state2 . scroll )
return false ;
return true ;
}
saveEphemeralState ( st ) {
var _a ;
return _ _awaiter ( this , void 0 , void 0 , function * ( ) {
let fileName = ( _a = this . app . workspace . getActiveFile ( ) ) === null || _a === void 0 ? void 0 : _a . path ;
if ( fileName && fileName == this . lastLoadedFileName ) { //do not save if file changed or was not loaded
2026-05-06 17:32:44 +08:00
this . db [ fileName ] = Object . assign ( Object . assign ( { } , st ) , { lastModified : Date . now ( ) } ) ;
2025-08-02 12:09:34 +08:00
}
} ) ;
}
2026-05-06 17:32:44 +08:00
restoreEphemeralState ( file ) {
var _a ;
2025-08-02 12:09:34 +08:00
return _ _awaiter ( this , void 0 , void 0 , function * ( ) {
let fileName = ( _a = this . app . workspace . getActiveFile ( ) ) === null || _a === void 0 ? void 0 : _a . path ;
if ( fileName && this . loadingFile && this . lastLoadedFileName == fileName ) //if already started loading
return ;
2026-05-06 17:32:44 +08:00
let activeLeaf = this . app . workspace . getMostRecentLeaf ( ) ;
//@ts-ignore no-official-API
if ( activeLeaf && this . loadedLeafIdList . includes ( activeLeaf . id + ':' + activeLeaf . getViewState ( ) . state . file ) )
return ;
this . loadedLeafIdList = [ ] ;
this . app . workspace . iterateAllLeaves ( ( leaf ) => {
if ( leaf . getViewState ( ) . type === "markdown" ) {
//@ts-ignore no-official-API
this . loadedLeafIdList . push ( leaf . id + ':' + leaf . getViewState ( ) . state . file ) ;
}
} ) ;
2025-08-02 12:09:34 +08:00
this . loadingFile = true ;
if ( this . lastLoadedFileName != fileName ) {
this . lastEphemeralState = { } ;
this . lastLoadedFileName = fileName ;
2026-05-06 17:32:44 +08:00
let st ;
2025-08-02 12:09:34 +08:00
if ( fileName ) {
2026-05-06 17:32:44 +08:00
st = this . db [ fileName ] ;
2025-08-02 12:09:34 +08:00
if ( st ) {
//waiting for load note
yield this . delay ( this . settings . delayAfterFileOpening ) ;
2026-05-06 17:32:44 +08:00
// Don't scroll when a link scrolls and highlights text
// i.e. if file is open by links like [link](note.md#header) and wikilinks
// See #10, #32, #46, #51
let containsFlashingSpan = this . app . workspace . containerEl . querySelector ( '.is-flashing' ) ;
if ( ! containsFlashingSpan ) {
2025-08-02 12:09:34 +08:00
yield this . delay ( 10 ) ;
2026-05-06 17:32:44 +08:00
this . setEphemeralState ( st ) ;
2025-08-02 12:09:34 +08:00
}
}
}
2026-05-06 17:32:44 +08:00
this . lastEphemeralState = st ;
2025-08-02 12:09:34 +08:00
}
2026-05-06 17:32:44 +08:00
this . loadingFile = false ;
2025-08-02 12:09:34 +08:00
} ) ;
}
2026-05-06 17:32:44 +08:00
pruneDb ( ) {
var _a ;
const { pruneOrphans , maxAgeDays , maxCount } = this . settings ;
if ( pruneOrphans ) {
for ( const key of Object . keys ( this . db ) ) {
if ( ! this . app . vault . getAbstractFileByPath ( key ) ) {
delete this . db [ key ] ;
}
}
}
if ( maxAgeDays > 0 ) {
const cutoff = Date . now ( ) - maxAgeDays * 86400000 ;
for ( const key of Object . keys ( this . db ) ) {
if ( ( ( _a = this . db [ key ] . lastModified ) !== null && _a !== void 0 ? _a : 0 ) < cutoff ) {
delete this . db [ key ] ;
}
}
}
if ( maxCount > 0 && Object . keys ( this . db ) . length > maxCount ) {
const sorted = Object . entries ( this . db )
. sort ( ( a , b ) => { var _a , _b ; return ( ( _a = b [ 1 ] . lastModified ) !== null && _a !== void 0 ? _a : 0 ) - ( ( _b = a [ 1 ] . lastModified ) !== null && _b !== void 0 ? _b : 0 ) ; } ) ;
this . db = Object . fromEntries ( sorted . slice ( 0 , maxCount ) ) ;
}
}
2025-08-02 12:09:34 +08:00
readDb ( ) {
return _ _awaiter ( this , void 0 , void 0 , function * ( ) {
let db = { } ;
if ( yield this . app . vault . adapter . exists ( this . settings . dbFileName ) ) {
let data = yield this . app . vault . adapter . read ( this . settings . dbFileName ) ;
db = JSON . parse ( data ) ;
2026-05-06 17:32:44 +08:00
const now = Date . now ( ) ;
for ( const key of Object . keys ( db ) ) {
if ( db [ key ] . lastModified === undefined ) {
db [ key ] . lastModified = now ;
}
}
2025-08-02 12:09:34 +08:00
}
return db ;
} ) ;
}
writeDb ( db ) {
return _ _awaiter ( this , void 0 , void 0 , function * ( ) {
//create folder for db file if not exist
let newParentFolder = this . settings . dbFileName . substring ( 0 , this . settings . dbFileName . lastIndexOf ( "/" ) ) ;
if ( ! ( yield this . app . vault . adapter . exists ( newParentFolder ) ) )
this . app . vault . adapter . mkdir ( newParentFolder ) ;
if ( JSON . stringify ( this . db ) !== JSON . stringify ( this . lastSavedDb ) ) {
this . app . vault . adapter . write ( this . settings . dbFileName , JSON . stringify ( db ) ) ;
this . lastSavedDb = JSON . parse ( JSON . stringify ( db ) ) ;
}
} ) ;
}
getEphemeralState ( ) {
// let state: EphemeralState = this.app.workspace.getActiveViewOfType(MarkdownView)?.getEphemeralState(); //doesn't work properly
var _a , _b , _c ;
let state = { } ;
state . scroll = Number ( ( _c = ( _b = ( _a = this . app . workspace . getActiveViewOfType ( obsidian . MarkdownView ) ) === null || _a === void 0 ? void 0 : _a . currentMode ) === null || _b === void 0 ? void 0 : _b . getScroll ( ) ) === null || _c === void 0 ? void 0 : _c . 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 ( obsidian . MarkdownView ) ;
if ( state . cursor ) {
let editor = this . getEditor ( ) ;
if ( editor ) {
editor . setSelection ( state . cursor . from , state . cursor . to ) ;
}
}
if ( view && state . scroll ) {
view . setEphemeralState ( state ) ;
// view.previewMode.applyScroll(state.scroll);
// view.sourceMode.applyScroll(state.scroll);
}
}
getEditor ( ) {
var _a ;
return ( _a = this . app . workspace . getActiveViewOfType ( obsidian . MarkdownView ) ) === null || _a === void 0 ? void 0 : _a . editor ;
}
loadSettings ( ) {
return _ _awaiter ( this , void 0 , void 0 , function * ( ) {
let settings = Object . assign ( { } , DEFAULT _SETTINGS , yield this . loadData ( ) ) ;
if ( ( settings === null || settings === void 0 ? void 0 : settings . saveTimer ) < SAFE _DB _FLUSH _INTERVAL ) {
settings . saveTimer = SAFE _DB _FLUSH _INTERVAL ;
}
2026-05-06 17:32:44 +08:00
if ( ! settings . dbFileName || settings . dbFileName === DEFAULT _DB _FILENAME _LEGACY ) {
settings . dbFileName = this . manifest . dir + '/cursor-positions.json' ;
}
2025-08-02 12:09:34 +08:00
this . settings = settings ;
} ) ;
}
saveSettings ( ) {
return _ _awaiter ( this , void 0 , void 0 , function * ( ) {
yield this . saveData ( this . settings ) ;
} ) ;
}
delay ( ms ) {
return _ _awaiter ( this , void 0 , void 0 , function * ( ) {
return new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
} ) ;
}
}
class SettingTab extends obsidian . PluginSettingTab {
constructor ( app , plugin ) {
super ( app , plugin ) ;
this . plugin = plugin ;
}
display ( ) {
let { containerEl } = this ;
containerEl . empty ( ) ;
containerEl . createEl ( 'h2' , { text : 'Remember cursor position - Settings' } ) ;
2026-05-06 17:32:44 +08:00
new obsidian . SettingGroup ( containerEl )
. addSetting ( ( setting ) => setting
2025-08-02 12:09:34 +08:00
. setName ( 'Data file name' )
. setDesc ( 'Save positions to this file' )
. addText ( ( text ) => text
. setPlaceholder ( 'Example: cursor-positions.json' )
. setValue ( this . plugin . settings . dbFileName )
. onChange ( ( value ) => _ _awaiter ( this , void 0 , void 0 , function * ( ) {
this . plugin . settings . dbFileName = value ;
yield this . plugin . saveSettings ( ) ;
2026-05-06 17:32:44 +08:00
} ) ) ) )
. addSetting ( ( setting ) => setting
2025-08-02 12:09:34 +08:00
. 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 )
2026-05-06 17:32:44 +08:00
. setDynamicTooltip ( )
2025-08-02 12:09:34 +08:00
. setValue ( this . plugin . settings . delayAfterFileOpening )
. onChange ( ( value ) => _ _awaiter ( this , void 0 , void 0 , function * ( ) {
this . plugin . settings . delayAfterFileOpening = value ;
yield this . plugin . saveSettings ( ) ;
2026-05-06 17:32:44 +08:00
} ) ) ) )
. addSetting ( ( setting ) => setting
2025-08-02 12:09:34 +08:00
. setName ( 'Delay between saving the cursor position to file' )
. setDesc ( "Useful for multi-device users. If you don't want to wait until closing Obsidian to the cursor position been saved." )
. addSlider ( ( text ) => text
. setLimits ( SAFE _DB _FLUSH _INTERVAL , SAFE _DB _FLUSH _INTERVAL * 10 , 10 )
2026-05-06 17:32:44 +08:00
. setDynamicTooltip ( )
2025-08-02 12:09:34 +08:00
. setValue ( this . plugin . settings . saveTimer )
. onChange ( ( value ) => _ _awaiter ( this , void 0 , void 0 , function * ( ) {
this . plugin . settings . saveTimer = value ;
yield this . plugin . saveSettings ( ) ;
2026-05-06 17:32:44 +08:00
window . clearInterval ( this . plugin . saveTimerIntervalId ) ;
this . plugin . saveTimerIntervalId = this . plugin . registerInterval ( window . setInterval ( ( ) => this . plugin . writeDb ( this . plugin . db ) , value ) ) ;
} ) ) ) ) ;
const { pruneOrphans , maxAgeDays , maxCount } = this . plugin . settings ;
const pruningEnabled = pruneOrphans || maxAgeDays > 0 || maxCount > 0 ;
const entryCount = Object . keys ( this . plugin . db ) . length ;
new obsidian . SettingGroup ( containerEl )
. setHeading ( 'Pruning' )
. addSetting ( ( setting ) => setting
. setName ( 'Remove entries for deleted or missing files' )
. setDesc ( 'On startup, remove saved positions for files that no longer exist in the vault. ' +
'Disable this if you use junctions, removable drives, or other setups where files may be temporarily unavailable.' )
. addToggle ( ( toggle ) => toggle
. setValue ( this . plugin . settings . pruneOrphans )
. onChange ( ( value ) => _ _awaiter ( this , void 0 , void 0 , function * ( ) {
this . plugin . settings . pruneOrphans = value ;
yield this . plugin . saveSettings ( ) ;
this . display ( ) ;
} ) ) ) )
. addSetting ( ( setting ) => setting
. setName ( 'Remove entries older than' )
. setDesc ( 'On startup, remove saved positions for files that have not been visited within the selected period.' )
. addDropdown ( ( drop ) => drop
. addOption ( '30' , '30 days' )
. addOption ( '60' , '60 days' )
. addOption ( '90' , '90 days' )
. addOption ( '365' , '1 year' )
. addOption ( '0' , 'Never' )
. setValue ( String ( this . plugin . settings . maxAgeDays ) )
. onChange ( ( value ) => _ _awaiter ( this , void 0 , void 0 , function * ( ) {
this . plugin . settings . maxAgeDays = Number ( value ) ;
yield this . plugin . saveSettings ( ) ;
this . display ( ) ;
} ) ) ) )
. addSetting ( ( setting ) => setting
. setName ( 'Maximum number of entries to keep' )
. setDesc ( 'On startup, if the number of saved positions exceeds this limit, the oldest entries are removed. Most-recently visited files are kept.' )
. addDropdown ( ( drop ) => drop
. addOption ( '50' , '50' )
. addOption ( '100' , '100' )
. addOption ( '250' , '250' )
. addOption ( '500' , '500' )
. addOption ( '0' , 'Never' )
. setValue ( String ( this . plugin . settings . maxCount ) )
. onChange ( ( value ) => _ _awaiter ( this , void 0 , void 0 , function * ( ) {
this . plugin . settings . maxCount = Number ( value ) ;
yield this . plugin . saveSettings ( ) ;
this . display ( ) ;
} ) ) ) )
. addSetting ( ( setting ) => setting
. setName ( 'Apply pruning rules' )
. setDesc ( ` Currently tracking ${ entryCount } ${ entryCount === 1 ? 'entry' : 'entries' } . Pruning runs automatically on next reload; use this to apply immediately. ` )
. addButton ( ( btn ) => {
btn . setButtonText ( 'Prune now' )
. setDisabled ( ! pruningEnabled ) ;
if ( pruningEnabled )
btn . setCta ( ) ;
btn . onClick ( ( ) => _ _awaiter ( this , void 0 , void 0 , function * ( ) {
this . plugin . pruneDb ( ) ;
yield this . plugin . writeDb ( this . plugin . db ) ;
this . display ( ) ;
} ) ) ;
} ) ) ;
2025-08-02 12:09:34 +08:00
}
}
module . exports = RememberCursorPosition ;
2026-05-06 17:32:44 +08:00
/* nosourcemap */