15 Commits

Author SHA1 Message Date
Daniel Bauer
325f2384b1 bump version 2025-02-15 11:27:41 +01:00
Daniel Bauer
e3d6be193a feat: add option for prefix suggestion 2025-02-15 11:26:03 +01:00
Daniel Bauer
f5f9d49b0b chore: remove unused timing 2025-02-15 09:53:55 +01:00
Daniel Bauer
6579606935 Update README.md 2025-02-13 09:31:48 +01:00
Daniel Bauer
26227eac6e bump version 2025-01-27 10:04:15 +01:00
Daniel Bauer
ba4dab5dc4 chore: remove logging 2025-01-27 10:03:19 +01:00
daniel
6b008d452a chore: fix formatting 2024-12-27 22:41:14 +01:00
daniel
314f99f462 bump version 2024-12-27 22:39:44 +01:00
daniel
064320bc5b chore: remove logging statements 2024-12-27 22:39:22 +01:00
daniel
5f7fdb244a chore: remove obsidian from name 2024-12-27 22:37:15 +01:00
daniel
a820b4453d version bump 2024-12-24 10:41:31 +01:00
daniel
4d9673eff7 chore: refactor suggestion index list into its own class 2024-12-24 10:41:06 +01:00
daniel
ddedb4e8c1 chore: code cleanup 2024-12-24 10:25:19 +01:00
daniel
41c9e60615 fix: fix addind a deleted value to index not working 2024-12-24 10:15:41 +01:00
daniel
70d0b94a62 refactor: buildIndex and updateIndex share code 2024-12-24 10:06:19 +01:00
6 changed files with 281 additions and 234 deletions

View File

@@ -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).

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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);
}
} }
} }

View File

@@ -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
View 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;
}
}