mirror of
https://github.com/dnlbauer/obsidian-dataview-autocompletion.git
synced 2026-09-10 22:25:29 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
325f2384b1 | ||
|
|
e3d6be193a | ||
|
|
f5f9d49b0b | ||
|
|
6579606935 | ||
|
|
26227eac6e | ||
|
|
ba4dab5dc4 | ||
|
|
6b008d452a | ||
|
|
314f99f462 | ||
|
|
064320bc5b | ||
|
|
5f7fdb244a | ||
|
|
a820b4453d | ||
|
|
4d9673eff7 | ||
|
|
ddedb4e8c1 | ||
|
|
41c9e60615 | ||
|
|
70d0b94a62 |
@@ -1,8 +1,8 @@
|
|||||||
# Obsidian Dataview Autocompletion
|
# Dataview Autocompletion
|
||||||
|
|
||||||
This is a plugin for Obsidian (https://obsidian.md).
|
This is a plugin for Obsidian (https://obsidian.md).
|
||||||
|
|
||||||
Obsidian Dataview Autocomplete enhances the functionality of the popular [Dataview plugin](https://github.com/blacksmithgu/obsidian-dataview)
|
Dataview Autocomplete enhances the functionality of the popular [Dataview plugin](https://github.com/blacksmithgu/obsidian-dataview)
|
||||||
by providing autocomplete suggestions for metadata fields in the editor. This makes it easy to reuse existing fields and reduces typing errors.
|
by providing autocomplete suggestions for metadata fields in the editor. This makes it easy to reuse existing fields and reduces typing errors.
|
||||||
|
|
||||||
<img src="./assets/demo.gif" alt="Demo" width="400" >
|
<img src="./assets/demo.gif" alt="Demo" width="400" >
|
||||||
@@ -14,7 +14,9 @@ by providing autocomplete suggestions for metadata fields in the editor. This ma
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
While plugin is [waiting](https://github.com/obsidianmd/obsidian-releases/pull/4913) to be reviewed and added to the community plugin list, you can install it manually following the steps below:
|
The plugin is available through Obsidian as a community plugin.
|
||||||
|
|
||||||
|
### Manual installation
|
||||||
|
|
||||||
1. Download the latest release files (manifest.json, styles.css, main.js) from the Releases page.
|
1. Download the latest release files (manifest.json, styles.css, main.js) from the Releases page.
|
||||||
2. Create a folder named "dataview-autocompletion" in the Obsidian plugins folder (.obsidian/plugins).
|
2. Create a folder named "dataview-autocompletion" in the Obsidian plugins folder (.obsidian/plugins).
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "dataview-autocompletion",
|
"id": "dataview-autocompletion",
|
||||||
"name": "Dataview Autocompletion",
|
"name": "Dataview Autocompletion",
|
||||||
"version": "0.9.1",
|
"version": "0.9.5",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "Adds autocompletion to Dataview metadata fields",
|
"description": "Adds autocompletion to Dataview metadata fields",
|
||||||
"author": "Daniel Bauer",
|
"author": "Daniel Bauer",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-dataview-autocompletion",
|
"name": "obsidian-dataview-autocompletion",
|
||||||
"version": "0.9.1",
|
"version": "0.9.5",
|
||||||
"description": "This is a plugin for Obsidian that provides autocompletion for Dataview metadata fields",
|
"description": "This is a plugin for Obsidian that provides autocompletion for Dataview metadata fields",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -12,19 +12,17 @@ import {
|
|||||||
} from "obsidian";
|
} from "obsidian";
|
||||||
import { getTriggerText } from "./trigger";
|
import { getTriggerText } from "./trigger";
|
||||||
import uFuzzy from "@leeoniya/ufuzzy";
|
import uFuzzy from "@leeoniya/ufuzzy";
|
||||||
import { getAPI, DataviewApi } from "obsidian-dataview";
|
|
||||||
import DataviewAutocompletePlugin from "./main";
|
import DataviewAutocompletePlugin from "./main";
|
||||||
|
import { SuggestionIndex } from "./SuggestionIndex";
|
||||||
|
|
||||||
export class DataviewSuggester extends EditorSuggest<String> {
|
export class DataviewSuggester extends EditorSuggest<String> {
|
||||||
plugin: DataviewAutocompletePlugin;
|
plugin: DataviewAutocompletePlugin;
|
||||||
|
|
||||||
maxSuggestions: number;
|
maxSuggestions: number;
|
||||||
searcher: uFuzzy;
|
searcher: uFuzzy;
|
||||||
|
suggestionsIndex: SuggestionIndex;
|
||||||
|
|
||||||
initialized: boolean = false;
|
initialized: boolean = false;
|
||||||
suggestionsList: string[] = [];
|
|
||||||
suggestionsRefs: Map<string, string[]> = new Map(); // maps file paths to included suggestions
|
|
||||||
suggestionsRefCount: Map<string, number> = new Map(); // maps suggestions to number of files including them
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
plugin: DataviewAutocompletePlugin,
|
plugin: DataviewAutocompletePlugin,
|
||||||
@@ -39,9 +37,10 @@ export class DataviewSuggester extends EditorSuggest<String> {
|
|||||||
intraMode: singleErrorMode ? 1 : 0,
|
intraMode: singleErrorMode ? 1 : 0,
|
||||||
intraIns: allowExtraChars ? 1 : 0,
|
intraIns: allowExtraChars ? 1 : 0,
|
||||||
});
|
});
|
||||||
|
this.suggestionsIndex = new SuggestionIndex(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
onTrigger(cursor: EditorPosition, editor: Editor, file: TFile): EditorSuggestTriggerInfo | null {
|
override onTrigger(cursor: EditorPosition, editor: Editor, file: TFile): EditorSuggestTriggerInfo | null {
|
||||||
const line = editor.getLine(cursor.line);
|
const line = editor.getLine(cursor.line);
|
||||||
|
|
||||||
let trigger = getTriggerText(line, cursor.ch);
|
let trigger = getTriggerText(line, cursor.ch);
|
||||||
@@ -55,21 +54,29 @@ export class DataviewSuggester extends EditorSuggest<String> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSuggestions(context: EditorSuggestContext): string[] | Promise<string[]> {
|
override getSuggestions(context: EditorSuggestContext): string[] | Promise<string[]> {
|
||||||
const idxs = this.searcher.filter(this.suggestionsList, context.query);
|
const suggestionsList = this.suggestionsIndex.suggestionsList;
|
||||||
|
|
||||||
|
const idxs = this.searcher.filter(suggestionsList, context.query);
|
||||||
if (idxs != null && idxs.length > 0) {
|
if (idxs != null && idxs.length > 0) {
|
||||||
let info = this.searcher.info(idxs, this.suggestionsList, context.query);
|
let info = this.searcher.info(idxs, suggestionsList, context.query);
|
||||||
let order = this.searcher.sort(info, this.suggestionsList, context.query);
|
let order = this.searcher.sort(info, suggestionsList, context.query);
|
||||||
|
|
||||||
// return top N suggestions with marks
|
// return top N suggestions with marks
|
||||||
return order
|
return order
|
||||||
.slice(0, this.maxSuggestions)
|
.slice(0, this.maxSuggestions)
|
||||||
.map((idx) => [idx, this.suggestionsList[info.idx[idx]]])
|
.map((idx) => [idx, suggestionsList[info.idx[idx]]])
|
||||||
.map((suggestion: [number, string]) => uFuzzy.highlight(suggestion[1], info.ranges[suggestion[0]]));
|
.map((suggestion: [number, string]) => uFuzzy.highlight(suggestion[1], info.ranges[suggestion[0]]));
|
||||||
}
|
}
|
||||||
return [];
|
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 {
|
renderMarkdownSuggestion(value: string, el: HTMLElement): void {
|
||||||
// render markdown preview of inline dataview metadata field
|
// render markdown preview of inline dataview metadata field
|
||||||
let suggestionText = this.context!.editor.getLine(this.context!.start.line).slice(
|
let suggestionText = this.context!.editor.getLine(this.context!.start.line).slice(
|
||||||
@@ -79,15 +86,26 @@ export class DataviewSuggester extends EditorSuggest<String> {
|
|||||||
suggestionText += value.replace(/<mark>(.*?)<\/mark>/g, "$1");
|
suggestionText += value.replace(/<mark>(.*?)<\/mark>/g, "$1");
|
||||||
suggestionText += suggestionText.startsWith("(") ? ")" : "]";
|
suggestionText += suggestionText.startsWith("(") ? ")" : "]";
|
||||||
|
|
||||||
|
// Different rendering for prefix suggestions
|
||||||
|
var markdownText;
|
||||||
|
if (/.*\:: [\]\)]/.test(suggestionText)) {
|
||||||
|
markdownText = htmlToMarkdown("[" + suggestionText.slice(1, -1) + "*…*]");
|
||||||
|
} else {
|
||||||
|
markdownText = htmlToMarkdown(suggestionText);
|
||||||
|
}
|
||||||
|
console.log(markdownText);
|
||||||
MarkdownRenderer.render(
|
MarkdownRenderer.render(
|
||||||
this.app,
|
this.app,
|
||||||
htmlToMarkdown(suggestionText),
|
markdownText,
|
||||||
el,
|
el,
|
||||||
this.context!.file.path,
|
this.context!.file.path,
|
||||||
this.app.workspace.getActiveViewOfType(MarkdownView)!,
|
this.app.workspace.getActiveViewOfType(MarkdownView)!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the suggestion as markdown source code with highlights
|
||||||
|
*/
|
||||||
renderSourceSuggestion(value: string, el: HTMLElement): void {
|
renderSourceSuggestion(value: string, el: HTMLElement): void {
|
||||||
// render source text with highlights
|
// render source text with highlights
|
||||||
// replaces <mark>...</mark> with <span>...</span>
|
// replaces <mark>...</mark> with <span>...</span>
|
||||||
@@ -102,7 +120,7 @@ export class DataviewSuggester extends EditorSuggest<String> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSuggestion(value: string, el: HTMLElement): void {
|
override renderSuggestion(value: string, el: HTMLElement): void {
|
||||||
const container = el.createDiv("dataview-suggestion-content");
|
const container = el.createDiv("dataview-suggestion-content");
|
||||||
const titleDiv = container.createDiv("dataview-suggestion-title");
|
const titleDiv = container.createDiv("dataview-suggestion-title");
|
||||||
const noteDiv = container.createDiv("dataview-suggestion-note");
|
const noteDiv = container.createDiv("dataview-suggestion-note");
|
||||||
@@ -111,22 +129,32 @@ export class DataviewSuggester extends EditorSuggest<String> {
|
|||||||
this.renderSourceSuggestion(value, noteDiv);
|
this.renderSourceSuggestion(value, noteDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectSuggestion(value: string, evt: MouseEvent | KeyboardEvent): void {
|
override selectSuggestion(value: string, evt: MouseEvent | KeyboardEvent): void {
|
||||||
// remove marks from selection
|
// remove marks from selection
|
||||||
value = value.replace(/<mark>(.*?)<\/mark>/g, "$1");
|
value = value.replace(/<mark>(.*?)<\/mark>/g, "$1");
|
||||||
|
|
||||||
const { editor, start, end } = this.context!;
|
const { editor, start, end } = this.context!;
|
||||||
editor.replaceRange(value, start, end);
|
editor.replaceRange(value, start, end);
|
||||||
|
|
||||||
const newCursorPos = {
|
// move cursor to end of suggestion for finished suggestions
|
||||||
|
// or to the end of the prefix
|
||||||
|
var newCursorPos;
|
||||||
|
if (value.endsWith(":: ")) {
|
||||||
|
newCursorPos = {
|
||||||
|
line: end.line,
|
||||||
|
ch: start.ch + value.length,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
newCursorPos = {
|
||||||
line: end.line,
|
line: end.line,
|
||||||
ch: start.ch + value.length + 1,
|
ch: start.ch + value.length + 1,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
editor.setCursor(newCursorPos);
|
editor.setCursor(newCursorPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDataviewIndexReady() {
|
public onDataviewIndexReady() {
|
||||||
this.buildNewIndex();
|
this.suggestionsIndex.buildNewIndex();
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,218 +164,13 @@ export class DataviewSuggester extends EditorSuggest<String> {
|
|||||||
// console.warn("Dataview Autocompletion index not ready yet. Skipping index update");
|
// console.warn("Dataview Autocompletion index not ready yet. Skipping index update");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.updateIndex(type, file, oldPath);
|
this.suggestionsIndex.updateIndex(type, file, oldPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a Dataview value to a string
|
* Trigger a full rebuild of the suggestion index
|
||||||
* returns the composite value in format "key:: value"
|
|
||||||
*/
|
*/
|
||||||
formatCompositeValue(key: string, value: any): string {
|
public rebuildIndex() {
|
||||||
const dataviewAPI = getAPI(this.app);
|
this.suggestionsIndex.buildNewIndex();
|
||||||
|
|
||||||
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 (dataviewAPI.value.typeOf(value) === "link" && value.type === "file") {
|
|
||||||
// parse wiki-style links
|
|
||||||
if (value.display !== undefined) {
|
|
||||||
stringValue = `[[${value.path.split("/").pop().replace(".md", "")}|${value.display}]]`;
|
|
||||||
} else {
|
|
||||||
stringValue = `[[${value.path.split("/").pop().replace(".md", "")}]]`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stringValue = dataviewAPI.value.toString(value);
|
|
||||||
}
|
|
||||||
return `${key}:: ${stringValue}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterCompositeValue(compositeValue: string): boolean {
|
|
||||||
for (const filterPattern of this.plugin.settings.ignoredFields) {
|
|
||||||
const regex = new RegExp(`^(${filterPattern})::.*`);
|
|
||||||
if (regex.test(compositeValue)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterFile(filepath: string): boolean {
|
|
||||||
for (const filterPattern of this.plugin.settings.ignoredFiles) {
|
|
||||||
const regex = new RegExp(filterPattern);
|
|
||||||
if (regex.test(filepath)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
buildNewIndex() {
|
|
||||||
console.log("Begin Rebuilding dataview suggestion index");
|
|
||||||
const startTime = performance.now();
|
|
||||||
const dataviewApi = getAPI(this.app);
|
|
||||||
|
|
||||||
const newSuggestions: string[] = [];
|
|
||||||
const newSuggestionsRefs: Map<string, string[]> = new Map();
|
|
||||||
const newSuggestionsRefCount: Map<string, number> = new Map();
|
|
||||||
|
|
||||||
const files = this.app.vault.getFiles();
|
|
||||||
for (const file of files) {
|
|
||||||
if (!this.filterFile(file.path)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const page = dataviewApi.page(file.path);
|
|
||||||
if (page === undefined) continue; // not a markdown file
|
|
||||||
|
|
||||||
const fields = Object.keys(page)
|
|
||||||
.filter((k) => k !== "file")
|
|
||||||
.map((k) => [k, page[k]]);
|
|
||||||
|
|
||||||
const pageRefs = [];
|
|
||||||
|
|
||||||
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 (!this.filterCompositeValue(compositeValue)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newSuggestions.indexOf(compositeValue) === -1) {
|
|
||||||
// suggestion not seen on any page yet
|
|
||||||
pageRefs.push(compositeValue);
|
|
||||||
newSuggestionsRefCount.set(compositeValue, 1);
|
|
||||||
newSuggestions.push(compositeValue);
|
|
||||||
} else if (pageRefs.indexOf(compositeValue) === -1) {
|
|
||||||
// suggestion not seen on this page, but on another
|
|
||||||
pageRefs.push(compositeValue);
|
|
||||||
newSuggestionsRefCount.set(compositeValue, newSuggestionsRefCount.get(compositeValue)! - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newSuggestionsRefs.set(file.path, pageRefs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace old index
|
|
||||||
this.suggestionsList = newSuggestions;
|
|
||||||
this.suggestionsRefCount = newSuggestionsRefCount;
|
|
||||||
this.suggestionsRefs = newSuggestionsRefs;
|
|
||||||
|
|
||||||
const endTime = performance.now();
|
|
||||||
console.log(
|
|
||||||
`Rebuilt dataview autocomplete index (${this.suggestionsList.length} elements, ${(endTime - startTime).toFixed(2)}ms)`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateIndex(type: string, file: TFile, oldPath?: string) {
|
|
||||||
// also triggers on create!
|
|
||||||
if (type === "update") {
|
|
||||||
if (!this.filterFile(file.path)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const updateCompositeValues = [];
|
|
||||||
|
|
||||||
const page = getAPI(this.app).page(file.path);
|
|
||||||
if (page === undefined) return; // not a markdown file
|
|
||||||
|
|
||||||
const fields = Object.keys(page)
|
|
||||||
.filter((k) => k !== "file")
|
|
||||||
.map((k) => [k, page[k]]);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const value of arrayVal) {
|
|
||||||
if (value === null || value === undefined) continue; // skip empty fields
|
|
||||||
|
|
||||||
let compositeValue = this.formatCompositeValue(key, value);
|
|
||||||
if (!this.filterCompositeValue(compositeValue)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (updateCompositeValues.indexOf(compositeValue) === -1) {
|
|
||||||
updateCompositeValues.push(compositeValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldCompositeValues = this.suggestionsRefs.get(file.path) || [];
|
|
||||||
|
|
||||||
// deleting value from index if update reduces refcount to 0
|
|
||||||
for (const oldCompositeValue of oldCompositeValues) {
|
|
||||||
if (updateCompositeValues.indexOf(oldCompositeValue) === -1) {
|
|
||||||
// delete value
|
|
||||||
this.suggestionsRefCount.set(
|
|
||||||
oldCompositeValue,
|
|
||||||
this.suggestionsRefCount.get(oldCompositeValue)! - 1,
|
|
||||||
);
|
|
||||||
if (this.suggestionsRefCount.get(oldCompositeValue) === 0) {
|
|
||||||
this.suggestionsList.splice(this.suggestionsList.indexOf(oldCompositeValue), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// adding value to index if not present in index
|
|
||||||
for (const newCompositeValue of updateCompositeValues) {
|
|
||||||
if (!this.suggestionsRefCount.has(newCompositeValue)) {
|
|
||||||
// not seen in this or other files
|
|
||||||
this.suggestionsList.push(newCompositeValue);
|
|
||||||
this.suggestionsRefCount.set(newCompositeValue, 1);
|
|
||||||
} else if (oldCompositeValues.indexOf(newCompositeValue) === -1) {
|
|
||||||
this.suggestionsRefCount.set(
|
|
||||||
newCompositeValue,
|
|
||||||
this.suggestionsRefCount.get(newCompositeValue)! + 1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.suggestionsRefs.set(file.path, updateCompositeValues);
|
|
||||||
} else if (type === "rename") {
|
|
||||||
if (this.filterFile(file.path) && this.filterFile(oldPath!)) {
|
|
||||||
// both not ignored -> move refs
|
|
||||||
this.suggestionsRefs.set(file.path, this.suggestionsRefs.get(oldPath!)!);
|
|
||||||
this.suggestionsRefs.delete(oldPath!);
|
|
||||||
} else if (this.filterFile(file.path)) {
|
|
||||||
// old path ignored, new path not ignored -> upsert
|
|
||||||
this.updateIndex("update", file, undefined);
|
|
||||||
} else if (this.filterFile(oldPath!)) {
|
|
||||||
// old path not ignored, new path ignored -> delete
|
|
||||||
for (const value of this.suggestionsRefs.get(oldPath!)!) {
|
|
||||||
this.suggestionsRefCount.set(value, this.suggestionsRefCount.get(value)! - 1);
|
|
||||||
if (this.suggestionsRefCount.get(value) === 0) {
|
|
||||||
this.suggestionsList.splice(this.suggestionsList.indexOf(value), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.suggestionsRefs.delete(oldPath!);
|
|
||||||
}
|
|
||||||
} else if (type === "delete") {
|
|
||||||
// iterate suggestion refs in deleted file and decrement their ref count
|
|
||||||
// if the ref count reaches 0, remove the suggestion from the list
|
|
||||||
for (const value of this.suggestionsRefs.get(file.path)!) {
|
|
||||||
this.suggestionsRefCount.set(value, this.suggestionsRefCount.get(value)! - 1);
|
|
||||||
if (this.suggestionsRefCount.get(value) === 0) {
|
|
||||||
this.suggestionsList.splice(this.suggestionsList.indexOf(value), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.suggestionsRefs.delete(file.path);
|
|
||||||
} else {
|
|
||||||
console.debug("Unknown update type:", type, file, oldPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export class SettingsTab extends PluginSettingTab {
|
|||||||
.map((val) => val.trim())
|
.map((val) => val.trim())
|
||||||
.filter((val) => val.length > 0);
|
.filter((val) => val.length > 0);
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
this.plugin.suggester?.buildNewIndex();
|
this.plugin.suggester?.rebuildIndex();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
@@ -50,7 +50,7 @@ export class SettingsTab extends PluginSettingTab {
|
|||||||
.map((val) => val.trim())
|
.map((val) => val.trim())
|
||||||
.filter((val) => val.length > 0);
|
.filter((val) => val.length > 0);
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
this.plugin.suggester?.buildNewIndex();
|
this.plugin.suggester?.rebuildIndex();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
222
src/SuggestionIndex.ts
Normal file
222
src/SuggestionIndex.ts
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import { App, TFile } from "obsidian";
|
||||||
|
import { DataObject, getAPI } from "obsidian-dataview";
|
||||||
|
import DataviewAutocompletePlugin from "./main";
|
||||||
|
|
||||||
|
export class SuggestionIndex {
|
||||||
|
plugin: DataviewAutocompletePlugin;
|
||||||
|
|
||||||
|
suggestionsList: string[] = [];
|
||||||
|
private suggestionsRefs: Map<string, string[]> = new Map(); // maps file paths to included suggestions
|
||||||
|
private suggestionsRefCount: Map<string, number> = new Map(); // maps suggestions to number of files including them
|
||||||
|
|
||||||
|
constructor(plugin: DataviewAutocompletePlugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildNewIndex() {
|
||||||
|
// console.log("Begin Rebuilding dataview suggestion index");
|
||||||
|
// const startTime = performance.now();
|
||||||
|
const dataviewApi = getAPI(this.plugin.app);
|
||||||
|
|
||||||
|
const newSuggestions: string[] = [];
|
||||||
|
const newSuggestionsRefs: Map<string, string[]> = new Map();
|
||||||
|
const newSuggestionsRefCount: Map<string, number> = new Map();
|
||||||
|
|
||||||
|
const files = this.plugin.app.vault.getFiles();
|
||||||
|
for (const file of files) {
|
||||||
|
if (!this.filterFile(file.path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = dataviewApi.page(file.path);
|
||||||
|
if (page === undefined) continue; // not a markdown file
|
||||||
|
|
||||||
|
const compositeValues = this.extractCompositeValuesFromPage(page);
|
||||||
|
const pageRefs = [];
|
||||||
|
|
||||||
|
for (let compositeValue of compositeValues) {
|
||||||
|
if (newSuggestions.indexOf(compositeValue) === -1) {
|
||||||
|
// suggestion not seen on any page yet
|
||||||
|
pageRefs.push(compositeValue);
|
||||||
|
newSuggestionsRefCount.set(compositeValue, 1);
|
||||||
|
newSuggestions.push(compositeValue);
|
||||||
|
} else if (pageRefs.indexOf(compositeValue) === -1) {
|
||||||
|
// suggestion not seen on this page, but on another
|
||||||
|
pageRefs.push(compositeValue);
|
||||||
|
newSuggestionsRefCount.set(compositeValue, newSuggestionsRefCount.get(compositeValue)! - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newSuggestionsRefs.set(file.path, pageRefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace old index
|
||||||
|
this.suggestionsList = newSuggestions;
|
||||||
|
this.suggestionsRefCount = newSuggestionsRefCount;
|
||||||
|
this.suggestionsRefs = newSuggestionsRefs;
|
||||||
|
|
||||||
|
// const endTime = performance.now();
|
||||||
|
// console.log(
|
||||||
|
// `Rebuilt dataview autocomplete index (${this.suggestionsList.length} elements, ${(endTime - startTime).toFixed(2)}ms)`,
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateIndex(type: string, file: TFile, oldPath?: string) {
|
||||||
|
// also triggers on create!
|
||||||
|
if (type === "update") {
|
||||||
|
if (!this.filterFile(file.path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = getAPI(this.plugin.app).page(file.path);
|
||||||
|
if (page === undefined) return; // not a markdown file
|
||||||
|
const updateCompositeValues = this.extractCompositeValuesFromPage(page);
|
||||||
|
|
||||||
|
const oldCompositeValues = this.suggestionsRefs.get(file.path) || [];
|
||||||
|
|
||||||
|
// deleting value from index if update reduces refcount to 0
|
||||||
|
for (const oldCompositeValue of oldCompositeValues) {
|
||||||
|
if (updateCompositeValues.indexOf(oldCompositeValue) === -1) {
|
||||||
|
// delete value
|
||||||
|
this.suggestionsRefCount.set(
|
||||||
|
oldCompositeValue,
|
||||||
|
this.suggestionsRefCount.get(oldCompositeValue)! - 1,
|
||||||
|
);
|
||||||
|
if (this.suggestionsRefCount.get(oldCompositeValue) === 0) {
|
||||||
|
this.suggestionsList.splice(this.suggestionsList.indexOf(oldCompositeValue), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// adding value to index if not present in index
|
||||||
|
for (const newCompositeValue of updateCompositeValues) {
|
||||||
|
if (
|
||||||
|
!this.suggestionsRefCount.has(newCompositeValue) ||
|
||||||
|
this.suggestionsRefCount.get(newCompositeValue) === 0
|
||||||
|
) {
|
||||||
|
// not seen in this or other files
|
||||||
|
this.suggestionsList.push(newCompositeValue);
|
||||||
|
this.suggestionsRefCount.set(newCompositeValue, 1);
|
||||||
|
} else if (oldCompositeValues.indexOf(newCompositeValue) === -1) {
|
||||||
|
this.suggestionsRefCount.set(
|
||||||
|
newCompositeValue,
|
||||||
|
this.suggestionsRefCount.get(newCompositeValue)! + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.suggestionsRefs.set(file.path, updateCompositeValues);
|
||||||
|
} else if (type === "rename") {
|
||||||
|
if (this.filterFile(file.path) && this.filterFile(oldPath!)) {
|
||||||
|
// both not ignored -> move refs
|
||||||
|
this.suggestionsRefs.set(file.path, this.suggestionsRefs.get(oldPath!)!);
|
||||||
|
this.suggestionsRefs.delete(oldPath!);
|
||||||
|
} else if (this.filterFile(file.path)) {
|
||||||
|
// old path ignored, new path not ignored -> upsert
|
||||||
|
this.updateIndex("update", file, undefined);
|
||||||
|
} else if (this.filterFile(oldPath!)) {
|
||||||
|
// old path not ignored, new path ignored -> delete
|
||||||
|
for (const value of this.suggestionsRefs.get(oldPath!)!) {
|
||||||
|
this.suggestionsRefCount.set(value, this.suggestionsRefCount.get(value)! - 1);
|
||||||
|
if (this.suggestionsRefCount.get(value) === 0) {
|
||||||
|
this.suggestionsList.splice(this.suggestionsList.indexOf(value), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.suggestionsRefs.delete(oldPath!);
|
||||||
|
}
|
||||||
|
} else if (type === "delete") {
|
||||||
|
// iterate suggestion refs in deleted file and decrement their ref count
|
||||||
|
// if the ref count reaches 0, remove the suggestion from the list
|
||||||
|
for (const value of this.suggestionsRefs.get(file.path)!) {
|
||||||
|
this.suggestionsRefCount.set(value, this.suggestionsRefCount.get(value)! - 1);
|
||||||
|
if (this.suggestionsRefCount.get(value) === 0) {
|
||||||
|
this.suggestionsList.splice(this.suggestionsList.indexOf(value), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.suggestionsRefs.delete(file.path);
|
||||||
|
} else {
|
||||||
|
console.debug("Unknown update type:", type, file, oldPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extractCompositeValuesFromPage(page: DataObject): string[] {
|
||||||
|
const fields = Object.keys(page)
|
||||||
|
.filter((k) => k !== "file")
|
||||||
|
.map((k) => [k, page[k]]);
|
||||||
|
|
||||||
|
const compositeValues: string[] = [];
|
||||||
|
|
||||||
|
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);
|
||||||
|
compositeValues.push(compositeValue);
|
||||||
|
let prefixValue = key + ":: ";
|
||||||
|
if (compositeValues.indexOf(prefixValue) < 0) {
|
||||||
|
compositeValues.push(prefixValue);
|
||||||
|
}
|
||||||
|
if (!this.filterCompositeValue(compositeValue)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return compositeValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters out composite values from suggestions that match any of the ignored fields.
|
||||||
|
* @returns true if the value should be shown in suggestions, false otherwise
|
||||||
|
*/
|
||||||
|
filterCompositeValue(compositeValue: string): boolean {
|
||||||
|
for (const filterPattern of this.plugin.settings.ignoredFields) {
|
||||||
|
const regex = new RegExp(`^(${filterPattern})::.*`);
|
||||||
|
if (regex.test(compositeValue)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Dataview value to a string
|
||||||
|
* returns the composite value in format "key:: value"
|
||||||
|
*/
|
||||||
|
formatCompositeValue(key: string, value: any): string {
|
||||||
|
const dataviewAPI = getAPI(this.plugin.app);
|
||||||
|
|
||||||
|
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 (dataviewAPI.value.typeOf(value) === "link" && value.type === "file") {
|
||||||
|
// parse wiki-style links
|
||||||
|
if (value.display !== undefined) {
|
||||||
|
stringValue = `[[${value.path.split("/").pop().replace(".md", "")}|${value.display}]]`;
|
||||||
|
} else {
|
||||||
|
stringValue = `[[${value.path.split("/").pop().replace(".md", "")}]]`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stringValue = dataviewAPI.value.toString(value);
|
||||||
|
}
|
||||||
|
return `${key}:: ${stringValue}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterFile(filepath: string): boolean {
|
||||||
|
for (const filterPattern of this.plugin.settings.ignoredFiles) {
|
||||||
|
const regex = new RegExp(filterPattern);
|
||||||
|
if (regex.test(filepath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user