From 3fa1406ca57d3904be9be3a694f2e8748f923266 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Tue, 18 Mar 2025 21:39:35 +0100
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20max=20length=20and=20stop=20c?=
=?UTF-8?q?haracters?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/constants.ts | 26 +---
src/editor/suggestion.ts | 175 ++++++++++++++---------
src/settings/settings-tab.ts | 260 ++++++++++++++++++++---------------
src/types.ts | 25 +++-
4 files changed, 288 insertions(+), 198 deletions(-)
diff --git a/src/constants.ts b/src/constants.ts
index 073dd85..e883056 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -4,28 +4,14 @@ import { MentionSettings } from './types';
* Default plugin settings
*/
export const DEFAULT_SETTINGS: MentionSettings = {
- mentionTypes: [],
- matchStart: true,
+ mentionTypes: [],
+ matchStart: true,
+ maxMatchLength: 15,
+ stopCharacters: '?!"\'`:;/#+*=&%$§<>',
};
/**
* List of allowed signs for mentions
*/
-export const ALLOWED_SIGNS = [
- '@',
- '%',
- '&',
- '!',
- '?',
- '+',
- ';',
- '.',
- '=',
- '^',
- '§',
- '$',
- '-',
- '_',
- '(',
- '{',
-];
+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 9985e24..973dd85 100644
--- a/src/editor/suggestion.ts
+++ b/src/editor/suggestion.ts
@@ -1,7 +1,13 @@
import { App, EditorSuggest, Editor, EditorPosition, TFile } from 'obsidian';
-import { MentionSettings, MentionSuggestion } from '../types';
import { MentionManager } from '../mention/mention-manager';
import { getTypeDef, createMentionLink } from '../mention/link-utils';
+import {
+ MentionSettings,
+ MentionSuggestion,
+ EditorSuggestTriggerInfo,
+ EditorSuggestContext,
+} from '../types';
+import { DEFAULT_SETTINGS } from '../constants';
/**
* Provides suggestion functionality for mentions in the editor
@@ -30,49 +36,88 @@ export class SuggestionProvider extends EditorSuggest {
* Determine if suggestions should be triggered
*/
onTrigger(cursor: EditorPosition, editor: Editor, file: TFile): EditorSuggestTriggerInfo | null {
- const usedSigns = this.mentionManager.getUsedSigns();
-
- let charsLeftOfCursor = editor.getLine(cursor.line).substring(0, cursor.ch);
+ const charsLeftOfCursor = editor.getLine(cursor.line).substring(0, cursor.ch);
if (!charsLeftOfCursor) {
return null;
}
- let signIndex = -1;
-
- for (let sign of usedSigns) {
- let index = charsLeftOfCursor.lastIndexOf(sign);
-
- if (index !== -1 && index > signIndex) {
- signIndex = index;
- this.currSign = sign;
- }
- }
+ // Find the most recent mention sign
+ const { signIndex, sign } = this.findMostRecentSign(charsLeftOfCursor);
if (signIndex < 0) {
return null;
}
- let query = signIndex >= 0 && charsLeftOfCursor.substring(signIndex + 1);
+ this.currSign = sign;
+ const query = charsLeftOfCursor.substring(signIndex + 1);
- if (
- query
- && !query.includes(']]')
- && (
- // if it's a sign at the start of a line
- signIndex === 0
- // or if there's a space character before it
- || charsLeftOfCursor[signIndex - 1] === ' '
- )
- ) {
- return {
- start: { line: cursor.line, ch: signIndex },
- end: { line: cursor.line, ch: cursor.ch },
- query,
- };
+ // Check various conditions that would prevent showing suggestions
+ if (!this.shouldShowSuggestions(query, signIndex, charsLeftOfCursor)) {
+ return null;
}
- return null;
+ return {
+ start: { line: cursor.line, ch: signIndex },
+ end: { line: cursor.line, ch: cursor.ch },
+ query,
+ };
+ }
+
+ /**
+ * Find the most recent mention sign in the text
+ */
+ private findMostRecentSign(text: string): { signIndex: number, sign: string } {
+ const usedSigns = this.mentionManager.getUsedSigns();
+ let signIndex = -1;
+ let foundSign = '';
+
+ for (let sign of usedSigns) {
+ let index = text.lastIndexOf(sign);
+
+ if (index !== -1 && index > signIndex) {
+ signIndex = index;
+ foundSign = sign;
+ }
+ }
+
+ return { signIndex, sign: foundSign };
+ }
+
+ /**
+ * Determine if suggestions should be shown based on various conditions
+ */
+ private shouldShowSuggestions(query: string, signIndex: number, charsLeftOfCursor: string): boolean {
+ if (!query) {
+ return false;
+ }
+
+ // Check if query includes closing brackets
+ if (query.includes(']]')) {
+ return false;
+ }
+
+ // Check if 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
+ const stopCharacters = this.settings.stopCharacters ?? DEFAULT_SETTINGS.stopCharacters;
+ if (stopCharacters) {
+ for (const char of stopCharacters) {
+ if (query.includes(char)) {
+ return false;
+ }
+ }
+ }
+
+ // Check if sign is at start of line or has a space before it
+ return (
+ signIndex === 0 ||
+ charsLeftOfCursor[signIndex - 1] === ' '
+ );
}
/**
@@ -81,23 +126,29 @@ export class SuggestionProvider extends EditorSuggest {
getSuggestions(context: EditorSuggestContext): 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
+ suggestions.push(this.createNewItemSuggestion(context));
+
+ return suggestions;
+ }
+
+ /**
+ * Get suggestions that match the search term
+ */
+ private getMatchingSuggestions(map: any, term: string, context: EditorSuggestContext): MentionSuggestion[] {
+ const suggestions: MentionSuggestion[] = [];
for (let key in map) {
- let isMatch;
-
if (!key) {
continue;
}
- const term = context.query.toLowerCase();
-
- if (this.settings.matchStart) {
- isMatch = key.startsWith(term);
- } else {
- isMatch = key.includes(term);
- }
-
- if (isMatch) {
+ if (this.isMatch(key, term)) {
suggestions.push({
suggestionType: 'set',
displayText: map[key].name.trim(),
@@ -107,15 +158,30 @@ export class SuggestionProvider extends EditorSuggest {
}
}
- // Always add option to create new item
- suggestions.push({
+ return suggestions;
+ }
+
+ /**
+ * Check if an item matches the search term based on settings
+ */
+ private isMatch(key: string, term: string): boolean {
+ if (this.settings.matchStart) {
+ return key.startsWith(term);
+ } else {
+ return key.includes(term);
+ }
+ }
+
+ /**
+ * Create a suggestion for creating a new item
+ */
+ private createNewItemSuggestion(context: EditorSuggestContext): MentionSuggestion {
+ return {
suggestionType: 'create',
displayText: context.query,
linkName: context.query,
context,
- });
-
- return suggestions;
+ };
}
/**
@@ -145,18 +211,3 @@ export class SuggestionProvider extends EditorSuggest {
);
}
}
-
-// Type definition for EditorSuggest trigger info
-interface EditorSuggestTriggerInfo {
- start: EditorPosition;
- end: EditorPosition;
- query: string;
-}
-
-// Type definition for EditorSuggest context
-interface EditorSuggestContext {
- start: EditorPosition;
- end: EditorPosition;
- query: string;
- editor: Editor;
-}
diff --git a/src/settings/settings-tab.ts b/src/settings/settings-tab.ts
index 98239a1..137efac 100644
--- a/src/settings/settings-tab.ts
+++ b/src/settings/settings-tab.ts
@@ -7,137 +7,169 @@ import MentionThingsPlugin from '../main';
* Settings tab for the Mention Things plugin
*/
export class SettingsTab extends PluginSettingTab {
- private plugin: MentionThingsPlugin;
+ private plugin: MentionThingsPlugin;
- constructor(app: App, plugin: MentionThingsPlugin) {
- super(app, plugin);
- this.plugin = plugin;
- }
+ constructor(app: App, plugin: MentionThingsPlugin) {
+ super(app, plugin);
+ this.plugin = plugin;
+ }
- /**
- * Display the settings tab
- */
- display(): void {
- const { containerEl } = this;
- let usedSigns = this.plugin.settings.mentionTypes
- .map(object => object.sign)
- .filter((sign): sign is string => sign !== undefined);
+ /**
+ * Display the settings tab
+ */
+ display(): void {
+ const { containerEl } = this;
+ let usedSigns = this.plugin.settings.mentionTypes
+ .map(object => object.sign)
+ .filter((sign): sign is string => sign !== undefined);
- containerEl.empty();
+ containerEl.empty();
- containerEl.createEl('h2', { text: 'Mention Things' });
+ containerEl.createEl('h2', { text: 'Mention Things' });
- // Render mention types section
- this.renderMentionTypesSection(containerEl, usedSigns);
+ // Render mention types section
+ this.renderMentionTypesSection(containerEl, usedSigns);
- // Render general settings section
- this.renderGeneralSettings(containerEl);
- }
+ // Render general settings section
+ this.renderGeneralSettings(containerEl);
+ }
- /**
- * Render the mention types section of the settings
- */
- private renderMentionTypesSection(containerEl: HTMLElement, usedSigns: string[]): void {
- containerEl.createEl('h3', { text: 'Mention Types' });
+ /**
+ * Render the mention types section of the settings
+ */
+ private renderMentionTypesSection(containerEl: HTMLElement, usedSigns: string[]): void {
+ containerEl.createEl('h3', { text: 'Mention Types' });
- // Render each mention type
- this.plugin.settings.mentionTypes.forEach((value, index) => {
- this.renderMentionTypeRow(containerEl, value, index, usedSigns);
- });
+ // Render each mention type
+ this.plugin.settings.mentionTypes.forEach((value, index) => {
+ this.renderMentionTypeRow(containerEl, value, index, usedSigns);
+ });
- // Add button for new type
- new Setting(containerEl)
- .addButton(cb => {
- cb.setButtonText('New Type')
- .setCta()
- .onClick(async () => {
- this.plugin.settings.mentionTypes.push({});
- await this.plugin.saveSettings();
- this.display();
- });
- });
- }
+ // Add button for new type
+ new Setting(containerEl)
+ .addButton(cb => {
+ cb.setButtonText('New Type')
+ .setCta()
+ .onClick(async () => {
+ this.plugin.settings.mentionTypes.push({});
+ await this.plugin.saveSettings();
+ this.display();
+ });
+ });
+ }
- /**
- * Render a single mention type row
- */
- private renderMentionTypeRow(containerEl: HTMLElement, value: any, index: number, usedSigns: string[]): void {
- const setting = new Setting(containerEl);
- const availableSigns = this.getAvailableSigns(usedSigns, value?.sign);
+ /**
+ * Render a single mention type row
+ */
+ private renderMentionTypeRow(containerEl: HTMLElement, value: any, index: number, usedSigns: string[]): void {
+ const setting = new Setting(containerEl);
+ const availableSigns = this.getAvailableSigns(usedSigns, value?.sign);
- // Sign dropdown
- setting.addDropdown(
- list => list
- .addOptions(availableSigns)
- .setValue(value?.sign || '')
- .onChange(async (value) => {
- this.plugin.settings.mentionTypes[index].sign = value;
- await this.plugin.saveSettings();
- this.display();
- }),
- );
+ // Sign dropdown
+ setting.addDropdown(
+ list => list
+ .addOptions(availableSigns)
+ .setValue(value?.sign || '')
+ .onChange(async (value) => {
+ this.plugin.settings.mentionTypes[index].sign = value;
+ await this.plugin.saveSettings();
+ this.display();
+ }),
+ );
- // Label text field
- setting.addText(
- text => text
- .setPlaceholder('Label. Example "Person"')
- .setValue(value?.label || '')
- .onChange(async (value) => {
- this.plugin.settings.mentionTypes[index].label = value;
- await this.plugin.saveSettings();
- })
- .inputEl.addClass('type_label'),
- );
+ // Label text field
+ setting.addText(
+ text => text
+ .setPlaceholder('Label. Example "Person"')
+ .setValue(value?.label || '')
+ .onChange(async (value) => {
+ this.plugin.settings.mentionTypes[index].label = value;
+ await this.plugin.saveSettings();
+ })
+ .inputEl.addClass('type_label'),
+ );
- // Delete button
- setting.addExtraButton(
- button => button
- .setIcon('cross')
- .setTooltip('Delete')
- .onClick(async () => {
- this.plugin.settings.mentionTypes.splice(index, 1);
- await this.plugin.saveSettings();
- this.display();
- }),
- );
+ // Delete button
+ setting.addExtraButton(
+ button => button
+ .setIcon('cross')
+ .setTooltip('Delete')
+ .onClick(async () => {
+ this.plugin.settings.mentionTypes.splice(index, 1);
+ await this.plugin.saveSettings();
+ this.display();
+ }),
+ );
- setting.infoEl.remove();
- }
+ setting.infoEl.remove();
+ }
- /**
- * Render general settings section
- */
- private renderGeneralSettings(containerEl: HTMLElement): void {
- containerEl.createEl('h3', { text: 'General Settings' });
+ /**
+ * Render general settings section
+ */
+ private renderGeneralSettings(containerEl: HTMLElement): void {
+ containerEl.createEl('h3', { text: 'General Settings' });
- new Setting(containerEl)
- .setName('Match from start')
- .setDesc('Whether to suggest only items that start with the search term. When disabled, any part of the name can match the search term')
- .addToggle(
- toggle => toggle
- .setValue(this.plugin.settings.matchStart ?? false)
- .onChange(async (value) => {
- this.plugin.settings.matchStart = value;
- await this.plugin.saveSettings();
- }),
- );
- }
+ // Match from start setting
+ new Setting(containerEl)
+ .setName('Match from start')
+ .setDesc('Whether to suggest only items that start with the search term. When disabled, any part of the name can match the search term')
+ .addToggle(
+ toggle => toggle
+ .setValue(this.plugin.settings.matchStart ?? false)
+ .onChange(async (value) => {
+ this.plugin.settings.matchStart = value;
+ await this.plugin.saveSettings();
+ }),
+ );
- /**
- * Get available signs for the dropdown
- * @param usedSigns Array of signs already in use
- * @param currentSign Current sign for this type (to allow keeping the same sign)
- * @returns Object mapping signs to display values
- */
- private getAvailableSigns(usedSigns: string[], currentSign?: string): AvailableSigns {
- const availableSigns: AvailableSigns = {};
+ // Max match length setting
+ new Setting(containerEl)
+ .setName('Max match length')
+ .setDesc('Maximum number of characters to match (3-50)')
+ .addSlider(slider => {
+ slider
+ .setLimits(3, 50, 1)
+ .setValue(this.plugin.settings.maxMatchLength ?? 15)
+ .setDynamicTooltip()
+ .onChange(async (value) => {
+ this.plugin.settings.maxMatchLength = value;
+ await this.plugin.saveSettings();
+ });
+ });
- ALLOWED_SIGNS.forEach(sign => {
- if (currentSign === sign || !usedSigns.includes(sign)) {
- availableSigns[sign] = `${sign}Name`;
- }
- });
+ // Stop characters setting
+ new Setting(containerEl)
+ .setName('Stop characters')
+ .setDesc('Characters that will stop the matching process. Leave empty to disable.')
+ .addText(text => {
+ text
+ .setPlaceholder('?!"\'`:;/#+*=&%$§<>')
+ .setValue(this.plugin.settings.stopCharacters ?? '?!"\'`:;/#+*=&%$§<>')
+ .onChange(async (value) => {
+ // Sanitize: remove duplicate characters
+ const uniqueChars = [...new Set(value.split(''))].join('');
+ this.plugin.settings.stopCharacters = uniqueChars;
+ await this.plugin.saveSettings();
+ });
+ });
+ }
- return availableSigns;
- }
+ /**
+ * Get available signs for the dropdown
+ * @param usedSigns Array of signs already in use
+ * @param currentSign Current sign for this type (to allow keeping the same sign)
+ * @returns Object mapping signs to display values
+ */
+ private getAvailableSigns(usedSigns: string[], currentSign?: string): AvailableSigns {
+ const availableSigns: AvailableSigns = {};
+
+ ALLOWED_SIGNS.forEach(sign => {
+ if (currentSign === sign || !usedSigns.includes(sign)) {
+ availableSigns[sign] = `${sign}Name`;
+ }
+ });
+
+ return availableSigns;
+ }
}
diff --git a/src/types.ts b/src/types.ts
index 55a2020..79cd42c 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,11 +1,13 @@
-import { App, TFile } from 'obsidian';
+import { EditorPosition, Editor } from 'obsidian';
/**
* Plugin settings interface
*/
export interface MentionSettings {
mentionTypes: MentionType[];
- matchStart: boolean;
+ matchStart?: boolean;
+ maxMatchLength?: number;
+ stopCharacters?: string;
}
/**
@@ -50,3 +52,22 @@ export interface MentionSuggestion {
linkName: string;
context: any;
}
+
+/**
+ * Type definition for EditorSuggest trigger info
+ */
+export interface EditorSuggestTriggerInfo {
+ start: EditorPosition;
+ end: EditorPosition;
+ query: string;
+}
+
+/**
+ * Type definition for EditorSuggest context
+ */
+export interface EditorSuggestContext {
+ start: EditorPosition;
+ end: EditorPosition;
+ query: string;
+ editor: Editor;
+}