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