mirror of
https://github.com/dnlbauer/obsidian-dataview-autocompletion.git
synced 2026-09-10 22:25:29 +00:00
159 lines
5.5 KiB
TypeScript
159 lines
5.5 KiB
TypeScript
import {
|
|
App,
|
|
Editor,
|
|
EditorPosition,
|
|
EditorSuggest,
|
|
EditorSuggestContext,
|
|
EditorSuggestTriggerInfo,
|
|
htmlToMarkdown,
|
|
MarkdownRenderer,
|
|
MarkdownView,
|
|
TFile,
|
|
} from "obsidian";
|
|
import { getTriggerText } from "./trigger";
|
|
import uFuzzy from "@leeoniya/ufuzzy";
|
|
import DataviewAutocompletePlugin from "./main";
|
|
import { SuggestionIndex } from "./SuggestionIndex";
|
|
|
|
export class DataviewSuggester extends EditorSuggest<String> {
|
|
plugin: DataviewAutocompletePlugin;
|
|
|
|
maxSuggestions: number;
|
|
searcher: uFuzzy;
|
|
suggestionsIndex: SuggestionIndex;
|
|
|
|
initialized: boolean = false;
|
|
|
|
constructor(
|
|
plugin: DataviewAutocompletePlugin,
|
|
maxSuggestions: number = 10,
|
|
singleErrorMode: boolean = false,
|
|
allowExtraChars: boolean = false,
|
|
) {
|
|
super(plugin.app);
|
|
this.plugin = plugin;
|
|
this.maxSuggestions = maxSuggestions;
|
|
this.searcher = new uFuzzy({
|
|
intraMode: singleErrorMode ? 1 : 0,
|
|
intraIns: allowExtraChars ? 1 : 0,
|
|
});
|
|
this.suggestionsIndex = new SuggestionIndex(plugin);
|
|
}
|
|
|
|
override onTrigger(cursor: EditorPosition, editor: Editor, file: TFile): EditorSuggestTriggerInfo | null {
|
|
const line = editor.getLine(cursor.line);
|
|
|
|
let trigger = getTriggerText(line, cursor.ch);
|
|
if (trigger !== null) {
|
|
return {
|
|
query: trigger[0],
|
|
start: { line: cursor.line, ch: trigger[1] },
|
|
end: { line: cursor.line, ch: trigger[2] },
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
override getSuggestions(context: EditorSuggestContext): string[] | Promise<string[]> {
|
|
const suggestionsList = this.suggestionsIndex.suggestionsList;
|
|
|
|
const idxs = this.searcher.filter(suggestionsList, context.query);
|
|
if (idxs != null && idxs.length > 0) {
|
|
let info = this.searcher.info(idxs, suggestionsList, context.query);
|
|
let order = this.searcher.sort(info, suggestionsList, context.query);
|
|
|
|
// return top N suggestions with marks
|
|
return order
|
|
.slice(0, this.maxSuggestions)
|
|
.map((idx) => [idx, suggestionsList[info.idx[idx]]])
|
|
.map((suggestion: [number, string]) => uFuzzy.highlight(suggestion[1], info.ranges[suggestion[0]]));
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Renders a suggestion string to the HTML element using a markdown renderer
|
|
* Uses the context to get the current editor and cursor position and
|
|
* finds out if the suggestion should be rendered with or without the
|
|
* field name.
|
|
*/
|
|
renderMarkdownSuggestion(value: string, el: HTMLElement): void {
|
|
// render markdown preview of inline dataview metadata field
|
|
let suggestionText = this.context!.editor.getLine(this.context!.start.line).slice(
|
|
this.context!.start.ch - 1,
|
|
this.context!.start.ch,
|
|
)!;
|
|
suggestionText += value.replace(/<mark>(.*?)<\/mark>/g, "$1");
|
|
suggestionText += suggestionText.startsWith("(") ? ")" : "]";
|
|
|
|
MarkdownRenderer.render(
|
|
this.app,
|
|
htmlToMarkdown(suggestionText),
|
|
el,
|
|
this.context!.file.path,
|
|
this.app.workspace.getActiveViewOfType(MarkdownView)!,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Render the suggestion as markdown source code with highlights
|
|
*/
|
|
renderSourceSuggestion(value: string, el: HTMLElement): void {
|
|
// render source text with highlights
|
|
// replaces <mark>...</mark> with <span>...</span>
|
|
const parts = value.split(/(<mark>.*?<\/mark>)/g);
|
|
parts.forEach((textPart) => {
|
|
if (textPart.startsWith("<mark>") && textPart.endsWith("</mark>")) {
|
|
const text = textPart.slice(6, -7); // Remove <mark> and </mark>
|
|
el.createEl("span", { text, cls: "dataview-suggestion-highlight" });
|
|
} else {
|
|
el.appendText(textPart);
|
|
}
|
|
});
|
|
}
|
|
|
|
override renderSuggestion(value: string, el: HTMLElement): void {
|
|
const container = el.createDiv("dataview-suggestion-content");
|
|
const titleDiv = container.createDiv("dataview-suggestion-title");
|
|
const noteDiv = container.createDiv("dataview-suggestion-note");
|
|
|
|
this.renderMarkdownSuggestion(value, titleDiv);
|
|
this.renderSourceSuggestion(value, noteDiv);
|
|
}
|
|
|
|
override selectSuggestion(value: string, evt: MouseEvent | KeyboardEvent): void {
|
|
// remove marks from selection
|
|
value = value.replace(/<mark>(.*?)<\/mark>/g, "$1");
|
|
|
|
const { editor, start, end } = this.context!;
|
|
editor.replaceRange(value, start, end);
|
|
|
|
const newCursorPos = {
|
|
line: end.line,
|
|
ch: start.ch + value.length + 1,
|
|
};
|
|
editor.setCursor(newCursorPos);
|
|
}
|
|
|
|
public onDataviewIndexReady() {
|
|
this.suggestionsIndex.buildNewIndex();
|
|
this.initialized = true;
|
|
}
|
|
|
|
// possible types: update, rename, delete. rename has oldPath
|
|
public onDataviewMetadataChange(type: string, file: TFile, oldPath?: string) {
|
|
if (!this.initialized) {
|
|
// console.warn("Dataview Autocompletion index not ready yet. Skipping index update");
|
|
return;
|
|
}
|
|
this.suggestionsIndex.updateIndex(type, file, oldPath);
|
|
}
|
|
|
|
/**
|
|
* Trigger a full rebuild of the suggestion index
|
|
*/
|
|
public rebuildIndex() {
|
|
this.suggestionsIndex.buildNewIndex();
|
|
}
|
|
}
|