diff --git a/src/constants.ts b/src/constants.ts index e883056..d1bf467 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,11 +7,14 @@ export const DEFAULT_SETTINGS: MentionSettings = { mentionTypes: [], matchStart: true, maxMatchLength: 15, - stopCharacters: '?!"\'`:;/#+*=&%$§<>', + stopCharacters: '?!;:"\'`/#*%<>[]()', }; /** * List of allowed signs for mentions + * + * Forbidden on Windows: " * : < > ? \ / | + * Conflict with Obsidian: [ ] ( ) | # % ` */ -export const ALLOWED_SIGNS_STRING = '@#+!$%^&*~=-:;><|'; +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 973dd85..83cb627 100644 --- a/src/editor/suggestion.ts +++ b/src/editor/suggestion.ts @@ -1,6 +1,6 @@ import { App, EditorSuggest, Editor, EditorPosition, TFile } from 'obsidian'; import { MentionManager } from '../mention/mention-manager'; -import { getTypeDef, createMentionLink } from '../mention/link-utils'; +import { getTypeDef, createMentionString } from '../mention/link-utils'; import { MentionSettings, MentionSuggestion, @@ -16,6 +16,7 @@ export class SuggestionProvider extends EditorSuggest { private settings: MentionSettings; private mentionManager: MentionManager; private currSign: string = ''; + private prevMention: string = ''; private fileMaps: any; constructor(app: App, settings: MentionSettings, mentionManager: MentionManager) { @@ -25,6 +26,10 @@ export class SuggestionProvider extends EditorSuggest { this.fileMaps = mentionManager.getFileMaps(); } + get currType() { + return getTypeDef(this.settings.mentionTypes, this.currSign); + } + /** * Update the suggestions map */ @@ -92,18 +97,28 @@ export class SuggestionProvider extends EditorSuggest { return false; } - // Check if query includes closing brackets - if (query.includes(']]')) { + // Verify if the current query is different from the previous completion + if (this.prevMention) { + if (query.startsWith(this.prevMention)) { + return false; + } + + // Reset the prev completion cache + this.prevMention = ''; + } + + // Is WikiLink? + if (/\[\[.*]]/.test(query) || /\[.*]\(.*\)/.test(query)) { return false; } - // Check if query exceeds max length + // Check if the 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 + // Check if the query contains any stop characters const stopCharacters = this.settings.stopCharacters ?? DEFAULT_SETTINGS.stopCharacters; if (stopCharacters) { for (const char of stopCharacters) { @@ -113,7 +128,7 @@ export class SuggestionProvider extends EditorSuggest { } } - // Check if sign is at start of line or has a space before it + // Check if the sign is at the start of the line or has a space before it. return ( signIndex === 0 || charsLeftOfCursor[signIndex - 1] === ' ' @@ -124,14 +139,14 @@ export class SuggestionProvider extends EditorSuggest { * Get suggestions based on the query */ getSuggestions(context: EditorSuggestContext): MentionSuggestion[] { - let suggestions: 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 + // Always add the option to create a new item suggestions.push(this.createNewItemSuggestion(context)); return suggestions; @@ -148,14 +163,17 @@ export class SuggestionProvider extends EditorSuggest { continue; } - if (this.isMatch(key, term)) { - suggestions.push({ - suggestionType: 'set', - displayText: map[key].name.trim(), - linkName: map[key].name, - context, - }); + if (!this.isMatch(key, term)) { + continue; } + + suggestions.push({ + suggestionType: 'set', + displayText: map[key].name.trim(), + linkName: map[key].name, + path: map[key].path, + context, + }); } return suggestions; @@ -167,9 +185,9 @@ export class SuggestionProvider extends EditorSuggest { private isMatch(key: string, term: string): boolean { if (this.settings.matchStart) { return key.startsWith(term); - } else { - return key.includes(term); } + + return key.includes(term); } /** @@ -180,6 +198,7 @@ export class SuggestionProvider extends EditorSuggest { suggestionType: 'create', displayText: context.query, linkName: context.query, + path: '', context, }; } @@ -189,7 +208,7 @@ export class SuggestionProvider extends EditorSuggest { */ renderSuggestion(value: MentionSuggestion, el: HTMLElement): void { if (value.suggestionType === 'create') { - const type = getTypeDef(this.settings.mentionTypes, this.currSign); + const type = this.currType; const label = type?.label || 'Item'; el.setText(`Create ${label}: ${value.displayText}`); @@ -198,14 +217,19 @@ export class SuggestionProvider extends EditorSuggest { } } - /** - * Handle selection of a suggestion - */ selectSuggestion(value: MentionSuggestion, evt: MouseEvent | KeyboardEvent): void { - const link = createMentionLink(this.currSign, value.linkName); + const config = this.currType; + if (!config?.sign) { + return; + } + + const context = value.context; + console.log('Insert:', config, value) + const insertion = createMentionString(this.app, context.file.path, value.path, config.sign, value.linkName, config.type ?? 'link'); + this.prevMention = value.linkName; value.context.editor.replaceRange( - link, + insertion, value.context.start, value.context.end, ); diff --git a/src/mention/file-indexer.ts b/src/mention/file-indexer.ts index 4cf7f51..d43e605 100644 --- a/src/mention/file-indexer.ts +++ b/src/mention/file-indexer.ts @@ -7,7 +7,7 @@ import { getLinkFromPath } from './link-utils'; */ export class FileIndexer { private app: App; - private settings: MentionSettings; + private readonly settings: MentionSettings; private fileMaps: FileMaps = {}; constructor(app: App, settings: MentionSettings) { @@ -26,11 +26,14 @@ export class FileIndexer { // 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); - } + if (! (file instanceof TFile) || file.extension !== 'md') { + return; + } + + const mentionLink = getLinkFromPath(file.path, this.settings); + + if (mentionLink) { + this.addFileToMap(mentionLink); } }); diff --git a/src/mention/link-utils.ts b/src/mention/link-utils.ts index 84aebff..65bcb05 100644 --- a/src/mention/link-utils.ts +++ b/src/mention/link-utils.ts @@ -1,3 +1,4 @@ +import { App, TFile } from 'obsidian'; import { MentionSettings, MentionType, MentionLink } from '../types'; /** @@ -11,6 +12,11 @@ export function getLinkFromPath(path: string, settings: MentionSettings): Mentio return null; } + const basename = path.split('/')?.pop()?.replace(/\.[^.]+$/, ''); + if (!basename) { + return null; + } + for (let i = 0; i < settings.mentionTypes.length; i++) { const type = settings.mentionTypes[i]; @@ -18,20 +24,15 @@ export function getLinkFromPath(path: string, settings: MentionSettings): Mentio continue; } - if (!path.includes('/' + type.sign)) { + if (!basename.startsWith(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 { + sign: type.sign, + name: basename.slice(type.sign.length).trim(), + path, + }; } return null; @@ -55,10 +56,40 @@ export function getTypeDef(types: MentionType[], sign: string): MentionType | nu /** * Create a link string for a mention + * @param app + * @param sourcePath The file with receives the link + * @param linkTo Path to the linked file * @param sign The mention sign * @param name The name to link to + * @param type Insertion type (link or text) * @returns Formatted link string */ -export function createMentionLink(sign: string, name: string): string { - return `[[${sign}${name}]]`; +export function createMentionString(app: App, sourcePath: string, linkTo: string, sign: string, name: string, type: string): string { + const basename = `${sign}${name}`; + + if ('text' === type) { + return basename; + } + + const file = app.vault.getAbstractFileByPath(linkTo); + + if (!(file instanceof TFile)) { + return basename; + } + + return app.fileManager.generateMarkdownLink(file, sourcePath, '', basename); +} + +export function generateLinkPreview(app: App, type: string, prefix: string, text: string): string { + const useMarkdownLinks = (app.vault as any).getConfig('useMarkdownLinks'); + + if ('text' === type) { + return text; + } + const link = encodeURIComponent(text); + const label = prefix + text; + + return useMarkdownLinks + ? `[${label}](${prefix}${link})` + : `[[${label}]]`; } diff --git a/src/settings/settings-tab.ts b/src/settings/settings-tab.ts index 0db6d70..f5b420c 100644 --- a/src/settings/settings-tab.ts +++ b/src/settings/settings-tab.ts @@ -1,7 +1,8 @@ import { App, PluginSettingTab, Setting } from 'obsidian'; -import { AvailableSigns } from '../types'; import { ALLOWED_SIGNS } from '../constants'; import MentionThingsPlugin from '../main'; +import { generateLinkPreview } from '../mention/link-utils'; +import { AvailableSigns, AvailableTypes } from '../types'; /** * Settings tab for the Mention Things plugin @@ -62,6 +63,9 @@ export class SettingsTab extends PluginSettingTab { private renderMentionTypeRow(containerEl: HTMLElement, value: any, index: number, usedSigns: string[]): void { const setting = new Setting(containerEl); const availableSigns = this.getAvailableSigns(usedSigns, value?.sign); + const availableTypes = this.getAvailableTypes(); + + setting.setClass('mention-type-row'); // Sign dropdown setting.addDropdown( @@ -83,10 +87,33 @@ export class SettingsTab extends PluginSettingTab { .onChange(async (value) => { this.plugin.settings.mentionTypes[index].label = value; await this.plugin.saveSettings(); + updatePreview(); }) - .inputEl.addClass('type_label'), + .inputEl.addClass('type-label'), ); + // Link style dropdown + setting.addDropdown( + list => list + .addOptions(availableTypes) + .setValue(value?.type || 'link') + .onChange(async (value) => { + this.plugin.settings.mentionTypes[index].type = value; + await this.plugin.saveSettings(); + this.display(); + }), + ); + + const updatePreview = (): void => { + const label = value.label || 'Name'; + preview.classList.add('type-preview'); + preview.innerText = generateLinkPreview(this.app, value.type, value.sign, label); + }; + + const preview = setting.controlEl.createSpan(); + setting.controlEl.append(preview); + updatePreview(); + // Delete button setting.addExtraButton( button => button @@ -146,8 +173,7 @@ export class SettingsTab extends PluginSettingTab { .setValue(this.plugin.settings.stopCharacters ?? '?!"\'`:;/#+*=&%$§<>') .onChange(async (value) => { // Sanitize: remove duplicate characters - const uniqueChars = [...new Set(value.split(''))].join(''); - this.plugin.settings.stopCharacters = uniqueChars; + this.plugin.settings.stopCharacters = [...new Set(value.split(''))].join(''); await this.plugin.saveSettings(); }); }); @@ -170,4 +196,13 @@ export class SettingsTab extends PluginSettingTab { return availableSigns; } + + private getAvailableTypes(): AvailableTypes { + const availableTypes: AvailableTypes = {}; + + availableTypes.text = 'As Text'; + availableTypes.link = 'As Link'; + + return availableTypes; + } } diff --git a/src/styles.css b/src/styles.css index 273d7ef..d548335 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,3 +1,20 @@ -.mention-type-row .type-label { - flex: 1 1 auto; +.mention-type-row { + --preview-width: 20ch; + + .type-label { + flex: 1 1 auto; + } + + .type-preview { + display: inline-block; + min-width: var(--preview-width); + max-width: var(--preview-width); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + text-align: left; + font-size: 12px; + opacity: 0.7; + } } diff --git a/src/types.ts b/src/types.ts index 79cd42c..ae9fc92 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,7 @@ export interface MentionSettings { * Represents a type of mention with its sign and label */ export interface MentionType { + type?: string; sign?: string; label?: string; } @@ -43,6 +44,10 @@ export interface AvailableSigns { [sign: string]: string; } +export interface AvailableTypes { + [type: string]: string; +} + /** * Suggestion item structure */ @@ -50,6 +55,7 @@ export interface MentionSuggestion { suggestionType: 'set' | 'create'; displayText: string; linkName: string; + path: string; context: any; }