From 4dd5f9abaf87733cc84e1939bbb4766284c3bd5e Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Wed, 25 Feb 2026 21:55:18 +0100
Subject: [PATCH] =?UTF-8?q?=F0=9F=91=94=20Major=20changes=20for=20v=201.10?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/constants.ts | 7 ++--
src/editor/suggestion.ts | 70 ++++++++++++++++++++++++------------
src/mention/file-indexer.ts | 15 ++++----
src/mention/link-utils.ts | 57 ++++++++++++++++++++++-------
src/settings/settings-tab.ts | 43 +++++++++++++++++++---
src/styles.css | 21 +++++++++--
src/types.ts | 6 ++++
7 files changed, 169 insertions(+), 50 deletions(-)
diff --git a/src/constants.ts b/src/constants.ts
index e883056..d1bf467 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -7,11 +7,14 @@ export const DEFAULT_SETTINGS: MentionSettings = {
mentionTypes: [],
matchStart: true,
maxMatchLength: 15,
- stopCharacters: '?!"\'`:;/#+*=&%$§<>',
+ stopCharacters: '?!;:"\'`/#*%<>[]()',
};
/**
* List of allowed signs for mentions
+ *
+ * Forbidden on Windows: " * : < > ? \ / |
+ * Conflict with Obsidian: [ ] ( ) | # % `
*/
-export const ALLOWED_SIGNS_STRING = '@#+!$%^&*~=-:;><|';
+export const ALLOWED_SIGNS_STRING = "@!+$&~-_=,;'{}^§★→¶°€£¥¢±µ©™®«»";
export const ALLOWED_SIGNS = ALLOWED_SIGNS_STRING.split('');
diff --git a/src/editor/suggestion.ts b/src/editor/suggestion.ts
index 973dd85..83cb627 100644
--- a/src/editor/suggestion.ts
+++ b/src/editor/suggestion.ts
@@ -1,6 +1,6 @@
import { App, EditorSuggest, Editor, EditorPosition, TFile } from 'obsidian';
import { MentionManager } from '../mention/mention-manager';
-import { getTypeDef, createMentionLink } from '../mention/link-utils';
+import { getTypeDef, createMentionString } from '../mention/link-utils';
import {
MentionSettings,
MentionSuggestion,
@@ -16,6 +16,7 @@ export class SuggestionProvider extends EditorSuggest {
private settings: MentionSettings;
private mentionManager: MentionManager;
private currSign: string = '';
+ private prevMention: string = '';
private fileMaps: any;
constructor(app: App, settings: MentionSettings, mentionManager: MentionManager) {
@@ -25,6 +26,10 @@ export class SuggestionProvider extends EditorSuggest {
this.fileMaps = mentionManager.getFileMaps();
}
+ get currType() {
+ return getTypeDef(this.settings.mentionTypes, this.currSign);
+ }
+
/**
* Update the suggestions map
*/
@@ -92,18 +97,28 @@ export class SuggestionProvider extends EditorSuggest {
return false;
}
- // Check if query includes closing brackets
- if (query.includes(']]')) {
+ // Verify if the current query is different from the previous completion
+ if (this.prevMention) {
+ if (query.startsWith(this.prevMention)) {
+ return false;
+ }
+
+ // Reset the prev completion cache
+ this.prevMention = '';
+ }
+
+ // Is WikiLink?
+ if (/\[\[.*]]/.test(query) || /\[.*]\(.*\)/.test(query)) {
return false;
}
- // Check if query exceeds max length
+ // Check if the query exceeds max length
const maxMatchLength = this.settings.maxMatchLength ?? DEFAULT_SETTINGS.maxMatchLength;
if (maxMatchLength && query.length > maxMatchLength) {
return false;
}
- // Check if query contains any stop characters
+ // Check if the query contains any stop characters
const stopCharacters = this.settings.stopCharacters ?? DEFAULT_SETTINGS.stopCharacters;
if (stopCharacters) {
for (const char of stopCharacters) {
@@ -113,7 +128,7 @@ export class SuggestionProvider extends EditorSuggest {
}
}
- // Check if sign is at start of line or has a space before it
+ // Check if the sign is at the start of the line or has a space before it.
return (
signIndex === 0 ||
charsLeftOfCursor[signIndex - 1] === ' '
@@ -124,14 +139,14 @@ export class SuggestionProvider extends EditorSuggest {
* Get suggestions based on the query
*/
getSuggestions(context: EditorSuggestContext): MentionSuggestion[] {
- let suggestions: MentionSuggestion[] = [];
+ let suggestions: MentionSuggestion[];
let map = this.fileMaps[this.currSign] || {};
const term = context.query.toLowerCase();
// Add matching existing items
suggestions = this.getMatchingSuggestions(map, term, context);
- // Always add option to create new item
+ // Always add the option to create a new item
suggestions.push(this.createNewItemSuggestion(context));
return suggestions;
@@ -148,14 +163,17 @@ export class SuggestionProvider extends EditorSuggest {
continue;
}
- if (this.isMatch(key, term)) {
- suggestions.push({
- suggestionType: 'set',
- displayText: map[key].name.trim(),
- linkName: map[key].name,
- context,
- });
+ if (!this.isMatch(key, term)) {
+ continue;
}
+
+ suggestions.push({
+ suggestionType: 'set',
+ displayText: map[key].name.trim(),
+ linkName: map[key].name,
+ path: map[key].path,
+ context,
+ });
}
return suggestions;
@@ -167,9 +185,9 @@ export class SuggestionProvider extends EditorSuggest {
private isMatch(key: string, term: string): boolean {
if (this.settings.matchStart) {
return key.startsWith(term);
- } else {
- return key.includes(term);
}
+
+ return key.includes(term);
}
/**
@@ -180,6 +198,7 @@ export class SuggestionProvider extends EditorSuggest {
suggestionType: 'create',
displayText: context.query,
linkName: context.query,
+ path: '',
context,
};
}
@@ -189,7 +208,7 @@ export class SuggestionProvider extends EditorSuggest {
*/
renderSuggestion(value: MentionSuggestion, el: HTMLElement): void {
if (value.suggestionType === 'create') {
- const type = getTypeDef(this.settings.mentionTypes, this.currSign);
+ const type = this.currType;
const label = type?.label || 'Item';
el.setText(`Create ${label}: ${value.displayText}`);
@@ -198,14 +217,19 @@ export class SuggestionProvider extends EditorSuggest {
}
}
- /**
- * Handle selection of a suggestion
- */
selectSuggestion(value: MentionSuggestion, evt: MouseEvent | KeyboardEvent): void {
- const link = createMentionLink(this.currSign, value.linkName);
+ const config = this.currType;
+ if (!config?.sign) {
+ return;
+ }
+
+ const context = value.context;
+ console.log('Insert:', config, value)
+ const insertion = createMentionString(this.app, context.file.path, value.path, config.sign, value.linkName, config.type ?? 'link');
+ this.prevMention = value.linkName;
value.context.editor.replaceRange(
- link,
+ insertion,
value.context.start,
value.context.end,
);
diff --git a/src/mention/file-indexer.ts b/src/mention/file-indexer.ts
index 4cf7f51..d43e605 100644
--- a/src/mention/file-indexer.ts
+++ b/src/mention/file-indexer.ts
@@ -7,7 +7,7 @@ import { getLinkFromPath } from './link-utils';
*/
export class FileIndexer {
private app: App;
- private settings: MentionSettings;
+ private readonly settings: MentionSettings;
private fileMaps: FileMaps = {};
constructor(app: App, settings: MentionSettings) {
@@ -26,11 +26,14 @@ export class FileIndexer {
// Process each file
files.forEach(file => {
- if (file instanceof TFile && file.extension === 'md') {
- const mentionLink = getLinkFromPath(file.path, this.settings);
- if (mentionLink) {
- this.addFileToMap(mentionLink);
- }
+ if (! (file instanceof TFile) || file.extension !== 'md') {
+ return;
+ }
+
+ const mentionLink = getLinkFromPath(file.path, this.settings);
+
+ if (mentionLink) {
+ this.addFileToMap(mentionLink);
}
});
diff --git a/src/mention/link-utils.ts b/src/mention/link-utils.ts
index 84aebff..65bcb05 100644
--- a/src/mention/link-utils.ts
+++ b/src/mention/link-utils.ts
@@ -1,3 +1,4 @@
+import { App, TFile } from 'obsidian';
import { MentionSettings, MentionType, MentionLink } from '../types';
/**
@@ -11,6 +12,11 @@ export function getLinkFromPath(path: string, settings: MentionSettings): Mentio
return null;
}
+ const basename = path.split('/')?.pop()?.replace(/\.[^.]+$/, '');
+ if (!basename) {
+ return null;
+ }
+
for (let i = 0; i < settings.mentionTypes.length; i++) {
const type = settings.mentionTypes[i];
@@ -18,20 +24,15 @@ export function getLinkFromPath(path: string, settings: MentionSettings): Mentio
continue;
}
- if (!path.includes('/' + type.sign)) {
+ if (!basename.startsWith(type.sign)) {
continue;
}
- const regex = new RegExp(`/${type.sign}([^/]+)\\.md$`);
- let result = regex.exec(path);
-
- if (result?.[1]) {
- return {
- sign: type.sign,
- name: result[1],
- path,
- };
- }
+ return {
+ sign: type.sign,
+ name: basename.slice(type.sign.length).trim(),
+ path,
+ };
}
return null;
@@ -55,10 +56,40 @@ export function getTypeDef(types: MentionType[], sign: string): MentionType | nu
/**
* Create a link string for a mention
+ * @param app
+ * @param sourcePath The file with receives the link
+ * @param linkTo Path to the linked file
* @param sign The mention sign
* @param name The name to link to
+ * @param type Insertion type (link or text)
* @returns Formatted link string
*/
-export function createMentionLink(sign: string, name: string): string {
- return `[[${sign}${name}]]`;
+export function createMentionString(app: App, sourcePath: string, linkTo: string, sign: string, name: string, type: string): string {
+ const basename = `${sign}${name}`;
+
+ if ('text' === type) {
+ return basename;
+ }
+
+ const file = app.vault.getAbstractFileByPath(linkTo);
+
+ if (!(file instanceof TFile)) {
+ return basename;
+ }
+
+ return app.fileManager.generateMarkdownLink(file, sourcePath, '', basename);
+}
+
+export function generateLinkPreview(app: App, type: string, prefix: string, text: string): string {
+ const useMarkdownLinks = (app.vault as any).getConfig('useMarkdownLinks');
+
+ if ('text' === type) {
+ return text;
+ }
+ const link = encodeURIComponent(text);
+ const label = prefix + text;
+
+ return useMarkdownLinks
+ ? `[${label}](${prefix}${link})`
+ : `[[${label}]]`;
}
diff --git a/src/settings/settings-tab.ts b/src/settings/settings-tab.ts
index 0db6d70..f5b420c 100644
--- a/src/settings/settings-tab.ts
+++ b/src/settings/settings-tab.ts
@@ -1,7 +1,8 @@
import { App, PluginSettingTab, Setting } from 'obsidian';
-import { AvailableSigns } from '../types';
import { ALLOWED_SIGNS } from '../constants';
import MentionThingsPlugin from '../main';
+import { generateLinkPreview } from '../mention/link-utils';
+import { AvailableSigns, AvailableTypes } from '../types';
/**
* Settings tab for the Mention Things plugin
@@ -62,6 +63,9 @@ export class SettingsTab extends PluginSettingTab {
private renderMentionTypeRow(containerEl: HTMLElement, value: any, index: number, usedSigns: string[]): void {
const setting = new Setting(containerEl);
const availableSigns = this.getAvailableSigns(usedSigns, value?.sign);
+ const availableTypes = this.getAvailableTypes();
+
+ setting.setClass('mention-type-row');
// Sign dropdown
setting.addDropdown(
@@ -83,10 +87,33 @@ export class SettingsTab extends PluginSettingTab {
.onChange(async (value) => {
this.plugin.settings.mentionTypes[index].label = value;
await this.plugin.saveSettings();
+ updatePreview();
})
- .inputEl.addClass('type_label'),
+ .inputEl.addClass('type-label'),
);
+ // Link style dropdown
+ setting.addDropdown(
+ list => list
+ .addOptions(availableTypes)
+ .setValue(value?.type || 'link')
+ .onChange(async (value) => {
+ this.plugin.settings.mentionTypes[index].type = value;
+ await this.plugin.saveSettings();
+ this.display();
+ }),
+ );
+
+ const updatePreview = (): void => {
+ const label = value.label || 'Name';
+ preview.classList.add('type-preview');
+ preview.innerText = generateLinkPreview(this.app, value.type, value.sign, label);
+ };
+
+ const preview = setting.controlEl.createSpan();
+ setting.controlEl.append(preview);
+ updatePreview();
+
// Delete button
setting.addExtraButton(
button => button
@@ -146,8 +173,7 @@ export class SettingsTab extends PluginSettingTab {
.setValue(this.plugin.settings.stopCharacters ?? '?!"\'`:;/#+*=&%$§<>')
.onChange(async (value) => {
// Sanitize: remove duplicate characters
- const uniqueChars = [...new Set(value.split(''))].join('');
- this.plugin.settings.stopCharacters = uniqueChars;
+ this.plugin.settings.stopCharacters = [...new Set(value.split(''))].join('');
await this.plugin.saveSettings();
});
});
@@ -170,4 +196,13 @@ export class SettingsTab extends PluginSettingTab {
return availableSigns;
}
+
+ private getAvailableTypes(): AvailableTypes {
+ const availableTypes: AvailableTypes = {};
+
+ availableTypes.text = 'As Text';
+ availableTypes.link = 'As Link';
+
+ return availableTypes;
+ }
}
diff --git a/src/styles.css b/src/styles.css
index 273d7ef..d548335 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1,3 +1,20 @@
-.mention-type-row .type-label {
- flex: 1 1 auto;
+.mention-type-row {
+ --preview-width: 20ch;
+
+ .type-label {
+ flex: 1 1 auto;
+ }
+
+ .type-preview {
+ display: inline-block;
+ min-width: var(--preview-width);
+ max-width: var(--preview-width);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ text-align: left;
+ font-size: 12px;
+ opacity: 0.7;
+ }
}
diff --git a/src/types.ts b/src/types.ts
index 79cd42c..ae9fc92 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -14,6 +14,7 @@ export interface MentionSettings {
* Represents a type of mention with its sign and label
*/
export interface MentionType {
+ type?: string;
sign?: string;
label?: string;
}
@@ -43,6 +44,10 @@ export interface AvailableSigns {
[sign: string]: string;
}
+export interface AvailableTypes {
+ [type: string]: string;
+}
+
/**
* Suggestion item structure
*/
@@ -50,6 +55,7 @@ export interface MentionSuggestion {
suggestionType: 'set' | 'create';
displayText: string;
linkName: string;
+ path: string;
context: any;
}