1
0
Fork 0
mirror of https://github.com/LadybirdBrowser/ladybird.git synced 2025-06-09 09:34:57 +09:00
ladybird/Applications/Spreadsheet/Spreadsheet.cpp
Tom 75f61fe3d9 AK: Make RefPtr, NonnullRefPtr, WeakPtr thread safe
This makes most operations thread safe, especially so that they
can safely be used in the Kernel. This includes obtaining a strong
reference from a weak reference, which now requires an explicit
call to WeakPtr::strong_ref(). Another major change is that
Weakable::make_weak_ref() may require the explicit target type.
Previously we used reinterpret_cast in WeakPtr, assuming that it
can be properly converted. But WeakPtr does not necessarily have
the knowledge to be able to do this. Instead, we now ask the class
itself to deliver a WeakPtr to the type that we want.

Also, WeakLink is no longer specific to a target type. The reason
for this is that we want to be able to safely convert e.g. WeakPtr<T>
to WeakPtr<U>, and before this we just reinterpret_cast the internal
WeakLink<T> to WeakLink<U>, which is a bold assumption that it would
actually produce the correct code. Instead, WeakLink now operates
on just a raw pointer and we only make those constructors/operators
available if we can verify that it can be safely cast.

In order to guarantee thread safety, we now use the least significant
bit in the pointer for locking purposes. This also means that only
properly aligned pointers can be used.
2020-11-10 19:11:52 +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);
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);
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;
}
}