Repository: blacksmithgu/obsidian-dataview (ts)

Issue:
DQL Comments
I've looked around in both the documentation and the GitHub issues (though I may have missed something), but I can't find a way to add comments to my queries.

Would it be possible to add comments to the DQL language that should simply be ignored.

For example:
```
TABLE
	price as "Price", // In GBP

	// Volume is in liters
	volume as "Volume"
	
FROM …
WHERE …
SORT ...
```

Pull request description:
Add comments to query language
This PR adds comments to DQL. Comments start with `//` and can be on their own line or after another statement.

Resolves #1624 
[[1]](https://github.com/blacksmithgu/obsidian-dataview/issues/1624#issuecomment-1794031757) and [[2]](https://github.com/blacksmithgu/obsidian-dataview/issues/1624#issuecomment-1794031826) are implemented. [[3]](https://github.com/blacksmithgu/obsidian-dataview/issues/1624#issuecomment-1794031826) can be performed by creating multiple single-line comments.

Example:
```
// This is a comment at the beginning
TABLE (time-played + 100) as long, rating as rate, length
// This is a comment
// This is a second comment
FROM #games or #gaming  // This is an inline comment
// This is a third comment
WHERE long > 150 and rate - 10 < 40
// This is a fourth comment

   // This is a comment with whitespace prior
SORT length + 8 + 4 DESCENDING, long ASC
// This is a comment at the end
```

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");