瀏覽代碼

Spreadsheet: Add support for multiple sheets

This also refactors the js integration stuff to allow sheets to
reference each other safely.
AnotherTest 4 年之前
父節點
當前提交
cb7fe4fe7c

+ 1 - 0
Applications/Spreadsheet/CMakeLists.txt

@@ -1,6 +1,7 @@
 set(SOURCES
     CellSyntaxHighlighter.cpp
     HelpWindow.cpp
+    JSIntegration.cpp
     Spreadsheet.cpp
     SpreadsheetModel.cpp
     SpreadsheetView.cpp

+ 157 - 0
Applications/Spreadsheet/JSIntegration.cpp

@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "JSIntegration.h"
+#include "Spreadsheet.h"
+#include "Workbook.h"
+#include <LibJS/Runtime/Error.h>
+#include <LibJS/Runtime/GlobalObject.h>
+#include <LibJS/Runtime/Object.h>
+#include <LibJS/Runtime/Value.h>
+
+namespace Spreadsheet {
+
+SheetGlobalObject::SheetGlobalObject(Sheet& sheet)
+    : m_sheet(sheet)
+{
+}
+
+SheetGlobalObject::~SheetGlobalObject()
+{
+}
+
+JS::Value SheetGlobalObject::get(const JS::PropertyName& name, JS::Value receiver) const
+{
+    if (name.is_string()) {
+        if (auto pos = Sheet::parse_cell_name(name.as_string()); pos.has_value()) {
+            auto& cell = m_sheet.ensure(pos.value());
+            cell.reference_from(m_sheet.current_evaluated_cell());
+            return cell.js_data();
+        }
+    }
+
+    return GlobalObject::get(name, receiver);
+}
+
+bool SheetGlobalObject::put(const JS::PropertyName& name, JS::Value value, JS::Value receiver)
+{
+    if (name.is_string()) {
+        if (auto pos = Sheet::parse_cell_name(name.as_string()); pos.has_value()) {
+            auto& cell = m_sheet.ensure(pos.value());
+            if (auto current = m_sheet.current_evaluated_cell())
+                current->reference_from(&cell);
+
+            cell.set_data(value); // FIXME: This produces un-savable state!
+            return true;
+        }
+    }
+
+    return GlobalObject::put(name, value, receiver);
+}
+
+void SheetGlobalObject::initialize()
+{
+    GlobalObject::initialize();
+    define_native_function("parse_cell_name", parse_cell_name, 1);
+}
+
+JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::parse_cell_name)
+{
+    if (interpreter.argument_count() != 1) {
+        interpreter.throw_exception<JS::TypeError>("Expected exactly one argument to parse_cell_name()");
+        return {};
+    }
+    auto name_value = interpreter.argument(0);
+    if (!name_value.is_string()) {
+        interpreter.throw_exception<JS::TypeError>("Expected a String argument to parse_cell_name()");
+        return {};
+    }
+    auto position = Sheet::parse_cell_name(name_value.as_string().string());
+    if (!position.has_value())
+        return JS::js_undefined();
+
+    auto object = JS::Object::create_empty(interpreter.global_object());
+    object->put("column", JS::js_string(interpreter, position.value().column));
+    object->put("row", JS::Value((unsigned)position.value().row));
+
+    return object;
+}
+
+WorkbookObject::WorkbookObject(Workbook& workbook)
+    : JS::Object(*JS::Object::create_empty(workbook.global_object()))
+    , m_workbook(workbook)
+{
+}
+
+WorkbookObject::~WorkbookObject()
+{
+}
+
+void WorkbookObject::initialize(JS::GlobalObject& global_object)
+{
+    Object::initialize(global_object);
+    define_native_function("sheet", sheet, 1);
+}
+
+JS_DEFINE_NATIVE_FUNCTION(WorkbookObject::sheet)
+{
+    if (interpreter.argument_count() != 1) {
+        interpreter.throw_exception<JS::TypeError>("Expected exactly one argument to sheet()");
+        return {};
+    }
+    auto name_value = interpreter.argument(0);
+    if (!name_value.is_string() && !name_value.is_number()) {
+        interpreter.throw_exception<JS::TypeError>("Expected a String or Number argument to sheet()");
+        return {};
+    }
+
+    auto* this_object = interpreter.this_value(global_object).to_object(interpreter, global_object);
+    if (!this_object)
+        return {};
+
+    if (!this_object->inherits("WorkbookObject")) {
+        interpreter.throw_exception<JS::TypeError>(JS::ErrorType::NotA, "WorkbookObject");
+        return {};
+    }
+
+    auto& workbook = static_cast<WorkbookObject*>(this_object)->m_workbook;
+
+    if (name_value.is_string()) {
+        auto& name = name_value.as_string().string();
+        for (auto& sheet : workbook.sheets()) {
+            if (sheet.name() == name)
+                return JS::Value(&sheet.global_object());
+        }
+    } else {
+        auto index = name_value.as_size_t();
+        if (index < workbook.sheets().size())
+            return JS::Value(&workbook.sheets()[index].global_object());
+    }
+
+    return JS::js_undefined();
+}
+
+}

+ 71 - 0
Applications/Spreadsheet/JSIntegration.h

@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibJS/Forward.h>
+#include <LibJS/Runtime/GlobalObject.h>
+
+namespace Spreadsheet {
+
+class Sheet;
+class Workbook;
+
+class SheetGlobalObject : public JS::GlobalObject {
+    JS_OBJECT(SheetGlobalObject, JS::GlobalObject);
+
+public:
+    SheetGlobalObject(Sheet& sheet);
+
+    virtual ~SheetGlobalObject() override;
+
+    virtual JS::Value get(const JS::PropertyName& name, JS::Value receiver = {}) const override;
+    virtual bool put(const JS::PropertyName& name, JS::Value value, JS::Value receiver = {}) override;
+    virtual void initialize() override;
+
+    JS_DECLARE_NATIVE_FUNCTION(parse_cell_name);
+
+private:
+    Sheet& m_sheet;
+};
+
+class WorkbookObject : public JS::Object {
+    JS_OBJECT(WorkbookObject, JS::Object);
+
+public:
+    WorkbookObject(Workbook& workbook);
+
+    virtual ~WorkbookObject() override;
+
+    virtual void initialize(JS::GlobalObject&) override;
+
+    JS_DECLARE_NATIVE_FUNCTION(sheet);
+
+private:
+    Workbook& m_workbook;
+};
+
+}

+ 58 - 102
Applications/Spreadsheet/Spreadsheet.cpp

@@ -25,96 +25,20 @@
  */
 
 #include "Spreadsheet.h"
+#include "JSIntegration.h"
+#include "Workbook.h"
 #include <AK/GenericLexer.h>
 #include <AK/JsonArray.h>
 #include <AK/JsonObject.h>
 #include <AK/JsonParser.h>
 #include <LibCore/File.h>
 #include <LibJS/Parser.h>
-#include <LibJS/Runtime/Error.h>
 #include <LibJS/Runtime/Function.h>
-#include <LibJS/Runtime/GlobalObject.h>
-#include <LibJS/Runtime/Object.h>
-#include <LibJS/Runtime/Value.h>
 
 namespace Spreadsheet {
 
-class SheetGlobalObject : public JS::GlobalObject {
-    JS_OBJECT(SheetGlobalObject, JS::GlobalObject);
-
-public:
-    SheetGlobalObject(Sheet& sheet)
-        : m_sheet(sheet)
-    {
-    }
-
-    virtual ~SheetGlobalObject() override
-    {
-    }
-
-    virtual JS::Value get(const JS::PropertyName& name, JS::Value receiver = {}) const override
-    {
-        if (name.is_string()) {
-            if (auto pos = Sheet::parse_cell_name(name.as_string()); pos.has_value()) {
-                auto& cell = m_sheet.ensure(pos.value());
-                cell.reference_from(m_sheet.current_evaluated_cell());
-                return cell.js_data();
-            }
-        }
-
-        return GlobalObject::get(name, receiver);
-    }
-
-    virtual bool put(const JS::PropertyName& name, JS::Value value, JS::Value receiver = {}) override
-    {
-        if (name.is_string()) {
-            if (auto pos = Sheet::parse_cell_name(name.as_string()); pos.has_value()) {
-                auto& cell = m_sheet.ensure(pos.value());
-                if (auto current = m_sheet.current_evaluated_cell())
-                    current->reference_from(&cell);
-
-                cell.set_data(value); // FIXME: This produces un-savable state!
-                return true;
-            }
-        }
-
-        return GlobalObject::put(name, value, receiver);
-    }
-
-    virtual void initialize() override
-    {
-        GlobalObject::initialize();
-        define_native_function("parse_cell_name", parse_cell_name, 1);
-    }
-
-    static JS_DEFINE_NATIVE_FUNCTION(parse_cell_name)
-    {
-        if (interpreter.argument_count() != 1) {
-            interpreter.throw_exception<JS::TypeError>("Expected exactly one argument to parse_cell_name()");
-            return {};
-        }
-        auto name_value = interpreter.argument(0);
-        if (!name_value.is_string()) {
-            interpreter.throw_exception<JS::TypeError>("Expected a String argument to parse_cell_name()");
-            return {};
-        }
-        auto position = Sheet::parse_cell_name(name_value.as_string().string());
-        if (!position.has_value())
-            return JS::js_undefined();
-
-        auto object = JS::Object::create_empty(interpreter.global_object());
-        object->put("column", JS::js_string(interpreter, position.value().column));
-        object->put("row", JS::Value((unsigned)position.value().row));
-
-        return object;
-    }
-
-private:
-    Sheet& m_sheet;
-};
-
-Sheet::Sheet(const StringView& name)
-    : Sheet(EmptyConstruct::EmptyConstructTag)
+Sheet::Sheet(const StringView& name, Workbook& workbook)
+    : Sheet(workbook)
 {
     m_name = name;
 
@@ -125,14 +49,40 @@ Sheet::Sheet(const StringView& name)
         add_column();
 }
 
-Sheet::Sheet(EmptyConstruct)
-    : m_interpreter(JS::Interpreter::create<SheetGlobalObject>(*this))
+Sheet::Sheet(Workbook& workbook)
+    : m_workbook(workbook)
 {
+    m_global_object = m_workbook.interpreter().heap().allocate_without_global_object<SheetGlobalObject>(*this);
+    m_global_object->set_prototype(&m_workbook.global_object());
+    m_global_object->initialize();
+    m_global_object->put("thisSheet", m_global_object); // Self-reference is unfortunate, but required.
+
+    // Sadly, these have to be evaluated once per sheet.
     auto file_or_error = Core::File::open("/res/js/Spreadsheet/runtime.js", Core::IODevice::OpenMode::ReadOnly);
     if (!file_or_error.is_error()) {
         auto buffer = file_or_error.value()->read_all();
-        evaluate(buffer);
+        JS::Parser parser { JS::Lexer(buffer) };
+        if (parser.has_errors()) {
+            dbg() << "Spreadsheet: Failed to parse runtime code";
+            for (auto& error : parser.errors())
+                dbg() << "Error: " << error.to_string() << "\n"
+                      << error.source_location_hint(buffer);
+        } else {
+            interpreter().run(global_object(), parser.parse_program());
+            if (auto exc = interpreter().exception()) {
+                dbg() << "Spreadsheet: Failed to run runtime code: ";
+                for (auto& t : exc->trace())
+                    dbg() << t;
+                interpreter().clear_exception();
+            }
+        }
     }
+
+}
+
+JS::Interpreter& Sheet::interpreter() const
+{
+    return m_workbook.interpreter();
 }
 
 Sheet::~Sheet()
@@ -208,14 +158,14 @@ JS::Value Sheet::evaluate(const StringView& source, Cell* on_behalf_of)
         return JS::js_undefined();
 
     auto program = parser.parse_program();
-    m_interpreter->run(m_interpreter->global_object(), program);
-    if (m_interpreter->exception()) {
-        auto exc = m_interpreter->exception()->value();
-        m_interpreter->clear_exception();
+    interpreter().run(global_object(), program);
+    if (interpreter().exception()) {
+        auto exc = interpreter().exception()->value();
+        interpreter().clear_exception();
         return exc;
     }
 
-    auto value = m_interpreter->last_value();
+    auto value = interpreter().last_value();
     if (value.is_empty())
         return JS::js_undefined();
     return value;
@@ -309,9 +259,9 @@ void Cell::reference_from(Cell* other)
     referencing_cells.append(other->make_weak_ptr());
 }
 
-RefPtr<Sheet> Sheet::from_json(const JsonObject& object)
+RefPtr<Sheet> Sheet::from_json(const JsonObject& object, Workbook& workbook)
 {
-    auto sheet = adopt(*new Sheet(EmptyConstruct::EmptyConstructTag));
+    auto sheet = adopt(*new Sheet(workbook));
     auto rows = object.get("rows").to_u32(20);
     auto columns = object.get("columns");
     if (!columns.is_array())
@@ -385,8 +335,8 @@ JsonObject Sheet::to_json() const
         data.set("kind", it.value->kind == Cell::Kind::Formula ? "Formula" : "LiteralString");
         if (it.value->kind == Cell::Formula) {
             data.set("source", it.value->data);
-            auto json = m_interpreter->global_object().get("JSON");
-            auto stringified = m_interpreter->call(json.as_object().get("stringify").as_function(), json, it.value->evaluated_data);
+            auto json = interpreter().global_object().get("JSON");
+            auto stringified = interpreter().call(json.as_object().get("stringify").as_function(), json, it.value->evaluated_data);
             data.set("value", stringified.to_string_without_side_effects());
         } else {
             data.set("value", it.value->data);
@@ -404,19 +354,19 @@ JsonObject Sheet::gather_documentation() const
     JsonObject object;
     const JS::PropertyName doc_name { "__documentation" };
 
-    auto& global_object = m_interpreter->global_object();
-    for (auto& it : global_object.shape().property_table()) {
+    auto add_docs_from = [&](auto& it, auto& global_object) {
         auto value = global_object.get(it.key);
-        if (!value.is_function())
-            continue;
+        if (!value.is_function() && !value.is_object())
+            return;
 
-        auto& fn = value.as_function();
-        if (!fn.has_own_property(doc_name))
-            continue;
+        auto& value_object = value.is_object() ? value.as_object() : value.as_function();
+        if (!value_object.has_own_property(doc_name))
+            return;
 
-        auto doc = fn.get(doc_name);
+        dbg() << "Found '" << it.key.to_display_string() << "'";
+        auto doc = value_object.get(doc_name);
         if (!doc.is_string())
-            continue;
+            return;
 
         JsonParser parser(doc.to_string_without_side_effects());
         auto doc_object = parser.parse();
@@ -425,7 +375,13 @@ JsonObject Sheet::gather_documentation() const
             object.set(it.key.to_display_string(), doc_object.value());
         else
             dbg() << "Sheet::gather_documentation(): Failed to parse the documentation for '" << it.key.to_display_string() << "'!";
-    }
+    };
+
+    for (auto& it : interpreter().global_object().shape().property_table())
+        add_docs_from(it, interpreter().global_object());
+
+    for (auto& it : global_object().shape().property_table())
+        add_docs_from(it, global_object());
 
     return object;
 }

+ 11 - 7
Applications/Spreadsheet/Spreadsheet.h

@@ -39,6 +39,9 @@
 
 namespace Spreadsheet {
 
+class Workbook;
+class SheetGlobalObject;
+
 struct Position {
     String column;
     size_t row { 0 };
@@ -140,7 +143,7 @@ public:
     static Optional<Position> parse_cell_name(const StringView&);
 
     JsonObject to_json() const;
-    static RefPtr<Sheet> from_json(const JsonObject&);
+    static RefPtr<Sheet> from_json(const JsonObject&, Workbook&);
 
     const String& name() const { return m_name; }
     void set_name(const StringView& name) { m_name = name; }
@@ -183,16 +186,15 @@ public:
     void update(Cell&);
 
     JS::Value evaluate(const StringView&, Cell* = nullptr);
-    JS::Interpreter& interpreter() { return *m_interpreter; }
+    JS::Interpreter& interpreter() const;
+    SheetGlobalObject& global_object() const { return *m_global_object; }
 
     Cell*& current_evaluated_cell() { return m_current_cell_being_evaluated; }
     bool has_been_visited(Cell* cell) const { return m_visited_cells_in_update.contains(cell); }
 
 private:
-    enum class EmptyConstruct { EmptyConstructTag };
-
-    explicit Sheet(EmptyConstruct);
-    explicit Sheet(const StringView& name);
+    explicit Sheet(Workbook&);
+    explicit Sheet(const StringView& name, Workbook&);
 
     String m_name;
     Vector<String> m_columns;
@@ -200,11 +202,13 @@ private:
     HashMap<Position, NonnullOwnPtr<Cell>> m_cells;
     Optional<Position> m_selected_cell; // FIXME: Make this a collection.
 
+    Workbook& m_workbook;
+    mutable SheetGlobalObject* m_global_object;
+
     Cell* m_current_cell_being_evaluated { nullptr };
 
     size_t m_current_column_name_length { 0 };
 
-    mutable NonnullOwnPtr<JS::Interpreter> m_interpreter;
     HashTable<Cell*> m_visited_cells_in_update;
 };
 

+ 17 - 4
Applications/Spreadsheet/SpreadsheetWidget.cpp

@@ -81,13 +81,13 @@ SpreadsheetWidget::SpreadsheetWidget(NonnullRefPtrVector<Sheet>&& sheets, bool s
     if (!m_workbook->has_sheets() && should_add_sheet_if_empty)
         m_workbook->add_sheet("Sheet 1");
 
-    setup_tabs();
+    setup_tabs(m_workbook->sheets());
 }
 
-void SpreadsheetWidget::setup_tabs()
+void SpreadsheetWidget::setup_tabs(NonnullRefPtrVector<Sheet> new_sheets)
 {
     RefPtr<GUI::Widget> first_tab_widget;
-    for (auto& sheet : m_workbook->sheets()) {
+    for (auto& sheet : new_sheets) {
         auto& tab = m_tab_widget->add_tab<SpreadsheetView>(sheet.name(), sheet);
         if (!first_tab_widget)
             first_tab_widget = &tab;
@@ -148,7 +148,20 @@ void SpreadsheetWidget::load(const StringView& filename)
         m_tab_widget->remove_tab(*widget);
     }
 
-    setup_tabs();
+    setup_tabs(m_workbook->sheets());
+}
+
+void SpreadsheetWidget::add_sheet()
+{
+    StringBuilder name;
+    name.append("Sheet");
+    name.appendf(" %d", m_workbook->sheets().size() + 1);
+
+    auto& sheet = m_workbook->add_sheet(name.string_view());
+
+    NonnullRefPtrVector<Sheet> new_sheets;
+    new_sheets.append(sheet);
+    setup_tabs(new_sheets);
 }
 
 void SpreadsheetWidget::set_filename(const String& filename)

+ 2 - 1
Applications/Spreadsheet/SpreadsheetWidget.h

@@ -41,6 +41,7 @@ public:
 
     void save(const StringView& filename);
     void load(const StringView& filename);
+    void add_sheet();
 
     const String& current_filename() const { return m_workbook->current_filename(); }
     void set_filename(const String& filename);
@@ -48,7 +49,7 @@ public:
 private:
     explicit SpreadsheetWidget(NonnullRefPtrVector<Sheet>&& sheets = {}, bool should_add_sheet_if_empty = true);
 
-    void setup_tabs();
+    void setup_tabs(NonnullRefPtrVector<Sheet> new_sheets);
 
     SpreadsheetView* m_selected_view { nullptr };
     RefPtr<GUI::Label> m_current_cell_label;

+ 12 - 1
Applications/Spreadsheet/Workbook.cpp

@@ -25,15 +25,26 @@
  */
 
 #include "Workbook.h"
+#include "JSIntegration.h"
 #include <AK/JsonArray.h>
 #include <AK/JsonObject.h>
 #include <AK/JsonObjectSerializer.h>
 #include <AK/JsonParser.h>
 #include <LibCore/File.h>
+#include <LibJS/Parser.h>
+#include <LibJS/Runtime/GlobalObject.h>
 #include <string.h>
 
 namespace Spreadsheet {
 
+Workbook::Workbook(NonnullRefPtrVector<Sheet>&& sheets)
+    : m_sheets(move(sheets))
+    , m_interpreter(JS::Interpreter::create<JS::GlobalObject>())
+{
+    m_workbook_object = interpreter().heap().allocate<WorkbookObject>(global_object(), *this);
+    global_object().put("workbook", workbook_object());
+}
+
 bool Workbook::set_filename(const String& filename)
 {
     if (m_current_filename == filename)
@@ -81,7 +92,7 @@ Result<bool, String> Workbook::load(const StringView& filename)
         if (!sheet_json.is_object())
             return IterationDecision::Continue;
 
-        auto sheet = Sheet::from_json(sheet_json.as_object());
+        auto sheet = Sheet::from_json(sheet_json.as_object(), *this);
         if (!sheet)
             return IterationDecision::Continue;
 

+ 14 - 5
Applications/Spreadsheet/Workbook.h

@@ -32,12 +32,11 @@
 
 namespace Spreadsheet {
 
+class WorkbookObject;
+
 class Workbook {
 public:
-    Workbook(NonnullRefPtrVector<Sheet>&& sheets)
-        : m_sheets(move(sheets))
-    {
-    }
+    Workbook(NonnullRefPtrVector<Sheet>&& sheets);
 
     Result<bool, String> save(const StringView& filename);
     Result<bool, String> load(const StringView& filename);
@@ -52,13 +51,23 @@ public:
 
     Sheet& add_sheet(const StringView& name)
     {
-        auto sheet = Sheet::construct(name);
+        auto sheet = Sheet::construct(name, *this);
         m_sheets.append(sheet);
         return *sheet;
     }
 
+    JS::Interpreter& interpreter() { return *m_interpreter; }
+    const JS::Interpreter& interpreter() const { return *m_interpreter; }
+
+    JS::GlobalObject& global_object() { return m_interpreter->global_object(); }
+    const JS::GlobalObject& global_object() const { return m_interpreter->global_object(); }
+
+    WorkbookObject* workbook_object() { return m_workbook_object; }
+
 private:
     NonnullRefPtrVector<Sheet> m_sheets;
+    NonnullOwnPtr<JS::Interpreter> m_interpreter;
+    WorkbookObject* m_workbook_object { nullptr };
 
     String m_current_filename;
 };

+ 3 - 0
Applications/Spreadsheet/main.cpp

@@ -95,6 +95,9 @@ int main(int argc, char* argv[])
     auto menubar = GUI::MenuBar::construct();
     auto& app_menu = menubar->add_menu("Spreadsheet");
 
+    app_menu.add_action(GUI::Action::create("Add New Sheet", Gfx::Bitmap::load_from_file("/res/icons/16x16/new-tab.png"), [&](auto&) {
+        spreadsheet_widget.add_sheet();
+    }));
     app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
         app->quit(0);
     }));

+ 19 - 6
Base/res/js/Spreadsheet/runtime.js

@@ -1,10 +1,8 @@
-const sheet = this;
-
 function range(start, end, columnStep, rowStep) {
     columnStep = integer(columnStep ?? 1);
     rowStep = integer(rowStep ?? 1);
-    start = sheet.parse_cell_name(start) ?? { column: "A", row: 0 };
-    end = sheet.parse_cell_name(end) ?? start;
+    start = parse_cell_name(start) ?? { column: "A", row: 0 };
+    end = parse_cell_name(end) ?? start;
 
     if (end.column.length > 1 || start.column.length > 1)
         throw new TypeError("Only single-letter column names are allowed (TODO)");
@@ -58,7 +56,7 @@ function select(criteria, t, f) {
 function sumIf(condition, cells) {
     let sum = null;
     for (let name of cells) {
-        let cell = sheet[name];
+        let cell = thisSheet[name];
         if (condition(cell)) sum = sum === null ? cell : sum + cell;
     }
     return sum;
@@ -67,7 +65,7 @@ function sumIf(condition, cells) {
 function countIf(condition, cells) {
     let count = 0;
     for (let name of cells) {
-        let cell = sheet[name];
+        let cell = thisSheet[name];
         if (condition(cell)) count++;
     }
     return count;
@@ -89,6 +87,10 @@ function integer(value) {
     return value | 0;
 }
 
+function sheet(name) {
+    return workbook.sheet(name);
+}
+
 // Cheat the system and add documentation
 range.__documentation = JSON.stringify({
     name: "range",
@@ -172,3 +174,14 @@ integer.__documentation = JSON.stringify({
         "A1 = integer(A0)": "Sets the value of the cell A1 to the integer value of the cell A0",
     },
 });
+
+sheet.__documentation = JSON.stringify({
+    name: "sheet",
+    argc: 1,
+    argnames: ["name or index"],
+    doc: "Returns a reference to another sheet, identified by _name_ or _index_",
+    examples: {
+        "sheet('Sheet 1').A4": "Read the value of the cell A4 in a sheet named 'Sheet 1'",
+        "sheet(0).A0 = 123": "Set the value of the cell A0 in the first sheet to 123",
+    },
+});