diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..073dd85 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,31 @@ +import { MentionSettings } from './types'; + +/** + * Default plugin settings + */ +export const DEFAULT_SETTINGS: MentionSettings = { + mentionTypes: [], + matchStart: true, +}; + +/** + * List of allowed signs for mentions + */ +export const ALLOWED_SIGNS = [ + '@', + '%', + '&', + '!', + '?', + '+', + ';', + '.', + '=', + '^', + '§', + '$', + '-', + '_', + '(', + '{', +]; diff --git a/src/editor/suggestion.ts b/src/editor/suggestion.ts new file mode 100644 index 0000000..9985e24 --- /dev/null +++ b/src/editor/suggestion.ts @@ -0,0 +1,162 @@ +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'; + +/** + * Provides suggestion functionality for mentions in the editor + */ +export class SuggestionProvider extends EditorSuggest { + private settings: MentionSettings; + private mentionManager: MentionManager; + private currSign: string = ''; + private fileMaps: any; + + constructor(app: App, settings: MentionSettings, mentionManager: MentionManager) { + super(app); + this.settings = settings; + this.mentionManager = mentionManager; + this.fileMaps = mentionManager.getFileMaps(); + } + + /** + * Update the suggestions map + */ + setSuggestionsMap(maps: any): void { + this.fileMaps = maps; + } + + /** + * 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); + + 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; + } + } + + if (signIndex < 0) { + return null; + } + + let query = signIndex >= 0 && 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, + }; + } + + return null; + } + + /** + * Get suggestions based on the query + */ + getSuggestions(context: EditorSuggestContext): MentionSuggestion[] { + let suggestions: MentionSuggestion[] = []; + let map = this.fileMaps[this.currSign] || {}; + + 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) { + suggestions.push({ + suggestionType: 'set', + displayText: map[key].name.trim(), + linkName: map[key].name, + context, + }); + } + } + + // Always add option to create new item + suggestions.push({ + suggestionType: 'create', + displayText: context.query, + linkName: context.query, + context, + }); + + return suggestions; + } + + /** + * Render a suggestion in the dropdown + */ + renderSuggestion(value: MentionSuggestion, el: HTMLElement): void { + if (value.suggestionType === 'create') { + const type = getTypeDef(this.settings.mentionTypes, this.currSign); + const label = type?.label || 'Item'; + + el.setText(`Create ${label}: ${value.displayText}`); + } else { + el.setText(value.displayText); + } + } + + /** + * Handle selection of a suggestion + */ + selectSuggestion(value: MentionSuggestion, evt: MouseEvent | KeyboardEvent): void { + const link = createMentionLink(this.currSign, value.linkName); + + value.context.editor.replaceRange( + link, + value.context.start, + value.context.end, + ); + } +} + +// 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/main.ts b/src/main.ts index d6b4752..7f5d9b5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,394 +1,109 @@ -const { - EditorSuggest, - Plugin, - PluginSettingTab, - Setting -} = require('obsidian') +import { Plugin } from 'obsidian'; +import { MentionSettings, FileMaps } from './types'; +import { DEFAULT_SETTINGS } from './constants'; +import { MentionManager } from './mention/mention-manager'; +import { SuggestionProvider } from './editor/suggestion'; +import { SettingsTab } from './settings/settings-tab'; -const DEFAULT_SETTINGS = { - mentionTypes: [], +export default class MentionThingsPlugin extends Plugin { + settings: MentionSettings; + mentionManager: MentionManager; + suggestionProvider: SuggestionProvider; - // matchStart: undefined, -} - -const getLink = (path, settings) => { - if (!path.endsWith('.md')) { - return null; - } - - for (let i = 0; i < settings.mentionTypes.length; i++) { - const type = settings.mentionTypes[i]; - - if (!type?.sign) { - continue; - } - - if (!path.includes('/' + 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 null; -} - -function getTypeDef(types, sign) { - for (let i = 0; i < types.length; i++) { - if (sign === types[i]?.sign) { - return types[i]; - } - } - - return null; -} - -module.exports = class AtPeople extends Plugin { async onload() { - await this.loadSettings() + // Load settings + await this.loadSettings(); - this.registerEvent(this.app.vault.on('delete', async event => { - await this.update(event) - })) - this.registerEvent(this.app.vault.on('create', async event => { - await this.update(event) - })) - this.registerEvent(this.app.vault.on('rename', async (event, originalFilepath) => { - await this.update(event, originalFilepath) - })) - - this.addSettingTab(new AtPeopleSettingTab(this.app, this)) - this.suggestor = new AtPeopleSuggestor(this.app, this.settings) - this.registerEditorSuggest(this.suggestor) - this.app.workspace.onLayoutReady(this.initialize) + // Initialize the mention manager + this.mentionManager = new MentionManager(this.app, this.settings); + + // Initialize the suggestion provider + this.suggestionProvider = new SuggestionProvider( + this.app, + this.settings, + this.mentionManager + ); + + // Register the suggestion provider with the editor + this.registerEditorSuggest(this.suggestionProvider); + + // Add the settings tab + this.addSettingTab(new SettingsTab(this.app, this)); + + // Register file events + this.registerFileEvents(); + + // Initialize when the layout is ready + this.app.workspace.onLayoutReady(() => { + const fileMaps = this.mentionManager.initialize(); + this.suggestionProvider.setSuggestionsMap(fileMaps); + }); } + /** + * Register file event handlers + */ + private registerFileEvents(): void { + // Handle file deletion + this.registerEvent( + this.app.vault.on('delete', async (file) => { + const updated = this.mentionManager.handleFileEvent(file); + if (updated) { + this.refreshSuggestions(); + } + }) + ); + + // Handle file creation + this.registerEvent( + this.app.vault.on('create', async (file) => { + const updated = this.mentionManager.handleFileEvent(file); + if (updated) { + this.refreshSuggestions(); + } + }) + ); + + // Handle file renaming + this.registerEvent( + this.app.vault.on('rename', async (file, oldPath) => { + const updated = this.mentionManager.handleFileEvent(file, oldPath); + if (updated) { + this.refreshSuggestions(); + } + }) + ); + } + + /** + * Refresh the suggestions list in the editor + */ + private refreshSuggestions(): void { + const fileMaps = this.mentionManager.getFileMaps(); + this.suggestionProvider.setSuggestionsMap(fileMaps); + } + + /** + * Load settings from disk + */ async loadSettings() { this.settings = Object.assign( {}, DEFAULT_SETTINGS, await this.loadData() - ) + ); } + /** + * Save settings to disk + */ async saveSettings() { - await this.saveData(this.settings || DEFAULT_SETTINGS) - } - - refreshFileList = () => { - this.suggestor.setSuggestionsMap(this.fileMaps) - } - - update = async ({path, deleted, ...remaining}, originalFilepath) => { - let needsUpdated = false; - - this.fileMaps = this.fileMaps || {} - - const addItem = getLink(path, this.settings) - - if (addItem) { - this.addFileToMap(addItem) - needsUpdated = true - } - - const removeItem = originalFilepath && getLink(originalFilepath, this.settings) - if (removeItem) { - this.removeFileFromMap(removeItem) - needsUpdated = true - } - - if (needsUpdated) { - this.refreshFileList() - } - } - - initialize = () => { - this.fileMaps = {} - - for (const filename in this.app.vault.fileMap) { - const addItem = getLink(filename, this.settings) - - if (addItem) { - this.addFileToMap(addItem) - } - } - - window.setTimeout(this.refreshFileList, 0, this) - } - - addFileToMap(item) { - const sign = item?.sign; + await this.saveData(this.settings); - if (!sign) { - return; - } - - const key = item.name.toLowerCase(); - this.fileMaps[sign] = this.fileMaps[sign] || {}; - this.fileMaps[sign][key] = item; - } - - removeFileFromMap(item) { - const sign = item?.sign; - - if (!sign) { - return; - } - - if (this.fileMaps[sign]) { - const key = item.name.toLowerCase(); - delete this.fileMaps[sign][key] + // Update the mention manager with new settings + if (this.mentionManager) { + this.mentionManager.updateSettings(this.settings); + this.refreshSuggestions(); } } } - - -class AtPeopleSuggestor extends EditorSuggest { - constructor(app, settings) { - super(app) - this.settings = settings - this.currSign = ''; - } - - setSuggestionsMap(maps) { - this.maps = maps; - } - - onTrigger(cursor, editor, tFile) { - const usedSigns = this.settings.mentionTypes.map(object => object.sign); - - let 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; - } - } - - if (signIndex < 0) { - return null; - } - - let query = signIndex >= 0 && charsLeftOfCursor.substring(signIndex + 1) - - if ( - query - && !query.includes(']]') - && ( - // if it's an @ 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, - } - } - - return null - } - - getSuggestions(context) { - let suggestions = [] - let map = this.maps[this.currSign] || {}; - - 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) { - suggestions.push({ - suggestionType: 'set', - displayText: map[key].name.trim(), - linkName: map[key].name, - context, - }) - } - } - - suggestions.push({ - suggestionType: 'create', - displayText: context.query, - linkName: context.query, - context, - }) - - return suggestions - } - - renderSuggestion(value, elem) { - if (value.suggestionType === 'create') { - const type = getTypeDef(this.settings.mentionTypes, this.currSign); - const label = type?.label || 'Item'; - - elem.setText(`Create ${label}: ${value.displayText}`) - } else { - elem.setText(value.displayText) - } - } - - selectSuggestion(value) { - let link - - console.log(value); - link = `[[${this.currSign}${value.linkName}]]` - - value.context.editor.replaceRange( - link, - value.context.start, - value.context.end, - ) - } - -} - - -// ---------------------------------------------------------------------------- -// Settings - -/** - * @param {string[]} usedSigns - * @param {string} currentSign - * @returns {object} - */ -function getAvailableSigns(usedSigns, currentSign) { - const allowedSigns = [ - '@', - '%', - '&', - '!', - '?', - '+', - ';', - '.', - '=', - '^', - '§', - '$', - '-', - '_', - '(', - '{', - ]; - - const availableSigns = {}; - - allowedSigns.forEach(sign => { - if (currentSign === sign || !usedSigns.includes(sign)) { - availableSigns[sign] = `${sign}Name` - } - }) - - return availableSigns; -} - -class AtPeopleSettingTab extends PluginSettingTab { - constructor(app, plugin) { - super(app, plugin) - this.plugin = plugin - } - - - display() { - const {containerEl} = this - let usedSigns = this.plugin.settings.mentionTypes.map(object => object.sign); - - containerEl.empty() - - containerEl.createEl("h2", {text: "Mention Things"}); - - this.plugin.settings.mentionTypes.forEach((value, index) => { - const setting = new Setting(containerEl) - const availableSigns = getAvailableSigns(usedSigns, value?.sign); - - 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() - }) - ) - - 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') - ) - - 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(); - }); - - new Setting(containerEl) - .addButton(cb => { - cb.setButtonText("New Type") - .setCta() - .onClick(async () => { - this.plugin.settings.mentionTypes.push({}); - await this.plugin.saveSettings(); - this.display(); - }); - }); - - 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) - .onChange(async (value) => { - this.plugin.settings.matchStart = value - await this.plugin.saveSettings() - }) - ) - } -} diff --git a/src/mention/file-indexer.ts b/src/mention/file-indexer.ts new file mode 100644 index 0000000..4cf7f51 --- /dev/null +++ b/src/mention/file-indexer.ts @@ -0,0 +1,100 @@ +import { App, TFile } from 'obsidian'; +import { MentionSettings, FileMaps, MentionLink } from '../types'; +import { getLinkFromPath } from './link-utils'; + +/** + * Handles indexing and tracking mentionable files + */ +export class FileIndexer { + private app: App; + private settings: MentionSettings; + private fileMaps: FileMaps = {}; + + constructor(app: App, settings: MentionSettings) { + this.app = app; + this.settings = settings; + } + + /** + * Initialize the file index by scanning all files in the vault + */ + initialize(): FileMaps { + this.fileMaps = {}; + + // Get all files in the vault using the proper Obsidian API + const files = this.app.vault.getAllLoadedFiles(); + + // 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); + } + } + }); + + return this.fileMaps; + } + + /** + * Get the current file maps + */ + getFileMaps(): FileMaps { + return this.fileMaps; + } + + /** + * Update the file index when a file is created, deleted, or renamed + */ + updateIndex(path: string, originalPath?: string): boolean { + let needsUpdate = false; + + // Handle new or updated file + const addItem = getLinkFromPath(path, this.settings); + if (addItem) { + this.addFileToMap(addItem); + needsUpdate = true; + } + + // Handle renamed or deleted file + if (originalPath) { + const removeItem = getLinkFromPath(originalPath, this.settings); + if (removeItem) { + this.removeFileFromMap(removeItem); + needsUpdate = true; + } + } + + return needsUpdate; + } + + /** + * Add a file to the appropriate map + */ + private addFileToMap(item: MentionLink): void { + const sign = item.sign; + + if (!sign) { + return; + } + + const key = item.name.toLowerCase(); + this.fileMaps[sign] = this.fileMaps[sign] || {}; + this.fileMaps[sign][key] = item; + } + + /** + * Remove a file from the map + */ + private removeFileFromMap(item: MentionLink): void { + const sign = item.sign; + + if (!sign || !this.fileMaps[sign]) { + return; + } + + const key = item.name.toLowerCase(); + delete this.fileMaps[sign][key]; + } +} diff --git a/src/mention/link-utils.ts b/src/mention/link-utils.ts new file mode 100644 index 0000000..84aebff --- /dev/null +++ b/src/mention/link-utils.ts @@ -0,0 +1,64 @@ +import { MentionSettings, MentionType, MentionLink } from '../types'; + +/** + * Extract mention information from a file path + * @param path File path to analyze + * @param settings Plugin settings + * @returns MentionLink object if the path matches a mention pattern, null otherwise + */ +export function getLinkFromPath(path: string, settings: MentionSettings): MentionLink | null { + if (!path.endsWith('.md')) { + return null; + } + + for (let i = 0; i < settings.mentionTypes.length; i++) { + const type = settings.mentionTypes[i]; + + if (!type?.sign) { + continue; + } + + if (!path.includes('/' + 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 null; +} + +/** + * Find a mention type definition by its sign + * @param types Array of mention types + * @param sign Sign character to look for + * @returns MentionType if found, null otherwise + */ +export function getTypeDef(types: MentionType[], sign: string): MentionType | null { + for (let i = 0; i < types.length; i++) { + if (sign === types[i]?.sign) { + return types[i]; + } + } + + return null; +} + +/** + * Create a link string for a mention + * @param sign The mention sign + * @param name The name to link to + * @returns Formatted link string + */ +export function createMentionLink(sign: string, name: string): string { + return `[[${sign}${name}]]`; +} diff --git a/src/mention/mention-manager.ts b/src/mention/mention-manager.ts new file mode 100644 index 0000000..d816645 --- /dev/null +++ b/src/mention/mention-manager.ts @@ -0,0 +1,61 @@ +import { App } from 'obsidian'; +import { MentionSettings, FileMaps } from '../types'; +import { FileIndexer } from './file-indexer'; + +/** + * Core mention functionality manager + */ +export class MentionManager { + private settings: MentionSettings; + private fileIndexer: FileIndexer; + + constructor(app: App, settings: MentionSettings) { + this.settings = settings; + this.fileIndexer = new FileIndexer(app, settings); + } + + /** + * Initialize the mention manager + */ + initialize(): FileMaps { + return this.fileIndexer.initialize(); + } + + /** + * Get the current file maps + */ + getFileMaps(): FileMaps { + return this.fileIndexer.getFileMaps(); + } + + /** + * Handle file events (create, delete, rename) + * @param fileEvent File event data + * @param originalPath Original path for rename events + * @returns Whether the file maps were updated + */ + handleFileEvent(fileEvent: { + path: string, + deleted?: boolean + }, originalPath?: string): boolean { + return this.fileIndexer.updateIndex(fileEvent.path, originalPath); + } + + /** + * Get all used signs from the settings + */ + getUsedSigns(): string[] { + return this.settings.mentionTypes.map(object => object.sign).filter(Boolean) as string[]; + } + + /** + * Update settings with new values + * @param settings New settings + */ + updateSettings(settings: MentionSettings): void { + this.settings = settings; + + // Re-initialize file maps with new settings + this.initialize(); + } +} diff --git a/src/settings/settings-tab.ts b/src/settings/settings-tab.ts new file mode 100644 index 0000000..98239a1 --- /dev/null +++ b/src/settings/settings-tab.ts @@ -0,0 +1,143 @@ +import { App, PluginSettingTab, Setting } from 'obsidian'; +import { AvailableSigns } from '../types'; +import { ALLOWED_SIGNS } from '../constants'; +import MentionThingsPlugin from '../main'; + +/** + * Settings tab for the Mention Things plugin + */ +export class SettingsTab extends PluginSettingTab { + private plugin: MentionThingsPlugin; + + 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); + + containerEl.empty(); + + containerEl.createEl('h2', { text: 'Mention Things' }); + + // Render mention types section + this.renderMentionTypesSection(containerEl, usedSigns); + + // 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 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(); + }); + }); + } + + /** + * 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(); + }), + ); + + // 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(); + }), + ); + + setting.infoEl.remove(); + } + + /** + * 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(); + }), + ); + } + + /** + * 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 new file mode 100644 index 0000000..55a2020 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,52 @@ +import { App, TFile } from 'obsidian'; + +/** + * Plugin settings interface + */ +export interface MentionSettings { + mentionTypes: MentionType[]; + matchStart: boolean; +} + +/** + * Represents a type of mention with its sign and label + */ +export interface MentionType { + sign?: string; + label?: string; +} + +/** + * Represents a link to a mentioned item + */ +export interface MentionLink { + sign: string; + name: string; + path: string; +} + +/** + * Structure for storing mentionable files + */ +export interface FileMaps { + [sign: string]: { + [key: string]: MentionLink; + }; +} + +/** + * Available signs for dropdown selection + */ +export interface AvailableSigns { + [sign: string]: string; +} + +/** + * Suggestion item structure + */ +export interface MentionSuggestion { + suggestionType: 'set' | 'create'; + displayText: string; + linkName: string; + context: any; +}