mirror of
https://github.com/stracker-phil/obsidian-mention-things.git
synced 2026-07-22 05:43:27 +00:00
♻️ Refactor the plugin into modular typescript
This commit is contained in:
parent
4fec59e532
commit
003d6fe5f7
8 changed files with 704 additions and 376 deletions
31
src/constants.ts
Normal file
31
src/constants.ts
Normal file
|
|
@ -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 = [
|
||||
'@',
|
||||
'%',
|
||||
'&',
|
||||
'!',
|
||||
'?',
|
||||
'+',
|
||||
';',
|
||||
'.',
|
||||
'=',
|
||||
'^',
|
||||
'§',
|
||||
'$',
|
||||
'-',
|
||||
'_',
|
||||
'(',
|
||||
'{',
|
||||
];
|
||||
162
src/editor/suggestion.ts
Normal file
162
src/editor/suggestion.ts
Normal file
|
|
@ -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<MentionSuggestion> {
|
||||
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;
|
||||
}
|
||||
467
src/main.ts
467
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()
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
100
src/mention/file-indexer.ts
Normal file
100
src/mention/file-indexer.ts
Normal file
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
64
src/mention/link-utils.ts
Normal file
64
src/mention/link-utils.ts
Normal file
|
|
@ -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}]]`;
|
||||
}
|
||||
61
src/mention/mention-manager.ts
Normal file
61
src/mention/mention-manager.ts
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
143
src/settings/settings-tab.ts
Normal file
143
src/settings/settings-tab.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
52
src/types.ts
Normal file
52
src/types.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in a new issue