👔 Major changes for v 1.10

This commit is contained in:
Philipp Stracker 2026-02-25 21:55:18 +01:00
parent 83b1460439
commit 4dd5f9abaf
No known key found for this signature in database
7 changed files with 169 additions and 50 deletions

View file

@ -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('');

View file

@ -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<MentionSuggestion> {
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<MentionSuggestion> {
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<MentionSuggestion> {
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<MentionSuggestion> {
}
}
// 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<MentionSuggestion> {
* 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<MentionSuggestion> {
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<MentionSuggestion> {
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<MentionSuggestion> {
suggestionType: 'create',
displayText: context.query,
linkName: context.query,
path: '',
context,
};
}
@ -189,7 +208,7 @@ export class SuggestionProvider extends EditorSuggest<MentionSuggestion> {
*/
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<MentionSuggestion> {
}
}
/**
* 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,
);

View file

@ -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);
}
});

View file

@ -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}]]`;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}