mirror of
https://github.com/stracker-phil/obsidian-mention-things.git
synced 2026-07-22 05:43:27 +00:00
✨ Add max length and stop characters
This commit is contained in:
parent
b4e8462652
commit
3fa1406ca5
4 changed files with 288 additions and 198 deletions
|
|
@ -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('');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
src/types.ts
25
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue