Files
obsidian-dataview-autocompl…/src/DataviewSuggester.ts
2024-12-13 13:48:11 +01:00

187 lines
5.9 KiB
TypeScript

import {
App,
Editor,
Plugin,
EditorPosition,
EditorSuggest,
EditorSuggestContext,
EditorSuggestTriggerInfo,
TFile,
} from "obsidian";
import { getTriggerText } from "./trigger";
import uFuzzy from "@leeoniya/ufuzzy";
import { getAPI, DataviewApi } from "obsidian-dataview";
export class DataviewSuggester extends EditorSuggest<String> {
suggestionsList: string[] = [];
maxSuggestions: number;
searcher: uFuzzy;
dataviewApi: DataviewApi;
constructor(
plugin: Plugin,
maxSuggestions: number = 10,
singleErrorMode: boolean = false,
allowExtraChars: boolean = false,
) {
super(plugin.app);
this.maxSuggestions = maxSuggestions;
this.searcher = new uFuzzy({
intraMode: singleErrorMode ? 1 : 0,
intraIns: allowExtraChars ? 1 : 0,
});
this.dataviewApi = getAPI(plugin.app);
}
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;
}
getSuggestions(
context: EditorSuggestContext,
): string[] | Promise<string[]> {
let idxs = this.searcher.filter(this.suggestionsList, context.query);
if (idxs != null && idxs.length > 0) {
let info = this.searcher.info(
idxs,
this.suggestionsList,
context.query,
);
let order = this.searcher.sort(
info,
this.suggestionsList,
context.query,
);
// return top N suggestions with marks
return order
.slice(0, this.maxSuggestions)
.map((idx) => [idx, this.suggestionsList[info.idx[idx]]])
.map((suggestion: [number, string]) =>
uFuzzy.highlight(suggestion[1], info.ranges[suggestion[0]]),
);
}
return [];
}
renderSuggestion(value: string, el: HTMLElement): void {
// replace marks with bold
const formattedHtml = value.replace(
/<mark>(.*?)<\/mark>/g,
'<span style="font-weight: bold;">$1</span>',
);
el.innerHTML = formattedHtml;
}
selectSuggestion(value: string, evt: MouseEvent | KeyboardEvent): void {
const { editor, start, end } = this.context!;
// remove marks from selection
value = value.replace(/<mark>(.*?)<\/mark>/g, "$1");
editor.replaceRange(value, start, end);
const newCursorPos = {
line: end.line + 1,
ch: 0,
};
editor.setCursor(newCursorPos);
}
public onDataviewIndexReady() {
this.buildNewIndex();
}
// possible types: update, rename, delete. rename has oldPath
public onDataviewMetadataChange(
type: string,
file: TFile,
oldPath?: string,
) {
// TODO handle efficiently with delta updates
this.buildNewIndex();
}
/**
* Converts a Dataview value to a string
* returns the composite value in format "key:: value"
*/
formatCompositeValue(key: string, value: any): string {
let stringValue: string;
// If the value is a string, number or boolean, we can simply convert it to a string
if (
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean"
) {
stringValue = value.toString();
} else if (
this.dataviewApi.value.typeOf(value) === "link" &&
value.type === "file" &&
value.display !== undefined
) {
// value.toString always adds a display value to wiki-style links.
// parse wiki-style links without display value manually here to prevent this from happening for [[filename]]
stringValue = `[[${value.path.split("/").pop().replace(".md", "")}]]`;
} else {
stringValue = this.dataviewApi.value.toString(value);
}
return `${key}:: ${stringValue}`;
}
buildNewIndex() {
console.log("Begin Rebuilding dataview suggestion index");
const startTime = performance.now();
const newSuggestions: string[] = [];
// Iterate all pages of the Dataview index, and ingest all fields into suggestions
// TODO: can we use official dataview api?
// @ts-ignore
for (const page of this.app.plugins.plugins.dataview.index.pages) {
const fields = page[1].fields;
for (let [key, val] of fields) {
// fields can be a single value or a dict, so we need to handle both
let arrayVal;
if (!Array.isArray(val)) {
arrayVal = [val];
} else {
arrayVal = val;
}
// Add composite value "key:: value" to suggestions list
for (const value of arrayVal) {
if (value === null || value === undefined) continue; // skip empty fields
let compositeValue = this.formatCompositeValue(key, value);
if (newSuggestions.indexOf(compositeValue) === -1) {
newSuggestions.push(compositeValue);
}
}
}
}
// replace old index
this.suggestionsList = newSuggestions;
const endTime = performance.now();
console.log(
`Rebuilt dataview autocomplete index (${this.suggestionsList.length} elements, ${(endTime - startTime).toFixed(2)}ms)`,
);
}
}