Skip to content

Commit 4d5e5f8

Browse files
authored
Merge pull request #6 from rca-umb/autocomplete
Autocomplete
2 parents e458770 + 1666476 commit 4d5e5f8

File tree

3 files changed

+158
-8
lines changed

3 files changed

+158
-8
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## Unreleased
8+
9+
### Added
10+
11+
- Display text suggestions. A suggestions popup will appear when the cursor is directly after an anchor link with display text. There will be three suggestions one, for each of the display text formats that can be used with this plugin (no note name, note name and then heading(s), heading(s) and then note name).
12+
13+
### Changed
14+
15+
- Heading separators are now validated to not include link breaking characters `[]|#^`. If any of these characters are typed into the separator field, the character will be ignored and a warning will appear.
16+
17+
### Fixed
18+
19+
- Minor typos.
20+
721
## [1.1.0] - 2025-1-25
822

923
### Removed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,5 @@ Show only the last heading: [[Title#Heading#Subheading#Subsubheading|Subsubheadi
3737
```
3838

3939
By default, the headings in the display text will be separated by a single space, but this can be changed to whatever you prefer. Some examples may be a comma (, ), colon (: ), or arrow (-> ). Just note that whatever is typed in the separator text box in the settings will be exactly what is used in the display text, nothing is added to it or removed from it.
40+
41+
Additionally, there is an option for enabling display text alternative suggestions. This is a suggestions window which will appear when the cursor is next to an existing anchor link. All three display text formats described above will be available as suggestions regardless of the option chosen for automatic display text generation. This is useful for users who wish to use multiple formats.

main.ts

Lines changed: 142 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,46 @@
1-
import { App, Editor, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
1+
import { App, Command, Editor, EditorPosition, EditorSuggest, EditorSuggestTriggerInfo, Notice, Plugin, PluginSettingTab, Setting, TFile } from 'obsidian';
2+
3+
interface AnchorDisplaySuggestion {
4+
displayText: string;
5+
source: string;
6+
}
27

38
interface AnchorDisplayTextSettings {
49
includeNoteName : string;
510
whichHeadings: string;
611
includeNotice: boolean;
7-
sep : string;
12+
sep: string;
13+
suggest: boolean
814
}
915

1016
const DEFAULT_SETTINGS: AnchorDisplayTextSettings = {
1117
includeNoteName: 'headersOnly',
1218
whichHeadings: 'allHeaders',
1319
includeNotice: false,
14-
sep: ' '
20+
sep: ' ',
21+
suggest: true
1522
}
1623

1724
export default class AnchorDisplayText extends Plugin {
1825
settings: AnchorDisplayTextSettings;
26+
suggestionsRegistered: boolean = false;
1927

2028
async onload() {
2129
await this.loadSettings();
22-
2330
this.addSettingTab(new AnchorDisplayTextSettingTab(this.app, this));
31+
if (this.settings.suggest) {
32+
this.registerEditorSuggest(new AnchorDisplaySuggest(this));
33+
this.suggestionsRegistered = true;
34+
}
2435

2536
// look for header link creation
2637
this.registerEvent(
2738
this.app.workspace.on('editor-change', (editor: Editor) => {
2839
// get what is being typed
2940
const cursor = editor.getCursor();
3041
const currentLine = editor.getLine(cursor.line);
31-
// match links to other anchor links WITHOUT an already defined display text
32-
const headerLinkPattern = /\[\[([^\]]+#[^|]+)\]\]/;
42+
// match anchor links WITHOUT an already defined display text
43+
const headerLinkPattern = /\[\[([^\]]+#[^|\n\r\]]+)\]\]/;
3344
const match = currentLine.slice(0, cursor.ch).match(headerLinkPattern);
3445
if (match) {
3546
// handle multiple subheadings
@@ -76,6 +87,99 @@ export default class AnchorDisplayText extends Plugin {
7687

7788
}
7889

90+
class AnchorDisplaySuggest extends EditorSuggest<AnchorDisplaySuggestion> {
91+
private plugin: AnchorDisplayText;
92+
93+
constructor(plugin: AnchorDisplayText) {
94+
super(plugin.app);
95+
this.plugin = plugin;
96+
}
97+
98+
onTrigger(cursor: EditorPosition, editor: Editor): EditorSuggestTriggerInfo | null {
99+
// turns off suggestions if the setting is disabled but the app hasn't been reloaded
100+
if (!this.plugin.settings.suggest) {
101+
return null;
102+
}
103+
const currentLine = editor.getLine(cursor.line);
104+
// match anchor links, even if they already have a display text
105+
const headerLinkPattern = /(\[\[([^\]]+#[^\n\r\]]+)\]\])$/;
106+
// only when cursor is immediately after the link
107+
const match = currentLine.slice(0, cursor.ch).match(headerLinkPattern);
108+
109+
if(!match) {
110+
return null;
111+
}
112+
113+
return {
114+
start: {
115+
line: cursor.line,
116+
ch: match.index! + match[1].length - 2, // 2 less to keep closing brackets
117+
},
118+
end: {
119+
line: cursor.line,
120+
ch: match.index! + match[1].length - 2,
121+
},
122+
query: match[2],
123+
};
124+
};
125+
126+
getSuggestions(context: EditorSuggestTriggerInfo): AnchorDisplaySuggestion[] {
127+
// don't include existing display text in headings
128+
const headings = context.query.split('|')[0].split('#');
129+
130+
let displayText = headings[1];
131+
for (let i = 2; i < headings.length; i++) {
132+
displayText += this.plugin.settings.sep + headings[i];
133+
}
134+
135+
const suggestion1: AnchorDisplaySuggestion = {
136+
displayText: displayText,
137+
source: 'Don\'t include note name',
138+
}
139+
const suggestion2: AnchorDisplaySuggestion = {
140+
displayText: `${headings[0]}${this.plugin.settings.sep}${displayText}`,
141+
source: 'Note name and than heading(s)',
142+
}
143+
const suggestion3: AnchorDisplaySuggestion = {
144+
displayText: `${displayText}${this.plugin.settings.sep}${headings[0]}`,
145+
source: 'Heading(s) and than note name',
146+
}
147+
return [suggestion1, suggestion2, suggestion3];
148+
};
149+
150+
renderSuggestion(value: AnchorDisplaySuggestion, el: HTMLElement) {
151+
// prompt instructions are a child of the suggestion container, which will
152+
// be the parent of the parent the element which gets passed to this function
153+
const suggestionEl = el.parentElement;
154+
const suggestionContainerEl = suggestionEl!.parentElement;
155+
// only need to render the prompt instructions once, but renderSuggestion gets called
156+
// on each suggestion
157+
if (suggestionContainerEl!.childElementCount < 2) {
158+
const promptInstructionsEl = suggestionContainerEl!.createDiv({cls: 'prompt-instructions'});
159+
const instructionEl = promptInstructionsEl.createDiv({cls: 'prompt-instruction'});
160+
instructionEl.createEl('span', {cls: 'prompt-instruction-command', text:'↵'});
161+
instructionEl.createEl('span', {text:'to accept'});
162+
}
163+
// class of the passed element will be suggestion-item, but we need suggestion-item mod-complex
164+
// to get appropriate styling
165+
el.setAttribute('class', 'suggestion-item mod-complex');
166+
const suggestionContentEl = el.createDiv({cls: 'suggestion-content'});
167+
suggestionContentEl.createDiv({cls: 'suggestion-title', text: value.displayText});
168+
suggestionContentEl.createDiv({cls: 'suggestion-note', text: value.source});
169+
};
170+
171+
selectSuggestion(value: AnchorDisplaySuggestion, evt: MouseEvent | KeyboardEvent): void {
172+
const editor = this.context!.editor;
173+
// if there is already display text, will need to overwrite it
174+
const displayTextPattern = /\|([^\]]+)/;
175+
const match = this.context!.query.match(displayTextPattern);
176+
if (match) {
177+
this.context!.start.ch = this.context!.start.ch - match[0].length;
178+
}
179+
editor.replaceRange(`|${value.displayText}`, this.context!.start, this.context!.end, 'headerDisplayText');
180+
};
181+
}
182+
79183
class AnchorDisplayTextSettingTab extends PluginSettingTab {
80184
plugin: AnchorDisplayText;
81185

@@ -84,6 +188,19 @@ class AnchorDisplayTextSettingTab extends PluginSettingTab {
84188
this.plugin = plugin;
85189
}
86190

191+
validateSep(value: string): string {
192+
let validValue: string = value;
193+
for (const c of value) {
194+
if ('[]#^|'.includes(c)) {
195+
validValue = validValue.replace(c, '');
196+
}
197+
}
198+
if (validValue != value) {
199+
new Notice(`Separators cannot contain any of the following characters: []#^|`);
200+
}
201+
return validValue;
202+
}
203+
87204
display(): void {
88205
const {containerEl} = this;
89206
containerEl.empty();
@@ -101,6 +218,7 @@ class AnchorDisplayTextSettingTab extends PluginSettingTab {
101218
this.plugin.saveSettings();
102219
});
103220
});
221+
104222
new Setting(containerEl)
105223
.setName('Include subheadings')
106224
.setDesc('Change which headings and subheadings are in the display text.')
@@ -114,13 +232,14 @@ class AnchorDisplayTextSettingTab extends PluginSettingTab {
114232
this.plugin.saveSettings();
115233
});
116234
});
235+
117236
new Setting(containerEl)
118-
.setName('Seperator')
237+
.setName('Separator')
119238
.setDesc('Choose what to insert between headings instead of #.')
120239
.addText(text => {
121240
text.setValue(this.plugin.settings.sep);
122241
text.onChange(value => {
123-
this.plugin.settings.sep = value;
242+
this.plugin.settings.sep = this.validateSep(value);
124243
this.plugin.saveSettings();
125244
});
126245
});
@@ -135,5 +254,20 @@ class AnchorDisplayTextSettingTab extends PluginSettingTab {
135254
this.plugin.saveSettings();
136255
});
137256
});
257+
258+
new Setting(containerEl)
259+
.setName('Suggest alternatives')
260+
.setDesc('Have a suggestion window to present alternative display text options when the cursor is directly after an anchor link.')
261+
.addToggle(toggle => {
262+
toggle.setValue(this.plugin.settings.suggest);
263+
toggle.onChange(value => {
264+
this.plugin.settings.suggest = value;
265+
this.plugin.saveSettings();
266+
if (!this.plugin.suggestionsRegistered) {
267+
this.plugin.registerEditorSuggest(new AnchorDisplaySuggest(this.plugin));
268+
this.plugin.suggestionsRegistered = true;
269+
}
270+
});
271+
});
138272
}
139273
}

0 commit comments

Comments
 (0)