1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-23 21:55:08 +00:00
serenity/Applications/Spreadsheet/Spreadsheet.cpp
AnotherTest e99c2261e3 Spreadsheet: Add support for copying ranges of cells to other cells
Now the entire range is copied to the area around the target cell,
translating the current cursor to the target.
2020-11-08 21:46:13 +01:00

525 lines
17 KiB
C++

/*
* 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 "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 <AK/ScopeGuard.h>
#include <AK/TemporaryChange.h>
#include <AK/URL.h>
#include <LibCore/File.h>
#include <LibJS/Parser.h>
#include <LibJS/Runtime/Function.h>
#include <ctype.h>
//#define COPY_DEBUG
namespace Spreadsheet {
Sheet::Sheet(const StringView& name, Workbook& workbook)
: Sheet(workbook)
{
m_name = name;
for (size_t i = 0; i < 20; ++i)
add_row();
for (size_t i = 0; i < 16; ++i)
add_column();
}
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();
JS::Parser parser { JS::Lexer(buffer) };
if (parser.has_errors()) {
dbgln("Spreadsheet: Failed to parse runtime code");
for (auto& error : parser.errors())
dbgln("Error: {}\n{}", error.to_string(), error.source_location_hint(buffer));
} else {
interpreter().run(global_object(), parser.parse_program());
if (auto exc = interpreter().exception()) {
dbgln("Spreadsheet: Failed to run runtime code: ");
for (auto& t : exc->trace())
dbgln("{}", t);
interpreter().vm().clear_exception();
}
}
}
}
Sheet::~Sheet()
{
}
JS::Interpreter& Sheet::interpreter() const
{
return m_workbook.interpreter();
}
size_t Sheet::add_row()
{
return m_rows++;
}
String Sheet::add_column()
{
if (m_current_column_name_length == 0) {
m_current_column_name_length = 1;
m_columns.append("A");
return "A";
}
if (m_current_column_name_length == 1) {
auto last_char = m_columns.last()[0];
if (last_char == 'Z') {
m_current_column_name_length = 2;
m_columns.append("AA");
return "AA";
}
last_char++;
m_columns.append({ &last_char, 1 });
return m_columns.last();
}
TODO();
}
void Sheet::update()
{
m_visited_cells_in_update.clear();
Vector<Cell*> cells_copy;
// Grab a copy as updates might insert cells into the table.
for (auto& it : m_cells)
cells_copy.append(it.value);
for (auto& cell : cells_copy) {
if (has_been_visited(cell))
continue;
m_visited_cells_in_update.set(cell);
if (cell->dirty) {
// Re-evaluate the cell value, if any.
cell->update({});
}
}
m_visited_cells_in_update.clear();
}
void Sheet::update(Cell& cell)
{
if (has_been_visited(&cell))
return;
m_visited_cells_in_update.set(&cell);
cell.update({});
}
JS::Value Sheet::evaluate(const StringView& source, Cell* on_behalf_of)
{
TemporaryChange cell_change { m_current_cell_being_evaluated, on_behalf_of };
auto parser = JS::Parser(JS::Lexer(source));
if (parser.has_errors())
return JS::js_undefined();
auto program = parser.parse_program();
interpreter().run(global_object(), program);
if (interpreter().exception()) {
auto exc = interpreter().exception()->value();
interpreter().vm().clear_exception();
return exc;
}
auto value = interpreter().vm().last_value();
if (value.is_empty())
return JS::js_undefined();
return value;
}
Cell* Sheet::at(const StringView& name)
{
auto pos = parse_cell_name(name);
if (pos.has_value())
return at(pos.value());
return nullptr;
}
Cell* Sheet::at(const Position& position)
{
auto it = m_cells.find(position);
if (it == m_cells.end())
return nullptr;
return it->value;
}
Optional<Position> Sheet::parse_cell_name(const StringView& name)
{
GenericLexer lexer(name);
auto col = lexer.consume_while(isalpha);
auto row = lexer.consume_while(isdigit);
if (!lexer.is_eof() || row.is_empty() || col.is_empty())
return {};
return Position { col, row.to_uint().value() };
}
Cell* Sheet::from_url(const URL& url)
{
auto maybe_position = position_from_url(url);
if (!maybe_position.has_value())
return nullptr;
return at(maybe_position.value());
}
Optional<Position> Sheet::position_from_url(const URL& url) const
{
if (!url.is_valid()) {
dbgln("Invalid url: {}", url.to_string());
return {};
}
if (url.protocol() != "spreadsheet" || url.host() != "cell") {
dbgln("Bad url: {}", url.to_string());
return {};
}
// FIXME: Figure out a way to do this cross-process.
ASSERT(url.path() == String::formatted("/{}", getpid()));
return parse_cell_name(url.fragment());
}
Position Sheet::offset_relative_to(const Position& base, const Position& offset, const Position& offset_base) const
{
auto offset_column_it = m_columns.find(offset.column);
auto offset_base_column_it = m_columns.find(offset_base.column);
auto base_column_it = m_columns.find(base.column);
if (offset_column_it.is_end()) {
dbg() << "Column '" << offset.column << "' does not exist!";
return base;
}
if (offset_base_column_it.is_end()) {
dbg() << "Column '" << offset_base.column << "' does not exist!";
return base;
}
if (base_column_it.is_end()) {
dbg() << "Column '" << base.column << "' does not exist!";
return offset;
}
auto new_column = column(offset_column_it.index() + base_column_it.index() - offset_base_column_it.index());
auto new_row = offset.row + base.row - offset_base.row;
return { move(new_column), new_row };
}
void Sheet::copy_cells(Vector<Position> from, Vector<Position> to, Optional<Position> resolve_relative_to)
{
auto copy_to = [&](auto& source_position, Position target_position) {
auto& target_cell = ensure(target_position);
auto* source_cell = at(source_position);
if (!source_cell) {
target_cell.set_data("");
return;
}
auto ref_cells = target_cell.referencing_cells;
target_cell = *source_cell;
target_cell.dirty = true;
target_cell.referencing_cells = move(ref_cells);
};
if (from.size() == to.size()) {
auto from_it = from.begin();
// FIXME: Ordering.
for (auto& position : to)
copy_to(*from_it++, position);
return;
}
if (to.size() == 1) {
// Resolve each index as relative to the first index offset from the selection.
auto& target = to.first();
for (auto& position : from) {
#ifdef COPY_DEBUG
dbg() << "Paste from '" << position.to_url() << "' to '" << target.to_url() << "'";
#endif
copy_to(position, resolve_relative_to.has_value() ? offset_relative_to(target, position, resolve_relative_to.value()) : target);
}
return;
}
// Just disallow misaligned copies.
dbg() << "Cannot copy " << from.size() << " cells to " << to.size() << " cells";
}
RefPtr<Sheet> Sheet::from_json(const JsonObject& object, Workbook& workbook)
{
auto sheet = adopt(*new Sheet(workbook));
auto rows = object.get("rows").to_u32(20);
auto columns = object.get("columns");
if (!columns.is_array())
return nullptr;
auto name = object.get("name").as_string_or("Sheet");
sheet->set_name(name);
for (size_t i = 0; i < rows; ++i)
sheet->add_row();
// FIXME: Better error checking.
columns.as_array().for_each([&](auto& value) {
sheet->m_columns.append(value.as_string());
return IterationDecision::Continue;
});
auto cells = object.get("cells").as_object();
auto json = sheet->interpreter().global_object().get("JSON");
auto& parse_function = json.as_object().get("parse").as_function();
auto read_format = [](auto& format, const auto& obj) {
if (auto value = obj.get("foreground_color"); value.is_string())
format.foreground_color = Color::from_string(value.as_string());
if (auto value = obj.get("background_color"); value.is_string())
format.background_color = Color::from_string(value.as_string());
};
cells.for_each_member([&](auto& name, JsonValue& value) {
auto position_option = parse_cell_name(name);
if (!position_option.has_value())
return IterationDecision::Continue;
auto position = position_option.value();
auto& obj = value.as_object();
auto kind = obj.get("kind").as_string_or("LiteralString") == "LiteralString" ? Cell::LiteralString : Cell::Formula;
OwnPtr<Cell> cell;
switch (kind) {
case Cell::LiteralString:
cell = make<Cell>(obj.get("value").to_string(), position, sheet->make_weak_ptr());
break;
case Cell::Formula: {
auto& interpreter = sheet->interpreter();
auto value = interpreter.vm().call(parse_function, json, JS::js_string(interpreter.heap(), obj.get("value").as_string()));
cell = make<Cell>(obj.get("source").to_string(), move(value), position, sheet->make_weak_ptr());
break;
}
}
auto type_name = obj.get_or("type", "Numeric").to_string();
cell->set_type(type_name);
auto type_meta = obj.get("type_metadata");
if (type_meta.is_object()) {
auto& meta_obj = type_meta.as_object();
auto meta = cell->type_metadata();
if (auto value = meta_obj.get("length"); value.is_number())
meta.length = value.to_i32();
if (auto value = meta_obj.get("format"); value.is_string())
meta.format = value.as_string();
read_format(meta.static_format, meta_obj);
cell->set_type_metadata(move(meta));
}
auto conditional_formats = obj.get("conditional_formats");
auto cformats = cell->conditional_formats();
if (conditional_formats.is_array()) {
conditional_formats.as_array().for_each([&](const auto& fmt_val) {
if (!fmt_val.is_object())
return IterationDecision::Continue;
auto& fmt_obj = fmt_val.as_object();
auto fmt_cond = fmt_obj.get("condition").to_string();
if (fmt_cond.is_empty())
return IterationDecision::Continue;
ConditionalFormat fmt;
fmt.condition = move(fmt_cond);
read_format(fmt, fmt_obj);
cformats.append(move(fmt));
return IterationDecision::Continue;
});
cell->set_conditional_formats(move(cformats));
}
auto evaluated_format = obj.get("evaluated_formats");
if (evaluated_format.is_object()) {
auto& evaluated_format_obj = evaluated_format.as_object();
auto& evaluated_fmts = cell->m_evaluated_formats;
read_format(evaluated_fmts, evaluated_format_obj);
}
sheet->m_cells.set(position, cell.release_nonnull());
return IterationDecision::Continue;
});
return sheet;
}
JsonObject Sheet::to_json() const
{
JsonObject object;
object.set("name", m_name);
auto save_format = [](const auto& format, auto& obj) {
if (format.foreground_color.has_value())
obj.set("foreground_color", format.foreground_color.value().to_string());
if (format.background_color.has_value())
obj.set("background_color", format.background_color.value().to_string());
};
auto columns = JsonArray();
for (auto& column : m_columns)
columns.append(column);
object.set("columns", move(columns));
object.set("rows", m_rows);
JsonObject cells;
for (auto& it : m_cells) {
StringBuilder builder;
builder.append(it.key.column);
builder.appendff("{}", it.key.row);
auto key = builder.to_string();
JsonObject data;
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 = interpreter().global_object().get("JSON");
auto stringified = interpreter().vm().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);
}
// Set type & meta
auto& type = it.value->type();
auto& meta = it.value->type_metadata();
data.set("type", type.name());
JsonObject metadata_object;
metadata_object.set("length", meta.length);
metadata_object.set("format", meta.format);
#if 0
metadata_object.set("alignment", alignment_to_string(meta.alignment));
#endif
save_format(meta.static_format, metadata_object);
data.set("type_metadata", move(metadata_object));
// Set conditional formats
JsonArray conditional_formats;
for (auto& fmt : it.value->conditional_formats()) {
JsonObject fmt_object;
fmt_object.set("condition", fmt.condition);
save_format(fmt, fmt_object);
conditional_formats.append(move(fmt_object));
}
data.set("conditional_formats", move(conditional_formats));
auto& evaluated_formats = it.value->evaluated_formats();
JsonObject evaluated_formats_obj;
save_format(evaluated_formats, evaluated_formats_obj);
data.set("evaluated_formats", move(evaluated_formats_obj));
cells.set(key, move(data));
}
object.set("cells", move(cells));
return object;
}
JsonObject Sheet::gather_documentation() const
{
JsonObject object;
const JS::PropertyName doc_name { "__documentation" };
auto add_docs_from = [&](auto& it, auto& global_object) {
auto value = global_object.get(it.key);
if (!value.is_function() && !value.is_object())
return;
auto& value_object = value.is_object() ? value.as_object() : value.as_function();
if (!value_object.has_own_property(doc_name))
return;
dbgln("Found '{}'", it.key.to_display_string());
auto doc = value_object.get(doc_name);
if (!doc.is_string())
return;
JsonParser parser(doc.to_string_without_side_effects());
auto doc_object = parser.parse();
if (doc_object.has_value())
object.set(it.key.to_display_string(), doc_object.value());
else
dbgln("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;
}
}