Repository: blacksmithgu/obsidian-dataview
Pull request #2179 (2023-12-02 22:42:02)

Update to latest version of hot reload master plugin
This commit updates `test-vault/.obsidian/plugins/hot-reload-master/main.js` to the latest version of the hot-reload-master plugin.


Patch:
diff --git a/package-lock.json b/package-lock.json
index d848ada..5751524 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "obsidian-dataview",
-  "version": "0.5.61",
+  "version": "0.5.64",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "obsidian-dataview",
-      "version": "0.5.61",
+      "version": "0.5.64",
       "license": "MIT",
       "dependencies": {
         "@codemirror/language": "https://github.com/lishid/cm-language",
diff --git a/src/query/parse.ts b/src/query/parse.ts
index c0f185e..c2ba563 100644
--- a/src/query/parse.ts
+++ b/src/query/parse.ts
@@ -13,6 +13,7 @@ import {
     QueryType,
     SortByStep,
     WhereStep,
+    Comment,
 } from "./query";
 import { Source, Sources } from "data-index/source";
 import { DEFAULT_QUERY_SETTINGS } from "settings";
@@ -25,6 +26,7 @@ import { Result } from "api/result";
 /** Typings for the outputs of all of the parser combinators. */
 interface QueryLanguageTypes {
     queryType: QueryType;
+    comment: Comment;
 
     explicitNamedField: NamedField;
     namedField: NamedField;
@@ -82,6 +84,16 @@ export const QUERY_LANGUAGE = P.createLanguage<QueryLanguageTypes>({
             EXPRESSION.identifier.or(EXPRESSION.string),
             (field, _as, ident) => QueryFields.named(ident, field)
         ),
+    comment: () =>
+        P.Parser((input, i) => {
+            // Parse a comment, which is a line starting with //.
+            let line = input.substring(i);
+            if (!line.startsWith("//")) return P.makeFailure(i, "Not a comment");
+            // The comment ends at the end of the line.
+            line = line.split("\n")[0];
+            let comment = line.substring(2).trim();
+            return P.makeSuccess(i + line.length, comment);
+        }),
     namedField: q =>
         P.alt<NamedField>(
             q.explicitNamedField,
@@ -184,9 +196,9 @@ export const QUERY_LANGUAGE = P.createLanguage<QueryLanguageTypes>({
     clause: q => P.alt(q.fromClause, q.whereClause, q.sortByClause, q.limitClause, q.groupByClause, q.flattenClause),
     query: q =>
         P.seqMap(
-            q.headerClause.trim(P.optWhitespace),
-            q.fromClause.trim(P.optWhitespace).atMost(1),
-            q.clause.trim(P.optWhitespace).many(),
+            q.headerClause.trim(optionalWhitespaceOrComment),
+            q.fromClause.trim(optionalWhitespaceOrComment).atMost(1),
+            q.clause.trim(optionalWhitespaceOrComment).many(),
             (header, from, clauses) => {
                 return {
                     header,
@@ -198,6 +210,14 @@ export const QUERY_LANGUAGE = P.createLanguage<QueryLanguageTypes>({
         ),
 });
 
+/**
+ * A parser for optional whitespace or comments. This is used to exclude whitespace and comments from other parsers.
+ */
+const optionalWhitespaceOrComment: P.Parser<string> = P.alt(P.whitespace, QUERY_LANGUAGE.comment)
+    .many() // Use many() since there may be zero whitespaces or comments.
+    // Transform the many to a single result.
+    .map(arr => arr.join(""));
+
 /**
  * Attempt to parse a query from the given query text, returning a string error
  * if the parse failed.
diff --git a/src/query/query.ts b/src/query/query.ts
index b548e71..6925f53 100644
--- a/src/query/query.ts
+++ b/src/query/query.ts
@@ -5,6 +5,9 @@ import { Field } from "expression/field";
 /** The supported query types (corresponding to view types). */
 export type QueryType = "list" | "table" | "task" | "calendar";
 
+/** A single-line comment. */
+export type Comment = string;
+
 /** Fields used in the query portion. */
 export interface NamedField {
     /** The effective name of this field. */


Test patch:
diff --git a/src/test/parse/parse.query.test.ts b/src/test/parse/parse.query.test.ts
index d78b4c2..9b11d5f 100644
--- a/src/test/parse/parse.query.test.ts
+++ b/src/test/parse/parse.query.test.ts
@@ -42,6 +42,39 @@ test("Link Source", () => {
     });
 });
 
+// <-- Comments -->
+describe("Comments", () => {
+    test("Valid Comments", () => {
+        let commentWithSpace = QUERY_LANGUAGE.comment.tryParse("// This is a comment");
+        expect(commentWithSpace).toEqual("This is a comment");
+
+        let commentNoSpace = QUERY_LANGUAGE.comment.tryParse("//This is a comment");
+        expect(commentNoSpace).toEqual("This is a comment");
+
+        let empty = QUERY_LANGUAGE.comment.tryParse("//");
+        expect(empty).toEqual("");
+
+        let emptyWithSpace = QUERY_LANGUAGE.comment.tryParse("// ");
+        expect(emptyWithSpace).toEqual("");
+    });
+
+    test("Invalid comment in quotes", () => {
+        let commentSolo = QUERY_LANGUAGE.comment.parse('"// This is not a comment"');
+        if (!commentSolo.status) {
+            expect(commentSolo.expected).toEqual(["Not a comment"]);
+        } else {
+            fail("Solo comment should not have parsed");
+        }
+
+        let commentMiddle = QUERY_LANGUAGE.comment.parse('"The is before the comment // This is not a comment"');
+        if (!commentMiddle.status) {
+            expect(commentMiddle.expected).toEqual(["Not a comment"]);
+        } else {
+            fail("Comment in the middle of a string should not have parsed");
+        }
+    });
+});
+
 // <-- Fields -->
 
 describe("Named Fields", () => {
@@ -129,6 +162,27 @@ describe("Table Queries", () => {
         expect((fat.operations[1] as SortByStep).fields.length).toBe(2);
     });
 
+    test("Commented Query", () => {
+        let commented = parseQuery(
+            "// This is a comment at the beginning\n" +
+                "TABLE (time-played + 100) as long, rating as rate, length\n" +
+                "// This is a comment\n" +
+                "// This is a second comment\n" +
+                "FROM #games or #gaming  // This is an inline comment\n" +
+                "// This is a third comment\n" +
+                "WHERE long > 150 and rate - 10 < 40\n" +
+                "// This is a fourth comment\n" +
+                "\n" +
+                "   // This is a comment with whitespace prior\n" +
+                "SORT length + 8 + 4 DESCENDING, long ASC\n" +
+                "// This is a comment at the end"
+        ).orElseThrow();
+        expect(commented.header.type).toBe("table");
+        expect((commented.header as TableQuery).fields.length).toBe(3);
+        expect(commented.source).toEqual(Sources.binaryOp(Sources.tag("#games"), "|", Sources.tag("#gaming")));
+        expect((commented.operations[1] as SortByStep).fields.length).toBe(2);
+    });
+
     test("WITHOUT ID", () => {
         let q = parseQuery("TABLE WITHOUT ID name, value").orElseThrow();
         expect(typeof q).toBe("object");
diff --git a/test-vault/.obsidian/plugins/hot-reload-master/main.js b/test-vault/.obsidian/plugins/hot-reload-master/main.js
index 53e4fc7..c7e7953 100644
--- a/test-vault/.obsidian/plugins/hot-reload-master/main.js
+++ b/test-vault/.obsidian/plugins/hot-reload-master/main.js
@@ -1,19 +1,34 @@
-const {Plugin, Notice} = require("obsidian");
+const {Plugin, Notice, debounce} = require("obsidian");
 const fs = require("fs");
 
 const watchNeeded = window.process.platform !== "darwin" && window.process.platform !== "win32";
 
 module.exports = class HotReload extends Plugin {
 
-    onload() { this.app.workspace.onLayoutReady( this._onload.bind(this) ); }
+    statCache = new Map();  // path -> Stat
+    queue = Promise.resolve();
 
-    async _onload() {
-        this.pluginReloaders = {};
-        this.inProgress = null;
-        await this.getPluginNames();
-        this.reindexPlugins = this.debouncedMethod(500, this.getPluginNames);
-        this.registerEvent( this.app.vault.on("raw", this.onFileChange.bind(this)) );
-        this.watch(".obsidian/plugins");
+    run(val, err) {
+        return this.queue = this.queue.then(val, err);
+    }
+
+    reindexPlugins = debounce(() => this.run(() => this.getPluginNames()), 500, true);
+    requestScan    = debounce(() => this.run(() => this.checkVersions()),  250, true);
+
+    onload() {
+        app.workspace.onLayoutReady(async ()=> {
+            this.pluginReloaders = {};
+            this.inProgress = null;
+            await this.getPluginNames();
+            this.registerEvent( this.app.vault.on("raw", this.requestScan));
+            this.watch(".obsidian/plugins");
+            this.requestScan();
+            this.addCommand({
+                id: "scan-for-changes",
+                name: "Check plugins for changes and reload them",
+                callback: () => this.requestScan()
+            })
+        });
     }
 
     watch(path) {
@@ -25,6 +40,22 @@ module.exports = class HotReload extends Plugin {
         }
     }
 
+    async checkVersions() {
+        const base = this.app.plugins.getPluginFolder();
+        for (const dir of Object.keys(this.pluginNames)) {
+            for (const file of ["manifest.json", "main.js", "styles.css", ".hotreload"]) {
+                const path = `${base}/${dir}/${file}`;
+                const stat = await app.vault.adapter.stat(path);
+                if (stat) {
+                    if (this.statCache.has(path) && stat.mtime !== this.statCache.get(path).mtime) {
+                        this.onFileChange(path);
+                    }
+                    this.statCache.set(path, stat);
+                }
+            }
+        }
+    }
+
     async getPluginNames() {
         const plugins = {}, enabled = new Set();
         for (const {id, dir} of Object.values(app.plugins.manifests)) {
@@ -50,34 +81,30 @@ module.exports = class HotReload extends Plugin {
         if (base !== "main.js" && base !== "styles.css") return;
         if (!this.enabledPlugins.has(plugin)) return;
         const reloader = this.pluginReloaders[plugin] || (
-            this.pluginReloaders[plugin] = this.debouncedMethod(750, this.requestReload, plugin)
+            this.pluginReloaders[plugin] = debounce(() => this.run(() => this.reload(plugin), console.error), 750, true)
         );
         reloader();
     }
 
-    requestReload(plugin) {
-        const next = this.inProgress = this.reload(plugin, this.inProgress);
-        next.finally(() => {if (this.inProgress === next) this.inProgress = null;})
-    }
+    async reload(plugin) {
+        const plugins = app.plugins;
+
+        // Don't reload disabled plugins
+        if (!plugins.enabledPlugins.has(plugin)) return;
+
+        await plugins.disablePlugin(plugin);
+        console.debug("disabled", plugin);
 
-    async reload(plugin, previous) {
-        const plugins = this.app.plugins;
+        // Ensure sourcemaps are loaded (Obsidian 14+)
+        const oldDebug = localStorage.getItem("debug-plugin");
+        localStorage.setItem("debug-plugin", "1");
         try {
-            // Wait for any other queued/in-progress reloads to finish
-            await previous;
-            await plugins.disablePlugin(plugin);
-            console.debug("disabled", plugin);
             await plugins.enablePlugin(plugin);
-            console.debug("enabled", plugin);
-            new Notice(`Plugin "${plugin}" has been reloaded`);
-        } catch(e) {}
-    }
-
-    debouncedMethod(ms, func, ...args) {
-        var timeout;
-        return () => {
-            if (timeout) clearTimeout(timeout);
-            timeout = setTimeout( () => { timeout = null; func.apply(this, args); }, ms);
+        } finally {
+            // Restore previous setting
+            if (oldDebug === null) localStorage.removeItem("debug-plugin"); else localStorage.setItem("debug-plugin", oldDebug);
         }
+        console.debug("enabled", plugin);
+        new Notice(`Plugin "${plugin}" has been reloaded`);
     }
-}
\ No newline at end of file
+}