Add max length and stop characters

This commit is contained in:
Philipp Stracker 2025-03-18 21:39:35 +01:00
parent b4e8462652
commit 3fa1406ca5
No known key found for this signature in database
4 changed files with 288 additions and 198 deletions

View file

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

View file

@ -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<MentionSuggestion> {
* 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<MentionSuggestion> {
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<MentionSuggestion> {
}
}
// 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<MentionSuggestion> {
);
}
}
// 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;
}

View file

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

View file

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