mirror of
https://github.com/dnlbauer/obsidian-dataview-autocompletion.git
synced 2026-09-11 06:35:29 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3f44af074 | ||
|
|
57be38b005 | ||
|
|
e744b48536 | ||
|
|
b79d363c9f | ||
|
|
2d9194265a | ||
|
|
27f2e64884 | ||
|
|
779bd0bd68 | ||
|
|
910d343948 | ||
|
|
5802a6cd5d | ||
|
|
d85c07126f | ||
|
|
020054857b | ||
|
|
f58f736df4 | ||
|
|
1644899116 | ||
|
|
1debbb51f2 | ||
|
|
dd86b55bed | ||
|
|
8240045641 | ||
|
|
2dae1175f8 |
16
README.md
16
README.md
@@ -3,10 +3,24 @@
|
|||||||
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)
|
Obsidian 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.
|
by providing autocomplete suggestions for metadata fields in the editor. This makes it easy to reuse existing fields and reduces typing errors.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Provides autocomplete suggestions for inline metadata fields used by Dataview.
|
||||||
|
- Customizable ignored fields and files for suggestions.
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
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).
|
||||||
|
3. Copy the files from step 1 into the new folder.
|
||||||
|
4. Enable the plugin in the Obsidian settings under the "Community plugins" section. You might have to restart Obsidian to rsee the plugin.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the **MIT** License. See [LICENSE](./LICENSE)
|
This project is licensed under the **MIT** License. See [LICENSE](./LICENSE)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "dataview-autocompletion",
|
"id": "dataview-autocompletion",
|
||||||
"name": "Dataview Autocompletion",
|
"name": "Dataview Autocompletion",
|
||||||
"version": "0.8.0",
|
"version": "0.9.0",
|
||||||
"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,13 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-dataview-autocompletion",
|
"name": "obsidian-dataview-autocompletion",
|
||||||
"version": "0.8",
|
"version": "0.9.0",
|
||||||
"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": {
|
||||||
"dev": "npx rollup --config rollup.config.js -w",
|
"dev": "npx rollup --config rollup.config.js -w",
|
||||||
"build": "npx rollup --config rollup.config.js --environment BUILD:production",
|
"build": "npx rollup --config rollup.config.js --environment BUILD:production",
|
||||||
"check-format": "npx prettier --check src",
|
"check-format": "npx prettier --check src",
|
||||||
|
"fix-format": "npx prettier --write src",
|
||||||
"test": "npx jest",
|
"test": "npx jest",
|
||||||
|
"bdd": "npx jest -i --watch --no-cache",
|
||||||
"version": "node ersion-bump.mjs && git add manifest.json versions.json"
|
"version": "node ersion-bump.mjs && git add manifest.json versions.json"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ import {
|
|||||||
EditorSuggest,
|
EditorSuggest,
|
||||||
EditorSuggestContext,
|
EditorSuggestContext,
|
||||||
EditorSuggestTriggerInfo,
|
EditorSuggestTriggerInfo,
|
||||||
|
htmlToMarkdown,
|
||||||
|
MarkdownRenderer,
|
||||||
|
MarkdownView,
|
||||||
TFile,
|
TFile,
|
||||||
} 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 { getAPI, DataviewApi } from "obsidian-dataview";
|
||||||
import DataviewAutocompletePlugin from "./main";
|
import DataviewAutocompletePlugin from "./main";
|
||||||
|
import { parentPort } from "worker_threads";
|
||||||
|
|
||||||
export class DataviewSuggester extends EditorSuggest<String> {
|
export class DataviewSuggester extends EditorSuggest<String> {
|
||||||
plugin: DataviewAutocompletePlugin;
|
plugin: DataviewAutocompletePlugin;
|
||||||
@@ -68,9 +72,40 @@ export class DataviewSuggester extends EditorSuggest<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderSuggestion(value: string, el: HTMLElement): void {
|
renderSuggestion(value: string, el: HTMLElement): void {
|
||||||
// replace marks with bold
|
// Split the value into parts based on the <mark> tags and their content
|
||||||
const formattedHtml = value.replace(/<mark>(.*?)<\/mark>/g, '<span style="font-weight: bold;">$1</span>');
|
const parts = value.split(/(<mark>.*?<\/mark>)/g);
|
||||||
el.innerHTML = formattedHtml;
|
|
||||||
|
const container = el.createDiv("dataview-suggestion-content");
|
||||||
|
const titleDiv = container.createDiv("dataview-suggestion-title");
|
||||||
|
|
||||||
|
parts.forEach((textPart) => {
|
||||||
|
if (textPart.startsWith("<mark>") && textPart.endsWith("</mark>")) {
|
||||||
|
const text = textPart.slice(6, -7); // Remove <mark> and </mark>
|
||||||
|
titleDiv.createEl("span", { text, cls: "dataview-suggestion-highlight" });
|
||||||
|
} else {
|
||||||
|
titleDiv.appendText(textPart);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (value.indexOf("[[") !== -1 || value.indexOf("]]") !== -1 || value.indexOf("](") !== -1) {
|
||||||
|
const noteDiv = container.createDiv("dataview-suggestion-note");
|
||||||
|
let suggestionText = "";
|
||||||
|
parts.forEach((textPart) => {
|
||||||
|
if (textPart.startsWith("<mark>") && textPart.endsWith("</mark>")) {
|
||||||
|
const text = textPart.slice(6, -7); // Remove <mark> and </mark>
|
||||||
|
suggestionText += text;
|
||||||
|
} else {
|
||||||
|
suggestionText += textPart;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
MarkdownRenderer.render(
|
||||||
|
this.app,
|
||||||
|
htmlToMarkdown(suggestionText),
|
||||||
|
noteDiv,
|
||||||
|
this.context!.file.path,
|
||||||
|
this.app.workspace.getActiveViewOfType(MarkdownView)!,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectSuggestion(value: string, evt: MouseEvent | KeyboardEvent): void {
|
selectSuggestion(value: string, evt: MouseEvent | KeyboardEvent): void {
|
||||||
@@ -112,14 +147,16 @@ export class DataviewSuggester extends EditorSuggest<String> {
|
|||||||
// If the value is a string, number or boolean, we can simply convert it to a 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") {
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
||||||
stringValue = value.toString();
|
stringValue = value.toString();
|
||||||
} else if (dataviewAPI.value.typeOf(value) === "link" && value.type === "file" && value.display !== undefined) {
|
} else if (dataviewAPI.value.typeOf(value) === "link" && value.type === "file") {
|
||||||
// value.toString always adds a display value to wiki-style links.
|
// parse wiki-style links
|
||||||
// parse wiki-style links without display value manually here to prevent this from happening for [[filename]]
|
if (value.display !== undefined) {
|
||||||
stringValue = `[[${value.path.split("/").pop().replace(".md", "")}]]`;
|
stringValue = `[[${value.path.split("/").pop().replace(".md", "")}|${value.display}]]`;
|
||||||
|
} else {
|
||||||
|
stringValue = `[[${value.path.split("/").pop().replace(".md", "")}]]`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
stringValue = dataviewAPI.value.toString(value);
|
stringValue = dataviewAPI.value.toString(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${key}:: ${stringValue}`;
|
return `${key}:: ${stringValue}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +196,8 @@ export class DataviewSuggester extends EditorSuggest<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const page = dataviewApi.page(file.path);
|
const page = dataviewApi.page(file.path);
|
||||||
|
if (page === undefined) continue; // not a markdown file
|
||||||
|
|
||||||
const fields = Object.keys(page)
|
const fields = Object.keys(page)
|
||||||
.filter((k) => k !== "file")
|
.filter((k) => k !== "file")
|
||||||
.map((k) => [k, page[k]]);
|
.map((k) => [k, page[k]]);
|
||||||
@@ -218,6 +257,8 @@ export class DataviewSuggester extends EditorSuggest<String> {
|
|||||||
const updateCompositeValues = [];
|
const updateCompositeValues = [];
|
||||||
|
|
||||||
const page = getAPI(this.app).page(file.path);
|
const page = getAPI(this.app).page(file.path);
|
||||||
|
if (page === undefined) return; // not a markdown file
|
||||||
|
|
||||||
const fields = Object.keys(page)
|
const fields = Object.keys(page)
|
||||||
.filter((k) => k !== "file")
|
.filter((k) => k !== "file")
|
||||||
.map((k) => [k, page[k]]);
|
.map((k) => [k, page[k]]);
|
||||||
@@ -297,13 +338,12 @@ export class DataviewSuggester extends EditorSuggest<String> {
|
|||||||
for (const value of this.suggestionsRefs.get(file.path)!) {
|
for (const value of this.suggestionsRefs.get(file.path)!) {
|
||||||
this.suggestionsRefCount.set(value, this.suggestionsRefCount.get(value)! - 1);
|
this.suggestionsRefCount.set(value, this.suggestionsRefCount.get(value)! - 1);
|
||||||
if (this.suggestionsRefCount.get(value) === 0) {
|
if (this.suggestionsRefCount.get(value) === 0) {
|
||||||
console.debug("deleting value from suggestion index", value);
|
|
||||||
this.suggestionsList.splice(this.suggestionsList.indexOf(value), 1);
|
this.suggestionsList.splice(this.suggestionsList.indexOf(value), 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.suggestionsRefs.delete(file.path);
|
this.suggestionsRefs.delete(file.path);
|
||||||
} else {
|
} else {
|
||||||
console.warn("Unknown update type:", type, file, oldPath);
|
console.debug("Unknown update type:", type, file, oldPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/main.ts
29
src/main.ts
@@ -1,6 +1,6 @@
|
|||||||
import { App, Plugin, PluginSettingTab, Setting, TFile } from "obsidian";
|
import { App, Plugin, PluginSettingTab, Setting, TFile } from "obsidian";
|
||||||
import { DataviewSuggester } from "./DataviewSuggester";
|
import { DataviewSuggester } from "./DataviewSuggester";
|
||||||
import { getAPI } from "obsidian-dataview";
|
import { getAPI, isPluginEnabled } from "obsidian-dataview";
|
||||||
import { SettingsTab } from "./SettingsTab";
|
import { SettingsTab } from "./SettingsTab";
|
||||||
|
|
||||||
interface DataviewAutocompleteSettings {
|
interface DataviewAutocompleteSettings {
|
||||||
@@ -15,18 +15,19 @@ const DEFAULT_SETTINGS: DataviewAutocompleteSettings = {
|
|||||||
|
|
||||||
export default class DataviewAutocompletePlugin extends Plugin {
|
export default class DataviewAutocompletePlugin extends Plugin {
|
||||||
settings: DataviewAutocompleteSettings;
|
settings: DataviewAutocompleteSettings;
|
||||||
suggester: DataviewSuggester | undefined;
|
suggester: DataviewSuggester;
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
this.addSettingTab(new SettingsTab(this.app, this));
|
this.addSettingTab(new SettingsTab(this.app, this));
|
||||||
|
|
||||||
|
this.suggester = new DataviewSuggester(this, 10, true, true);
|
||||||
|
this.registerEditorSuggest(this.suggester);
|
||||||
|
|
||||||
this.registerEvent(
|
this.registerEvent(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.app.metadataCache.on("dataview:index-ready", () => {
|
this.app.metadataCache.on("dataview:index-ready", () => {
|
||||||
if (this.suggester !== undefined) {
|
this.suggester.onDataviewIndexReady();
|
||||||
this.suggester.onDataviewIndexReady();
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -35,24 +36,14 @@ export default class DataviewAutocompletePlugin extends Plugin {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
"dataview:metadata-change",
|
"dataview:metadata-change",
|
||||||
(type: string, file: TFile, oldPath?: string) => {
|
(type: string, file: TFile, oldPath?: string) => {
|
||||||
if (this.suggester !== undefined) {
|
this.suggester.onDataviewMetadataChange(type, file, oldPath);
|
||||||
this.suggester.onDataviewMetadataChange(type, file, oldPath);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.app.workspace.onLayoutReady(() => {
|
if (isPluginEnabled(this.app) && getAPI(this.app)?.index.initialized) {
|
||||||
// @ts-ignore
|
this.suggester.onDataviewIndexReady();
|
||||||
if (!Object.keys(this.app.plugins.plugins).includes("dataview")) {
|
}
|
||||||
console.warn("Dataview plugin not installed. Dataview Autocompletion plugin will not work.");
|
|
||||||
} else {
|
|
||||||
// register suggester, requires dataview to be loaded first.
|
|
||||||
console.log("Registering dataview autocompletion suggester");
|
|
||||||
this.suggester = new DataviewSuggester(this, 10, true, true);
|
|
||||||
this.registerEditorSuggest(this.suggester);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {}
|
onunload() {}
|
||||||
|
|||||||
@@ -3,29 +3,28 @@
|
|||||||
* and captures the enclosed text
|
* and captures the enclosed text
|
||||||
* If the brackets start a markdown link, they are ignored.
|
* If the brackets start a markdown link, they are ignored.
|
||||||
* Wiki links don't need to be ignored since Obsidian overwrites suggestions for them.
|
* Wiki links don't need to be ignored since Obsidian overwrites suggestions for them.
|
||||||
|
* We are not allowd to use lookbehinds, because iOS does not support them in older versions.
|
||||||
*/
|
*/
|
||||||
const filledRegex = new RegExp(
|
const filledRegex = new RegExp(
|
||||||
"(" +
|
[
|
||||||
[
|
// pattern for parantheses
|
||||||
// pattern for parantheses
|
// look for opening parantheses; no closing or opening square bracket before to exlude markdown links, etc.
|
||||||
// pos. lookbehind for opening parantheses; no closing square bracket before to exlude markdown links
|
/(?:^\(|[^\[\]]\()/,
|
||||||
/(?<=^\(|[^\]]\()/,
|
/(.+?)/,
|
||||||
/.+?/,
|
// pos. lookahead for closing paranthesis; not followed by another one for nested ((test))
|
||||||
// pos. lookahead for closing paranthesis; not followed by another one for nested ((test))
|
/(?=\)$|\)[^\)])/,
|
||||||
/(?=\)$|\)[^\)])/,
|
|
||||||
|
|
||||||
/|/,
|
/|/,
|
||||||
|
|
||||||
// pattern for square brackets
|
// pattern for square brackets
|
||||||
// pos. lookbehind for opening square bracket
|
// look for opening square brackets
|
||||||
/(?<=\[)/,
|
/(?:\[)/,
|
||||||
/.+?/,
|
/(.+?)/,
|
||||||
// pos. lookahead for closing bracket; not followed by another one ([[test]]) or an opening paranthese (markdown link!)
|
// pos. lookahead for closing bracket; not followed by another one ([[test]]) or an opening paranthese (markdown link!)
|
||||||
/(?=\]$|\][^\]\(])/,
|
/(?=\]$|\][^\]\(])/,
|
||||||
]
|
]
|
||||||
.map((s) => s.source)
|
.map((s) => s.source)
|
||||||
.join("") +
|
.join(""),
|
||||||
")",
|
|
||||||
"g",
|
"g",
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -57,12 +56,13 @@ function getTriggerTextFromRegex(line: string, cursorPos: number, regex: RegExp)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchStart = match!.index;
|
const matchText = match[1] || match[2];
|
||||||
const matchEnd = matchStart + match[1].length;
|
const matchStart = match!.index + match[0].indexOf(matchText);
|
||||||
|
const matchEnd = matchStart + matchText.length;
|
||||||
const cursorInMatch = cursorPos >= matchStart && cursorPos <= matchEnd;
|
const cursorInMatch = cursorPos >= matchStart && cursorPos <= matchEnd;
|
||||||
|
|
||||||
if (cursorInMatch) {
|
if (cursorInMatch) {
|
||||||
return [match[1], matchStart, matchEnd];
|
return [matchText, matchStart, matchEnd];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
30
styles.css
30
styles.css
@@ -1,8 +1,28 @@
|
|||||||
/*
|
div.dataview-suggestion-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-inline-end: auto;
|
||||||
|
}
|
||||||
|
|
||||||
This CSS file will be included with your plugin, and
|
div.dataview-suggestion-title {
|
||||||
available in the app when your plugin is enabled.
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
If your plugin does not need CSS, delete this file.
|
div.dataview-suggestion-note p {
|
||||||
|
margin-block: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.dataview-suggestion-note{
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
width: 100%;
|
||||||
|
flex-basis: 100%;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.dataview-suggestion-highlight {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
*/
|
|
||||||
|
|||||||
3
test-vault/.obsidian/community-plugins.json
vendored
3
test-vault/.obsidian/community-plugins.json
vendored
@@ -1,5 +1,6 @@
|
|||||||
[
|
[
|
||||||
"sample-plugin",
|
"sample-plugin",
|
||||||
"dataview",
|
"dataview",
|
||||||
"obsidian-dataview-autocompletion"
|
"obsidian-dataview-autocompletion",
|
||||||
|
"dataview-autocompletion"
|
||||||
]
|
]
|
||||||
1
test-vault/not-a-markdown-file.json
Normal file
1
test-vault/not-a-markdown-file.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"test": "this is a test file"}
|
||||||
Reference in New Issue
Block a user