瀏覽代碼

Spreadsheet: Make it possible to refer to ranges in other sheets

Now the range A0:C4 in a sheet named "foo" can be represented as:

    R`sheet("foo"):A0:C4`

This makes it possible to do cross-sheet lookups and more.
Ali Mohammad Pur 3 年之前
父節點
當前提交
746b8ec8de

+ 58 - 20
Base/res/js/Spreadsheet/runtime.js

@@ -67,7 +67,11 @@ class Position {
             point = current_point.up(1)
             point = current_point.up(1)
         )
         )
             current_point = point;
             current_point = point;
-        return R(current_point.name + ":" + up_one.name);
+
+        const sheetName = Object.is(this.sheet, thisSheet)
+            ? ""
+            : `sheet(${JSON.stringify(this.sheet.name)}):`;
+        return R(sheetName + current_point.name + ":" + up_one.name);
     }
     }
 
 
     range_down() {
     range_down() {
@@ -75,7 +79,11 @@ class Position {
         let current_point = down_one;
         let current_point = down_one;
         for (let point = current_point.down(1); point.value() !== ""; point = current_point.down(1))
         for (let point = current_point.down(1); point.value() !== ""; point = current_point.down(1))
             current_point = point;
             current_point = point;
-        return R(current_point.name + ":" + down_one.name);
+
+        const sheetName = Object.is(this.sheet, thisSheet)
+            ? ""
+            : `sheet(${JSON.stringify(this.sheet.name)}):`;
+        return R(sheetName + current_point.name + ":" + down_one.name);
     }
     }
 
 
     range_left() {
     range_left() {
@@ -88,7 +96,11 @@ class Position {
             point = current_point.left(1)
             point = current_point.left(1)
         )
         )
             current_point = point;
             current_point = point;
-        return R(current_point.name + ":" + left_one.name);
+
+        const sheetName = Object.is(this.sheet, thisSheet)
+            ? ""
+            : `sheet(${JSON.stringify(this.sheet.name)}):`;
+        return R(sheetName + current_point.name + ":" + left_one.name);
     }
     }
 
 
     range_right() {
     range_right() {
@@ -100,7 +112,11 @@ class Position {
             point = current_point.right(1)
             point = current_point.right(1)
         )
         )
             current_point = point;
             current_point = point;
-        return R(current_point.name + ":" + right_one.name);
+
+        const sheetName = Object.is(this.sheet, thisSheet)
+            ? ""
+            : `sheet(${JSON.stringify(this.sheet.name)}):`;
+        return R(sheetName + current_point.name + ":" + right_one.name);
     }
     }
 
 
     with_column(value) {
     with_column(value) {
@@ -124,7 +140,9 @@ class Position {
     }
     }
 
 
     toString() {
     toString() {
-        return `<Cell at ${this.name}>`;
+        return `<Cell at ${this.name}${
+            Object.is(this.sheet, thisSheet) ? "" : ` in sheet(${JSON.stringify(this.sheet.name)})`
+        }>`;
     }
     }
 }
 }
 
 
@@ -276,7 +294,15 @@ class Ranges extends CommonRange {
 }
 }
 
 
 class Range extends CommonRange {
 class Range extends CommonRange {
-    constructor(startingColumnName, endingColumnName, startingRow, endingRow, columnStep, rowStep) {
+    constructor(
+        startingColumnName,
+        endingColumnName,
+        startingRow,
+        endingRow,
+        columnStep,
+        rowStep,
+        sheet
+    ) {
         super();
         super();
         // using == to account for '0' since js will parse `+'0'` to 0
         // using == to account for '0' since js will parse `+'0'` to 0
         if (columnStep == 0 || rowStep == 0)
         if (columnStep == 0 || rowStep == 0)
@@ -292,6 +318,7 @@ class Range extends CommonRange {
         this.columnStep = columnStep ?? 1;
         this.columnStep = columnStep ?? 1;
         this.rowStep = rowStep ?? 1;
         this.rowStep = rowStep ?? 1;
         this.spansEntireColumn = endingRow === undefined;
         this.spansEntireColumn = endingRow === undefined;
+        this.sheet = sheet;
         if (!this.spansEntireColumn && startingRow === undefined)
         if (!this.spansEntireColumn && startingRow === undefined)
             throw new Error("A Range with a defined end row must also have a defined start row");
             throw new Error("A Range with a defined end row must also have a defined start row");
 
 
@@ -299,32 +326,32 @@ class Range extends CommonRange {
     }
     }
 
 
     first() {
     first() {
-        return new Position(this.startingColumnName, this.startingRow);
+        return new Position(this.startingColumnName, this.startingRow, this.sheet);
     }
     }
 
 
     forEach(callback) {
     forEach(callback) {
         const ranges = [];
         const ranges = [];
-        let startingColumnIndex = thisSheet.column_index(this.startingColumnName);
-        let endingColumnIndex = thisSheet.column_index(this.endingColumnName);
+        let startingColumnIndex = this.sheet.column_index(this.startingColumnName);
+        let endingColumnIndex = this.sheet.column_index(this.endingColumnName);
         let columnDistance = endingColumnIndex - startingColumnIndex;
         let columnDistance = endingColumnIndex - startingColumnIndex;
         for (
         for (
             let columnOffset = 0;
             let columnOffset = 0;
             columnOffset <= columnDistance;
             columnOffset <= columnDistance;
             columnOffset += this.columnStep
             columnOffset += this.columnStep
         ) {
         ) {
-            const columnName = thisSheet.column_arithmetic(this.startingColumnName, columnOffset);
+            const columnName = this.sheet.column_arithmetic(this.startingColumnName, columnOffset);
             ranges.push({
             ranges.push({
                 column: columnName,
                 column: columnName,
                 rowStart: this.startingRow,
                 rowStart: this.startingRow,
                 rowEnd: this.spansEntireColumn
                 rowEnd: this.spansEntireColumn
-                    ? thisSheet.get_column_bound(columnName)
+                    ? this.sheet.get_column_bound(columnName)
                     : this.endingRow,
                     : this.endingRow,
             });
             });
         }
         }
 
 
         outer: for (const range of ranges) {
         outer: for (const range of ranges) {
             for (let row = range.rowStart; row <= range.rowEnd; row += this.rowStep) {
             for (let row = range.rowStart; row <= range.rowEnd; row += this.rowStep) {
-                if (callback(new Position(range.column, row)) === Break) break outer;
+                if (callback(new Position(range.column, row, this.sheet)) === Break) break outer;
             }
             }
         }
         }
     }
     }
@@ -338,8 +365,8 @@ class Range extends CommonRange {
     }
     }
 
 
     normalize() {
     normalize() {
-        const startColumnIndex = thisSheet.column_index(this.startingColumnName);
-        const endColumnIndex = thisSheet.column_index(this.endingColumnName);
+        const startColumnIndex = this.sheet.column_index(this.startingColumnName);
+        const endColumnIndex = this.sheet.column_index(this.endingColumnName);
         if (startColumnIndex > endColumnIndex) {
         if (startColumnIndex > endColumnIndex) {
             const temp = this.startingColumnName;
             const temp = this.startingColumnName;
             this.startingColumnName = this.endingColumnName;
             this.startingColumnName = this.endingColumnName;
@@ -359,11 +386,15 @@ class Range extends CommonRange {
         const endingRow = this.endingRow ?? "";
         const endingRow = this.endingRow ?? "";
         const showSteps = this.rowStep !== 1 || this.columnStep !== 1;
         const showSteps = this.rowStep !== 1 || this.columnStep !== 1;
         const steps = showSteps ? `:${this.columnStep}:${this.rowStep}` : "";
         const steps = showSteps ? `:${this.columnStep}:${this.rowStep}` : "";
-        return `R\`${this.startingColumnName}${this.startingRow}:${this.endingColumnName}${endingRow}${steps}\``;
+        const sheetName = Object.is(thisSheet, this.sheet)
+            ? ""
+            : `sheet(${JSON.stringify(this.sheet.name)}):`;
+        return `R\`${sheetName}${this.startingColumnName}${this.startingRow}:${this.endingColumnName}${endingRow}${steps}\``;
     }
     }
 }
 }
 
 
-const R_FORMAT = /^([a-zA-Z_]+)(?:(\d+):([a-zA-Z_]+)(\d+)?(?::(\d+):(\d+))?)?$/;
+const R_FORMAT =
+    /^(?:sheet\(("(?:[^"]|\\")*")\):)?([a-zA-Z_]+)(?:(\d+):([a-zA-Z_]+)(\d+)?(?::(\d+):(\d+))?)?$/;
 function R(fmt, ...args) {
 function R(fmt, ...args) {
     if (args.length !== 0) throw new TypeError("R`` format must be a literal");
     if (args.length !== 0) throw new TypeError("R`` format must be a literal");
     // done because:
     // done because:
@@ -372,11 +403,17 @@ function R(fmt, ...args) {
     // myFunc`ABC` => "["ABC"]"
     // myFunc`ABC` => "["ABC"]"
     if (Array.isArray(fmt)) fmt = fmt[0];
     if (Array.isArray(fmt)) fmt = fmt[0];
     if (!R_FORMAT.test(fmt))
     if (!R_FORMAT.test(fmt))
-        throw new Error("Invalid Format. Expected Format: R`A` or R`A0:A1` or R`A0:A2:1:2`");
-    // Format: Col(Row:Col(Row)?(:ColStep:RowStep)?)?
+        throw new Error(
+            'Invalid Format. Expected Format: R`A` or R`A0:A1` or R`A0:A2:1:2` or R`sheet("sheetName"):...`'
+        );
+    // Format: (sheet("sheetName"):)?Col(Row:Col(Row)?(:ColStep:RowStep)?)?
     // Ignore the first element of the match array as that will be the whole match.
     // Ignore the first element of the match array as that will be the whole match.
     const [, ...matches] = fmt.match(R_FORMAT);
     const [, ...matches] = fmt.match(R_FORMAT);
-    const [startCol, startRow, endCol, endRow, colStep, rowStep] = matches;
+    const [sheetExpression, startCol, startRow, endCol, endRow, colStep, rowStep] = matches;
+    const sheetFromName = name => {
+        if (name == null || name === "") return thisSheet;
+        return sheet(JSON.parse(name));
+    };
     return new Range(
     return new Range(
         startCol,
         startCol,
         endCol ?? startCol,
         endCol ?? startCol,
@@ -384,7 +421,8 @@ function R(fmt, ...args) {
         // Don't make undefined an integer, because then it becomes 0.
         // Don't make undefined an integer, because then it becomes 0.
         !!endRow ? integer(endRow) : endRow,
         !!endRow ? integer(endRow) : endRow,
         integer(colStep ?? 1),
         integer(colStep ?? 1),
-        integer(rowStep ?? 1)
+        integer(rowStep ?? 1),
+        sheetFromName(sheetExpression)
     );
     );
 }
 }
 
 

+ 12 - 0
Userland/Applications/Spreadsheet/JSIntegration.cpp

@@ -154,6 +154,7 @@ void SheetGlobalObject::initialize_global_object()
     define_native_function("column_arithmetic", column_arithmetic, 2, attr);
     define_native_function("column_arithmetic", column_arithmetic, 2, attr);
     define_native_function("column_index", column_index, 1, attr);
     define_native_function("column_index", column_index, 1, attr);
     define_native_function("get_column_bound", get_column_bound, 1, attr);
     define_native_function("get_column_bound", get_column_bound, 1, attr);
+    define_native_accessor("name", get_name, nullptr, attr);
 }
 }
 
 
 void SheetGlobalObject::visit_edges(Visitor& visitor)
 void SheetGlobalObject::visit_edges(Visitor& visitor)
@@ -167,6 +168,17 @@ void SheetGlobalObject::visit_edges(Visitor& visitor)
     }
     }
 }
 }
 
 
+JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::get_name)
+{
+    auto* this_object = TRY(vm.this_value(global_object).to_object(global_object));
+
+    if (!is<SheetGlobalObject>(this_object))
+        return vm.throw_completion<JS::TypeError>(global_object, JS::ErrorType::NotAnObjectOfType, "SheetGlobalObject");
+
+    auto sheet_object = static_cast<SheetGlobalObject*>(this_object);
+    return JS::js_string(global_object.heap(), sheet_object->m_sheet.name());
+}
+
 JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::get_real_cell_contents)
 JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::get_real_cell_contents)
 {
 {
     auto* this_object = TRY(vm.this_value(global_object).to_object(global_object));
     auto* this_object = TRY(vm.this_value(global_object).to_object(global_object));

+ 1 - 0
Userland/Applications/Spreadsheet/JSIntegration.h

@@ -39,6 +39,7 @@ public:
     JS_DECLARE_NATIVE_FUNCTION(column_index);
     JS_DECLARE_NATIVE_FUNCTION(column_index);
     JS_DECLARE_NATIVE_FUNCTION(column_arithmetic);
     JS_DECLARE_NATIVE_FUNCTION(column_arithmetic);
     JS_DECLARE_NATIVE_FUNCTION(get_column_bound);
     JS_DECLARE_NATIVE_FUNCTION(get_column_bound);
+    JS_DECLARE_NATIVE_FUNCTION(get_name);
 
 
 private:
 private:
     virtual void visit_edges(Visitor&) override;
     virtual void visit_edges(Visitor&) override;

+ 22 - 0
Userland/Applications/Spreadsheet/Tests/basic.js

@@ -78,6 +78,28 @@ describe("Range", () => {
         expect(cellsVisited).toEqual(6);
         expect(cellsVisited).toEqual(6);
     });
     });
 
 
+    test("multiple sheets", () => {
+        const workbook = createWorkbook();
+        const sheet1 = createSheet(workbook, "Sheet 1");
+        const sheet2 = createSheet(workbook, "Sheet 2");
+        sheet1.makeCurrent();
+
+        sheet1.setCell("A", 0, "0");
+        sheet1.focusCell("A", 0);
+
+        sheet2.setCell("A", 0, "0");
+        sheet2.setCell("A", 10, "0");
+        sheet2.setCell("B", 1, "0");
+        sheet2.focusCell("A", 0);
+
+        expect(R).toBeDefined();
+        let cellsVisited = 0;
+        R`sheet("Sheet 2"):A0:A10`.forEach(name => {
+            ++cellsVisited;
+        });
+        expect(cellsVisited).toEqual(11);
+    });
+
     test("Ranges", () => {
     test("Ranges", () => {
         const workbook = createWorkbook();
         const workbook = createWorkbook();
         const sheet = createSheet(workbook, "Sheet 1");
         const sheet = createSheet(workbook, "Sheet 1");