|
| 1 | +const { App, Editor, EditorSuggest, TFile, Notice, Plugin, PluginSettingTab, Setting } = require('obsidian') |
| 2 | + |
| 3 | +const DEFAULT_SETTINGS = { |
| 4 | + peopleFolder: 'People/', |
| 5 | + // Defaults: |
| 6 | + // useExplicitLinks: undefined, |
| 7 | + // useLastNameFolder: undefined, |
| 8 | +} |
| 9 | + |
| 10 | +const NAME_REGEX = /\/@([^\/]+)\.md$/ |
| 11 | +const LAST_NAME_REGEX = /([\S]+)$/ |
| 12 | + |
| 13 | +const getPersonName = (filename, settings) => filename.startsWith(settings.peopleFolder) |
| 14 | + && filename.endsWith('.md') |
| 15 | + && filename.includes('/@') |
| 16 | + && NAME_REGEX.exec(filename)?.[1] |
| 17 | + |
| 18 | +module.exports = class AtPeople extends Plugin { |
| 19 | + async onload() { |
| 20 | + await this.loadSettings() |
| 21 | + this.registerEvent(this.app.vault.on('delete', async event => { await this.update(event) })) |
| 22 | + this.registerEvent(this.app.vault.on('create', async event => { await this.update(event) })) |
| 23 | + this.registerEvent(this.app.vault.on('rename', async (event, originalFilepath) => { await this.update(event, originalFilepath) })) |
| 24 | + this.app.workspace.onLayoutReady(this.initialize) |
| 25 | + this.addSettingTab(new AtPeopleSettingTab(this.app, this)) |
| 26 | + this.suggestor = new AtPeopleSuggestor(this.app, this.settings) |
| 27 | + this.registerEditorSuggest(this.suggestor) |
| 28 | + } |
| 29 | + |
| 30 | + async loadSettings() { |
| 31 | + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()) |
| 32 | + } |
| 33 | + |
| 34 | + async saveSettings() { |
| 35 | + await this.saveData(this.settings || DEFAULT_SETTINGS) |
| 36 | + } |
| 37 | + |
| 38 | + updatePeopleMap = () => { |
| 39 | + this.suggestor.updatePeopleMap(this.peopleFileMap) |
| 40 | + } |
| 41 | + |
| 42 | + update = async ({ path, deleted, ...remaining }, originalFilepath) => { |
| 43 | + this.peopleFileMap = this.peopleFileMap || {} |
| 44 | + const name = getPersonName(path, this.settings) |
| 45 | + let needsUpdated |
| 46 | + if (name) { |
| 47 | + this.peopleFileMap[name] = path |
| 48 | + needsUpdated = true |
| 49 | + } |
| 50 | + originalFilepath = originalFilepath && getPersonName(originalFilepath, this.settings) |
| 51 | + if (originalFilepath) { |
| 52 | + delete this.peopleFileMap[originalFilepath] |
| 53 | + needsUpdated = true |
| 54 | + } |
| 55 | + if (needsUpdated) this.updatePeopleMap() |
| 56 | + } |
| 57 | + |
| 58 | + initialize = () => { |
| 59 | + this.peopleFileMap = {} |
| 60 | + for (const filename in this.app.vault.fileMap) { |
| 61 | + const name = getPersonName(filename, this.settings) |
| 62 | + if (name) this.peopleFileMap[name] = filename |
| 63 | + } |
| 64 | + this.updatePeopleMap() |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +class AtPeopleSuggestor extends EditorSuggest { |
| 69 | + constructor(app, settings) { |
| 70 | + super(app) |
| 71 | + this.settings = settings |
| 72 | + } |
| 73 | + updatePeopleMap(peopleFileMap) { |
| 74 | + this.peopleFileMap = peopleFileMap |
| 75 | + } |
| 76 | + onTrigger(cursor, editor, tFile) { |
| 77 | + let charsLeftOfCursor = editor.getLine(cursor.line).substring(0, cursor.ch) |
| 78 | + let atIndex = charsLeftOfCursor.lastIndexOf('@') |
| 79 | + let query = atIndex >= 0 && charsLeftOfCursor.substring(atIndex + 1) |
| 80 | + if (query && !query.includes(']]')) { |
| 81 | + return { |
| 82 | + start: { line: cursor.line, ch: atIndex }, |
| 83 | + end: { line: cursor.line, ch: cursor.ch }, |
| 84 | + query, |
| 85 | + } |
| 86 | + } |
| 87 | + return null |
| 88 | + } |
| 89 | + getSuggestions(context) { |
| 90 | + let suggestions = [] |
| 91 | + for (let key in (this.peopleFileMap || {})) |
| 92 | + if (key.toLowerCase().startsWith(context.query)) |
| 93 | + suggestions.push({ |
| 94 | + suggestionType: 'set', |
| 95 | + displayText: key, |
| 96 | + context, |
| 97 | + }) |
| 98 | + suggestions.push({ |
| 99 | + suggestionType: 'create', |
| 100 | + displayText: context.query, |
| 101 | + context, |
| 102 | + }) |
| 103 | + return suggestions |
| 104 | + } |
| 105 | + renderSuggestion(value, elem) { |
| 106 | + if (value.suggestionType === 'create') elem.setText('New person: ' + value.displayText) |
| 107 | + else elem.setText(value.displayText) |
| 108 | + } |
| 109 | + selectSuggestion(value) { |
| 110 | + let link |
| 111 | + if (this.settings.useExplicitLinks && this.settings.useLastNameFolder) { |
| 112 | + let lastName = LAST_NAME_REGEX.exec(value.displayText) |
| 113 | + lastName = lastName && lastName[1] && (lastName[1] + '/') || '' |
| 114 | + link = `[[${this.settings.peopleFolder}${lastName}@${value.displayText}.md|@${value.displayText}]]` |
| 115 | + } else if (this.settings.useExplicitLinks && !this.settings.useLastNameFolder) { |
| 116 | + link = `[[${this.settings.peopleFolder}@${value.displayText}.md|@${value.displayText}]]` |
| 117 | + } else { |
| 118 | + link = `[[@${value.displayText}]]` |
| 119 | + } |
| 120 | + value.context.editor.replaceRange( |
| 121 | + link, |
| 122 | + value.context.start, |
| 123 | + value.context.end, |
| 124 | + ) |
| 125 | + } |
| 126 | +} |
| 127 | + |
| 128 | +class AtPeopleSettingTab extends PluginSettingTab { |
| 129 | + constructor(app, plugin) { |
| 130 | + super(app, plugin) |
| 131 | + this.plugin = plugin |
| 132 | + } |
| 133 | + display() { |
| 134 | + const { containerEl } = this |
| 135 | + containerEl.empty() |
| 136 | + new Setting(containerEl) |
| 137 | + .setName('People folder') |
| 138 | + .setDesc('The folder where people files live, e.g. "People/". (With trailing slash.)') |
| 139 | + .addText( |
| 140 | + text => text |
| 141 | + .setPlaceholder(DEFAULT_SETTINGS.peopleFolder) |
| 142 | + .setValue(this.plugin.settings.peopleFolder) |
| 143 | + .onChange(async (value) => { |
| 144 | + this.plugin.settings.peopleFolder = value |
| 145 | + await this.plugin.saveSettings() |
| 146 | + }) |
| 147 | + ) |
| 148 | + new Setting(containerEl) |
| 149 | + .setName('Explicit links') |
| 150 | + .setDesc('When inserting links include the full path, e.g. [[People/@Bob Dole.md|@Bob Dole]]') |
| 151 | + .addToggle( |
| 152 | + toggle => toggle.onChange(async (value) => { |
| 153 | + this.plugin.settings.useExplicitLinks = value |
| 154 | + await this.plugin.saveSettings() |
| 155 | + }) |
| 156 | + ) |
| 157 | + new Setting(containerEl) |
| 158 | + .setName('Last name folder') |
| 159 | + .setDesc('When using explicit links, use the "last name" (the last non-spaced word) as a sub-folder, e.g. [[People/Dole/@Bob Dole.md|@Bob Dole]]') |
| 160 | + .addToggle( |
| 161 | + toggle => toggle.onChange(async (value) => { |
| 162 | + this.plugin.settings.useLastNameFolder = value |
| 163 | + await this.plugin.saveSettings() |
| 164 | + }) |
| 165 | + ) |
| 166 | + } |
| 167 | +} |
0 commit comments