Testing out autocomplete feature for obj.

properties.

Looking to add more options and expand autocomplete
This commit is contained in:
Jomar 2026-04-13 18:17:19 -07:00
parent e89a78437b
commit e6825554a7
9 changed files with 328 additions and 190 deletions

View file

@ -4,13 +4,14 @@ import {create , all} from "mathjs";
import type {ChartOptions, ChartConfiguration} from "chart.js/auto";
import { Notice, App, TFile} from "obsidian";
import { getApp } from "appContext";
import { getApp } from "./appContext";
import { customNotice,isTuple } from "main";
import { validLineProperties } from "./graphs";
import { min } from "mathjs";
import { string } from "mathjs";
import { isArray } from "chart.js/dist/helpers/helpers.core";
import {validPlotProperties,validRoots} from "./plotProperties"
const math = create(all);
math.import({ // Created an alias so the user can write the more "normal" ln(x) and Mathjs wont hate me.
@ -66,42 +67,8 @@ export type parsedText = {
const builtInConstants = ["e","E","pi","PI"];
export const validPlotProperties = [
"type",
"min",
"max",
"display",
"position",
"text",
"color",
"stepSize",
"beginAtZero",
"suggestedMin",
"suggestedMax",
"lineWidth",
"drawBorder",
"drawOnChartArea",
"drawTicks",
"tickLength",
"mode",
"intersect",
"enabled",
"borderColor",
"backgroundColor",
"borderWidth",
"pointRadius",
"tension",
"fill",
"hidden"
];
const validRoots = [
"plugins",
"scales",
"elements",
"interaction",
"animation"
]

131
helpers/plotProperties.ts Normal file
View file

@ -0,0 +1,131 @@
import { CompletionContext } from "@codemirror/autocomplete";
import { PlotPluginSettings } from "settings";
import { getDefaultPlotProperties } from "main";
import LinearScaleBase from "chart.js/dist/scales/scale.linearbase";
export const myCompletions = {
"obj.": [
{label: "plugins", type: "property", info: "Chart plugins"},
{label: "scales", type: "property", info: "Axis settings"},
{label: "elements",type: "property",info: "Line settings"},
{label: "animations",type: "property",info: "Animation settings"},
{label: "interaction",type: "property",info: "Interaction settings"}
],
}
export const validRoots = [
"plugins",
"scales",
"elements",
"interaction",
"animation"
]
export const validPlotProperties = [
"type",
"min",
"max",
"display",
"position",
"text",
"color",
"stepSize",
"beginAtZero",
"suggestedMin",
"suggestedMax",
"lineWidth",
"drawBorder",
"drawOnChartArea",
"drawTicks",
"tickLength",
"mode",
"intersect",
"enabled",
"borderColor",
"backgroundColor",
"borderWidth",
"pointRadius",
"tension",
"fill",
"hidden"
];
type TreeNode = {
[key: string]: TreeNode;
};
export function linePlotCompletionSource(settings: PlotPluginSettings) {
const chartTree = objectToTree(getDefaultPlotProperties(settings)); // Get default properties from main and create a tree object to travers
return function(context: CompletionContext) { // function that autocompletion accepts
if (!inLineplotBlock(context)) return null; // if the text doesnt have ```lineplot at some point before, dont autocomplete. Couldnt get syntaxTree to work
const match = context.matchBefore(/[a-zA-Z0-9_.()]*/); // Matches text before cursor stop
if (!match) return null;
if (match.text.startsWith("obj.")) {
const pathText = match.text.slice(4); // Remove obj.
const parts = pathText.split("."); // Gets path scales.x
const partial = parts.pop() ?? ""; // Removes what is being typed
let node: any = chartTree; // Traversal starts at root
for (const part of parts) { // Walk through path
if (!part) continue;
if (!(part in node)) return null; // If part not a valid jey in nodes (chartTree) stop suggesting
node = node[part]; // the node becomes the next object inside part so node was scales.x is now x. -> options for x
}
return {
from: context.pos - partial.length, // we only replace the property being written not the whole obj. -> path
options: Object.keys(node).filter(key => key.startsWith(partial)).map( key => ({
label: key,
type: "property"
}))
};
}
return null;
}
}
function inLineplotBlock(context: CompletionContext): boolean {
let line = context.state.doc.lineAt(context.pos);
let foundStart = false;
for (let n = line.number; n >= 1; n--) {
const txt = context.state.doc.line(n).text.trim();
if (txt.startsWith("```")) {
if (txt === "```lineplot") {
foundStart = true;
}
break;
}
}
return foundStart;
}
export function objectToTree(obj: any): TreeNode {
const tree: TreeNode = {};
for (const key in obj) {
const value = obj[key];
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
tree[key] = objectToTree(value);
} else {
tree[key] = {};
}
}
return tree;
}

65
package-lock.json generated
View file

@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "0-BSD",
"dependencies": {
"@codemirror/autocomplete": "^6.20.1",
"chart.js": "^4.5.1",
"chartjs-plugin-zoom": "^2.2.0",
"mathjs": "^15.1.1",
@ -35,12 +36,37 @@
"node": ">=6.9.0"
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.20.1",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz",
"integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.12.3",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz",
"integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
@ -50,7 +76,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
@ -713,12 +738,35 @@
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@lezer/common": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
"license": "MIT"
},
"node_modules/@lezer/highlight": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.3.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz",
"integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@microsoft/eslint-plugin-sdl": {
"version": "1.1.0",
@ -1561,8 +1609,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
@ -4762,8 +4809,7 @@
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/supports-color": {
"version": "7.2.0",
@ -5093,8 +5139,7 @@
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",

View file

@ -24,6 +24,7 @@
"typescript-eslint": "8.35.1"
},
"dependencies": {
"@codemirror/autocomplete": "^6.20.1",
"chart.js": "^4.5.1",
"chartjs-plugin-zoom": "^2.2.0",
"mathjs": "^15.1.1",

View file

@ -1,13 +1,14 @@
import {handleMarkdown, handleGlobalOptions, evaluateExpressions, PlotData, parsedText, handleTableData} from "./parser"
import { createPlot, buildDatasets} from "./graphs";
import {handleMarkdown, handleGlobalOptions, evaluateExpressions, PlotData, parsedText, handleTableData} from "../helpers/parser"
import { createPlot, buildDatasets} from "../helpers/graphs";
import {Notice, Plugin} from "obsidian";
import { apply } from "mathjs";
import { setApp } from "appContext";
import { setApp } from "../helpers/appContext";
import { PlotPluginSettings, DEFAULT_SETTINGS, PlotSettingTab } from "settings";
import { ChartConfiguration } from "chart.js/auto";
import {autocompletion} from "@codemirror/autocomplete";
import {EditorView} from "@codemirror/view";
import { linePlotCompletionSource } from "../helpers/plotProperties";
export default class PlotPlugin extends Plugin {
settings!: PlotPluginSettings;
@ -17,12 +18,19 @@ export default class PlotPlugin extends Plugin {
await this.loadSettings();
this.addSettingTab(new PlotSettingTab(this.app,this));
this.registerEditorExtension([
autocompletion({
override: [linePlotCompletionSource(this.settings)],
activateOnTyping: true,
})]
)
setApp(this.app); // Sets current app as the working app to use globally.
this.registerMarkdownCodeBlockProcessor("lineplot",
async (source: string, el: HTMLElement) => { // Runs whenever a codeblock with the "lineplot" identifier is edited.
const defaultProperties: ChartConfiguration<"line">["options"] = this.getDefaultPlotProperties();
const defaultProperties: ChartConfiguration<"line">["options"] = getDefaultPlotProperties(this.settings);
const sourcePaths = getSourcePaths(source); // Gets any source:: item from path PATHS. Stores them for comparisons later.
if (defaultProperties === undefined) return;
let cachedParsedText = await handleMarkdown(source,defaultProperties); // Evaluates all the markdown in the codeblock and creates a ParsedText type object.
@ -60,6 +68,7 @@ export default class PlotPlugin extends Plugin {
if (!chartInstance) { // If the chart doesnt exist yet. Create it
const wrapper = el.createDiv("plot-wrapper");
wrapper.setCssProps({
"--wrapper-height": `${this.settings.canvasHeight}px`,
"--wrapper-padding": `${this.settings.canvasPadding}px`,
@ -136,126 +145,11 @@ export default class PlotPlugin extends Plugin {
refreshPlots() {
this.app.workspace.updateOptions();
}
getDefaultPlotProperties(): ChartConfiguration<"line">["options"] {
const style = getComputedStyle(document.body);
const fontFamily = style.getPropertyValue("--font-interface").trim() || "sans-serif";
return {
responsive: true,
maintainAspectRatio: false,
animation: false,
interaction: {
mode: "nearest",
intersect: false
},
layout: {
padding: 8
},
plugins: {
legend: {
display: this.settings.legendStatus,
position: "right",
labels: {
boxWidth: 5,
boxHeight: 5,
padding: 14,
usePointStyle: true,
font: {
family: fontFamily,
size: 12
}
}
},
zoom: {
pan: {
enabled: this.settings.zoomStatus ? false : true,
mode: "xy"
},
zoom: {
wheel: {
enabled: this.settings.zoomStatus ? false : true,
},
pinch: {
enabled: this.settings.zoomStatus ? false : true,
},
mode: "xy"
}
},
title: {
display: this.settings.titleStatus,
font: {
family: fontFamily,
size: 13,
weight: 600
}
},
tooltip: {
enabled: true,
padding: 10,
cornerRadius: 8
},
},
elements: {
line: {
borderWidth: this.settings.EborderWidth,
tension: 0.15,
fill: false,
},
point: {
radius: this.settings.pointRadius,
hoverRadius: 4,
hitRadius: 6,
pointStyle: "circle"
}
},
scales: {
x: {
type: this.settings.xScalesType,
display: true,
grid: {
display: true
},
ticks: {
display: true,
font: {
family: fontFamily,
size: 11
}
},
title: {
display: this.settings.titleStatus,
text: ""
}
},
y: {
type: this.settings.yScalesType,
display: true,
grid: {
display: true
},
ticks: {
display: true
},
title: {
display: this.settings.titleStatus,
text: ""
}}}};
}
}
@ -265,25 +159,7 @@ export default class PlotPlugin extends Plugin {
export function isTuple(arr: number[]): arr is [number, number, number] {
return arr.length === 3 && arr.every(n => typeof n === "number");
}
/*
function declareScalesType(data: PlotData[], scale: string) {
let sample = data[0]?.data[0]?.x;
switch (scale) {
case "x":
sample = data[0].data[0].x;
case "y":
sample = data[1]?.data[1]?.y;
}
console.log(sample);
if (typeof sample === "number") return "linear";
if (typeof sample === "string") return "category";
return "linear";
}
*/
function getSourcePaths(src: string) {
const paths: string[] = [];
@ -333,3 +209,121 @@ export function customNotice(msg: string, cls = "", timeout = 4000) {
});
}
export function getDefaultPlotProperties(settings: PlotPluginSettings): ChartConfiguration<"line">["options"] {
const style = getComputedStyle(document.body);
const fontFamily = style.getPropertyValue("--font-interface").trim() || "sans-serif";
return {
responsive: true,
maintainAspectRatio: false,
animation: false,
interaction: {
mode: "nearest",
intersect: false
},
layout: {
padding: 8
},
plugins: {
legend: {
display: settings.legendStatus,
position: "right",
labels: {
boxWidth: 5,
boxHeight: 5,
padding: 14,
usePointStyle: true,
font: {
family: fontFamily,
size: 12
}
}
},
zoom: {
pan: {
enabled: settings.zoomStatus ? false : true,
mode: "xy"
},
zoom: {
wheel: {
enabled: settings.zoomStatus ? false : true,
},
pinch: {
enabled: settings.zoomStatus ? false : true,
},
mode: "xy"
}
},
title: {
display: settings.titleStatus,
font: {
family: fontFamily,
size: 13,
weight: 600
}
},
tooltip: {
enabled: true,
padding: 10,
cornerRadius: 8
},
},
elements: {
line: {
borderWidth: settings.EborderWidth,
tension: 0.15,
fill: false,
},
point: {
radius: settings.pointRadius,
hoverRadius: 4,
hitRadius: 6,
pointStyle: "circle"
}
},
scales: {
x: {
type: settings.xScalesType,
display: true,
grid: {
display: true
},
ticks: {
display: true,
font: {
family: fontFamily,
size: 11
}
},
title: {
display: settings.titleStatus,
text: ""
}
},
y: {
type: settings.yScalesType,
display: true,
grid: {
display: true
},
ticks: {
display: true
},
title: {
display: settings.titleStatus,
text: ""
}}}};
}

View file

@ -27,5 +27,5 @@
},
"include": [
"src/**/*.ts"
]
, "helpers/graphs.ts", "helpers/parser.ts", "helpers/appContext.ts", "helpers/declarations.d.ts" ]
}