diff --git a/Userland/Applications/Spreadsheet/Cell.h b/Userland/Applications/Spreadsheet/Cell.h index 8395b25710..0707a557a5 100644 --- a/Userland/Applications/Spreadsheet/Cell.h +++ b/Userland/Applications/Spreadsheet/Cell.h @@ -50,6 +50,15 @@ struct Cell : public Weakable { bool dirty() const { return m_dirty; } void clear_dirty() { m_dirty = false; } + StringView name_for_javascript(Sheet const& sheet) const + { + if (!m_name_for_javascript.is_empty()) + return m_name_for_javascript; + + m_name_for_javascript = String::formatted("cell {}", m_position.to_cell_identifier(sheet)); + return m_name_for_javascript; + } + void set_thrown_value(JS::Value value) { m_thrown_value = value; } Optional thrown_value() const { @@ -116,6 +125,7 @@ private: CellType const* m_type { nullptr }; CellTypeMetadata m_type_metadata; Position m_position; + mutable String m_name_for_javascript; Vector m_conditional_formats; Format m_evaluated_formats; diff --git a/Userland/Applications/Spreadsheet/Spreadsheet.cpp b/Userland/Applications/Spreadsheet/Spreadsheet.cpp index 995a1be233..1cebe1a055 100644 --- a/Userland/Applications/Spreadsheet/Spreadsheet.cpp +++ b/Userland/Applications/Spreadsheet/Spreadsheet.cpp @@ -165,8 +165,12 @@ void Sheet::update(Cell& cell) JS::ThrowCompletionOr Sheet::evaluate(StringView source, Cell* on_behalf_of) { TemporaryChange cell_change { m_current_cell_being_evaluated, on_behalf_of }; + auto name = on_behalf_of ? on_behalf_of->name_for_javascript(*this) : "cell "sv; + auto script_or_error = JS::Script::parse( + source, + interpreter().realm(), + name); - auto script_or_error = JS::Script::parse(source, interpreter().realm()); if (script_or_error.is_error()) return interpreter().vm().throw_completion(interpreter().global_object(), script_or_error.error().first().to_string()); diff --git a/Userland/Applications/Spreadsheet/SpreadsheetModel.cpp b/Userland/Applications/Spreadsheet/SpreadsheetModel.cpp index e643f0cf95..3d496c6ad3 100644 --- a/Userland/Applications/Spreadsheet/SpreadsheetModel.cpp +++ b/Userland/Applications/Spreadsheet/SpreadsheetModel.cpp @@ -103,6 +103,36 @@ GUI::Variant SheetModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) return {}; } + if (to_underlying(role) == to_underlying(Role::Tooltip)) { + auto const* cell = m_sheet->at({ (size_t)index.column(), (size_t)index.row() }); + if (!cell || !cell->thrown_value().has_value()) + return {}; + + auto value = cell->thrown_value().value(); + if (!value.is_object()) + return {}; + + auto& object = value.as_object(); + if (!is(object)) + return {}; + + auto& error = static_cast(object); + auto const& trace = error.traceback(); + StringBuilder builder; + builder.appendff("{}\n", error.get_without_side_effects(object.vm().names.message).to_string_without_side_effects()); + for (auto const& frame : trace.in_reverse()) { + if (frame.source_range.filename.contains("runtime.js")) { + if (frame.function_name == "") + builder.appendff(" in a builtin function at line {}, column {}\n", frame.source_range.start.line, frame.source_range.start.column); + else + builder.appendff(" while evaluating builtin '{}'\n", frame.function_name); + } else if (frame.source_range.filename.starts_with("cell ")) { + builder.appendff(" in cell '{}', at line {}, column {}\n", frame.source_range.filename.substring_view(5), frame.source_range.start.line, frame.source_range.start.column); + } + } + return builder.to_string(); + } + return {}; } diff --git a/Userland/Applications/Spreadsheet/SpreadsheetModel.h b/Userland/Applications/Spreadsheet/SpreadsheetModel.h index 1177e480b2..a54b210405 100644 --- a/Userland/Applications/Spreadsheet/SpreadsheetModel.h +++ b/Userland/Applications/Spreadsheet/SpreadsheetModel.h @@ -13,6 +13,11 @@ namespace Spreadsheet { class SheetModel final : public GUI::Model { public: + enum class Role : UnderlyingType { + _Custom = to_underlying(GUI::ModelRole::Custom), + Tooltip, + }; + static NonnullRefPtr create(Sheet& sheet) { return adopt_ref(*new SheetModel(sheet)); } virtual ~SheetModel() override = default; diff --git a/Userland/Applications/Spreadsheet/SpreadsheetView.cpp b/Userland/Applications/Spreadsheet/SpreadsheetView.cpp index 8205b4dfa7..ff36a3eccc 100644 --- a/Userland/Applications/Spreadsheet/SpreadsheetView.cpp +++ b/Userland/Applications/Spreadsheet/SpreadsheetView.cpp @@ -77,6 +77,16 @@ void InfinitelyScrollableTableView::mousemove_event(GUI::MouseEvent& event) sheet.disable_updates(); ScopeGuard sheet_update_enabler { [&] { sheet.enable_updates(); } }; + if (!is_dragging()) { + auto tooltip = model->data(index, static_cast(SheetModel::Role::Tooltip)); + if (tooltip.is_string()) { + set_tooltip(tooltip.as_string()); + show_or_hide_tooltip(); + } else { + set_tooltip({}); + } + } + m_is_hovering_cut_zone = false; m_is_hovering_extend_zone = false; if (selection().size() > 0 && !m_is_dragging_for_select) { diff --git a/Userland/Applications/Spreadsheet/SpreadsheetView.h b/Userland/Applications/Spreadsheet/SpreadsheetView.h index 2723ed32a7..b6b629e8d4 100644 --- a/Userland/Applications/Spreadsheet/SpreadsheetView.h +++ b/Userland/Applications/Spreadsheet/SpreadsheetView.h @@ -78,6 +78,8 @@ private: virtual void mouseup_event(GUI::MouseEvent&) override; virtual void drop_event(GUI::DropEvent&) override; + bool is_dragging() const { return m_is_dragging_for_cut || m_is_dragging_for_extend || m_is_dragging_for_select; } + bool m_is_hovering_extend_zone { false }; bool m_is_hovering_cut_zone { false }; bool m_is_dragging_for_select { false };