Skip to content

Commit e29a3fe

Browse files
authored
Merge branch 'master' into handle-block-id
2 parents 549cd0f + 4d5e5f8 commit e29a3fe

File tree

3 files changed

+159
-9
lines changed

3 files changed

+159
-9
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: 143 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,50 @@
1-
import { App, Editor, Notice, Plugin, PluginSettingTab, Setting, debounce } from 'obsidian';
1+
import { App, Command, Editor, EditorPosition, EditorSuggest, EditorSuggestTriggerInfo, Notice, Plugin, PluginSettingTab, Setting, TFile, debounce } 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', debounce((editor: Editor) => {
2839
// Only process if the last typed character is ']'
2940
const cursor = editor.getCursor();
3041
const currentLine = editor.getLine(cursor.line);
42+
3143
const lastChar = currentLine[cursor.ch - 1];
32-
3344
if (lastChar !== ']') return;
3445

35-
// get what is being typed
36-
const headerLinkPattern = /\[\[([^\]]+#[^|]+)\]\]/;
46+
// match anchor links WITHOUT an already defined display text
47+
const headerLinkPattern = /\[\[([^\]]+#[^|\n\r\]]+)\]\]/;
3748
const match = currentLine.slice(0, cursor.ch).match(headerLinkPattern);
3849
if (match) {
3950
// handle multiple subheadings
@@ -85,6 +96,99 @@ export default class AnchorDisplayText extends Plugin {
8596

8697
}
8798

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

@@ -93,6 +197,19 @@ class AnchorDisplayTextSettingTab extends PluginSettingTab {
93197
this.plugin = plugin;
94198
}
95199

200+
validateSep(value: string): string {
201+
let validValue: string = value;
202+
for (const c of value) {
203+
if ('[]#^|'.includes(c)) {
204+
validValue = validValue.replace(c, '');
205+
}
206+
}
207+
if (validValue != value) {
208+
new Notice(`Separators cannot contain any of the following characters: []#^|`);
209+
}
210+
return validValue;
211+
}
212+
96213
display(): void {
97214
const {containerEl} = this;
98215
containerEl.empty();
@@ -110,6 +227,7 @@ class AnchorDisplayTextSettingTab extends PluginSettingTab {
110227
this.plugin.saveSettings();
111228
});
112229
});
230+
113231
new Setting(containerEl)
114232
.setName('Include subheadings')
115233
.setDesc('Change which headings and subheadings are in the display text.')
@@ -123,13 +241,14 @@ class AnchorDisplayTextSettingTab extends PluginSettingTab {
123241
this.plugin.saveSettings();
124242
});
125243
});
244+
126245
new Setting(containerEl)
127-
.setName('Seperator')
246+
.setName('Separator')
128247
.setDesc('Choose what to insert between headings instead of #.')
129248
.addText(text => {
130249
text.setValue(this.plugin.settings.sep);
131250
text.onChange(value => {
132-
this.plugin.settings.sep = value;
251+
this.plugin.settings.sep = this.validateSep(value);
133252
this.plugin.saveSettings();
134253
});
135254
});
@@ -144,5 +263,20 @@ class AnchorDisplayTextSettingTab extends PluginSettingTab {
144263
this.plugin.saveSettings();
145264
});
146265
});
266+
267+
new Setting(containerEl)
268+
.setName('Suggest alternatives')
269+
.setDesc('Have a suggestion window to present alternative display text options when the cursor is directly after an anchor link.')
270+
.addToggle(toggle => {
271+
toggle.setValue(this.plugin.settings.suggest);
272+
toggle.onChange(value => {
273+
this.plugin.settings.suggest = value;
274+
this.plugin.saveSettings();
275+
if (!this.plugin.suggestionsRegistered) {
276+
this.plugin.registerEditorSuggest(new AnchorDisplaySuggest(this.plugin));
277+
this.plugin.suggestionsRegistered = true;
278+
}
279+
});
280+
});
147281
}
148282
}

0 commit comments

Comments
 (0)