Skip to content

Commit ebc86de

Browse files
committed
init: this is working pretty well so far
0 parents  commit ebc86de

7 files changed

+298
-0
lines changed

.editorconfig

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
indent_style = tab
6+
indent_size = 4
7+
8+
[*.js]
9+
insert_final_newline = true
10+
trim_trailing_whitespace = true
11+
12+
[*.md]
13+
trim_trailing_whitespace = true

LICENSE.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Very Open License (VOL)
2+
3+
The contributor(s) to this creative work voluntarily grant permission
4+
to any individual(s) or entities of any kind
5+
- to use the creative work in any manner,
6+
- to modify the creative work without restriction,
7+
- to sell the creative work or derivatives thereof for profit, and
8+
- to release modifications of the creative work in part or whole under any license
9+
with no requirement for compensation or recognition of any kind.

README.md

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Obsidian `@People`
2+
3+
Obsidian plugin to add that familiar @-to-tag-someone syntax:
4+
5+
![](./example.png)
6+
7+
When you hit enter on a suggestion, it'll create a link that looks like this:
8+
9+
```
10+
The author was [[@Rich Hickey]]
11+
```
12+
13+
and leave the cursor at the end.
14+
15+
## Options
16+
17+
There's not a lot to configure here, but they are important:
18+
19+
### 1. Where are the people files?
20+
21+
You probably want to group the people files in a folder.
22+
23+
I usually do something like this:
24+
25+
```
26+
People/
27+
@Rich Hickey.md
28+
@Rich Harris.md
29+
```
30+
31+
You can configure that in settings to point to somewhere else, like `Reference/People/` or whatever makes sense.
32+
33+
### 2. Explicit link structure?
34+
35+
By default, the plugin will insert the simple version:
36+
37+
```
38+
[[@Rich Hickey]]
39+
```
40+
41+
But you might rather make that explicit, in which case you can enable "explicit links" and they'll look like this instead:
42+
43+
```
44+
[[People/@Rich Hickey.md|@Rich Hickey]]
45+
```
46+
47+
### 3. Last name grouping?
48+
49+
For my personal Obsidian vaults, I have a lot of people with my same last name, so I put them in sub-folders for organization.
50+
51+
You can toggle the "last name folder" option, and it'll do that in the links.
52+
53+
The earlier example folder structure would be:
54+
55+
```
56+
People/
57+
Hickey/
58+
@Rich Hickey.md
59+
Harris/
60+
@Rich Harris.md
61+
```
62+
63+
And then the inserted link would look like:
64+
65+
```
66+
[[People/Hickey/@Rich Hickey.md|@Rich Hickey]]
67+
```
68+
69+
> Note: figuring out what the "last name" is (or if it even has one) is really complicated! This plugin takes a very simply approach: if you split a name by the space character, it'll just pick the last "word". So for example "Charles Le Fabre" would be "Fabre" and *not* "Le Fabre".
70+
>
71+
> I'm open to better implementations that don't add a lot of complexity, just start a discussion.
72+
73+
## License
74+
75+
Published and made available freely under the [Very Open License](http://veryopenlicense.com/).

example.png

25 KB
Loading

main.js

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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+
}

manifest.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"id": "at-people",
3+
"name": "At People",
4+
"version": "1.0.0",
5+
"description": "Use the @ to create links to people files.",
6+
"author": "saibotsivad",
7+
"authorUrl": "https://davistobias.com",
8+
"isDesktopOnly": false
9+
}

package.json

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "obsidian-at-people",
3+
"version": "1.0.0",
4+
"description": "Use the @ to create links to people files.",
5+
"main": "main.js",
6+
"repository": {
7+
"type": "git",
8+
"url": "git+https://github.com/saibotsivad/obsidian-at-people.git"
9+
},
10+
"keywords": [
11+
"obsidian",
12+
"obsidian-plugin",
13+
"people"
14+
],
15+
"author": {
16+
"name": "Tobias Davis",
17+
"email": "[email protected]",
18+
"url": "https://davistobias.com"
19+
},
20+
"license": "See license in LICENSE.md",
21+
"bugs": {
22+
"url": "https://github.com/saibotsivad/obsidian-at-people/issues"
23+
},
24+
"homepage": "https://github.com/saibotsivad/obsidian-at-people#readme"
25+
}

0 commit comments

Comments
 (0)