From f606e7855641a0a257fda774b748ca08432bab64 Mon Sep 17 00:00:00 2001 From: AnotherTest Date: Mon, 28 Dec 2020 23:33:16 +0330 Subject: [PATCH] Spreadsheet: Show a small inline doc window for the "current" function If the user is typing in the cell editor and has the cursor in a function call, try to show a tip for the arguments of that function: (cursor denoted by `|`) ``` sum(| ``` should show: ``` sum(cell names) ``` in a tooltip-like window below the editor. --- Applications/Spreadsheet/JSIntegration.cpp | 73 +++++++++++++++++++ Applications/Spreadsheet/JSIntegration.h | 6 ++ Applications/Spreadsheet/Spreadsheet.cpp | 39 +++++++++- Applications/Spreadsheet/Spreadsheet.h | 3 + .../Spreadsheet/SpreadsheetWidget.cpp | 56 +++++++++++++- Applications/Spreadsheet/SpreadsheetWidget.h | 6 ++ 6 files changed, 180 insertions(+), 3 deletions(-) diff --git a/Applications/Spreadsheet/JSIntegration.cpp b/Applications/Spreadsheet/JSIntegration.cpp index 9c0ccfb8ab..a10d8a884d 100644 --- a/Applications/Spreadsheet/JSIntegration.cpp +++ b/Applications/Spreadsheet/JSIntegration.cpp @@ -27,6 +27,7 @@ #include "JSIntegration.h" #include "Spreadsheet.h" #include "Workbook.h" +#include #include #include #include @@ -34,6 +35,78 @@ namespace Spreadsheet { +Optional get_function_and_argument_index(StringView source) +{ + JS::Lexer lexer { source }; + // Track 's, and how many complete expressions are inside the parenthesised expression. + Vector state; + StringView last_name; + Vector names; + size_t open_parens_since_last_commit = 0; + size_t open_curlies_and_brackets_since_last_commit = 0; + bool previous_was_identifier = false; + auto token = lexer.next(); + while (token.type() != JS::TokenType::Eof) { + switch (token.type()) { + case JS::TokenType::Identifier: + previous_was_identifier = true; + last_name = token.value(); + break; + case JS::TokenType::ParenOpen: + if (!previous_was_identifier) { + open_parens_since_last_commit++; + break; + } + previous_was_identifier = false; + state.append(0); + names.append(last_name); + break; + case JS::TokenType::ParenClose: + previous_was_identifier = false; + if (open_parens_since_last_commit == 0) { + state.take_last(); + names.take_last(); + break; + } + --open_parens_since_last_commit; + break; + case JS::TokenType::Comma: + previous_was_identifier = false; + if (open_parens_since_last_commit == 0 && open_curlies_and_brackets_since_last_commit == 0) { + state.last()++; + break; + } + break; + case JS::TokenType::BracketOpen: + previous_was_identifier = false; + open_curlies_and_brackets_since_last_commit++; + break; + case JS::TokenType::BracketClose: + previous_was_identifier = false; + if (open_curlies_and_brackets_since_last_commit > 0) + open_curlies_and_brackets_since_last_commit--; + break; + case JS::TokenType::CurlyOpen: + previous_was_identifier = false; + open_curlies_and_brackets_since_last_commit++; + break; + case JS::TokenType::CurlyClose: + previous_was_identifier = false; + if (open_curlies_and_brackets_since_last_commit > 0) + open_curlies_and_brackets_since_last_commit--; + break; + default: + previous_was_identifier = false; + break; + } + + token = lexer.next(); + } + if (!names.is_empty() && !state.is_empty()) + return FunctionAndArgumentIndex { names.last(), state.last() }; + return {}; +} + SheetGlobalObject::SheetGlobalObject(Sheet& sheet) : m_sheet(sheet) { diff --git a/Applications/Spreadsheet/JSIntegration.h b/Applications/Spreadsheet/JSIntegration.h index 2aeece9a6b..88f46d929d 100644 --- a/Applications/Spreadsheet/JSIntegration.h +++ b/Applications/Spreadsheet/JSIntegration.h @@ -32,6 +32,12 @@ namespace Spreadsheet { +struct FunctionAndArgumentIndex { + String function_name; + size_t argument_index { 0 }; +}; +Optional get_function_and_argument_index(StringView source); + class SheetGlobalObject final : public JS::GlobalObject { JS_OBJECT(SheetGlobalObject, JS::GlobalObject); diff --git a/Applications/Spreadsheet/Spreadsheet.cpp b/Applications/Spreadsheet/Spreadsheet.cpp index f0f28b3e80..e01eb77781 100644 --- a/Applications/Spreadsheet/Spreadsheet.cpp +++ b/Applications/Spreadsheet/Spreadsheet.cpp @@ -691,7 +691,44 @@ JsonObject Sheet::gather_documentation() const for (auto& it : global_object().shape().property_table()) add_docs_from(it, global_object()); - return object; + m_cached_documentation = move(object); + return m_cached_documentation.value(); +} + +String Sheet::generate_inline_documentation_for(StringView function, size_t argument_index) +{ + if (!m_cached_documentation.has_value()) + gather_documentation(); + + auto& docs = m_cached_documentation.value(); + auto entry = docs.get(function); + if (entry.is_null() || !entry.is_object()) + return String::formatted("{}(...???{})", function, argument_index); + + auto& entry_object = entry.as_object(); + size_t argc = entry_object.get("argc").to_int(0); + auto argnames_value = entry_object.get("argnames"); + if (!argnames_value.is_array()) + return String::formatted("{}(...{}???{})", function, argc, argument_index); + auto& argnames = argnames_value.as_array(); + StringBuilder builder; + builder.appendff("{}(", function); + for (size_t i = 0; i < (size_t)argnames.size(); ++i) { + if (i != 0 && i < (size_t)argnames.size()) + builder.append(", "); + if (i == argument_index) + builder.append('<'); + else if (i >= argc) + builder.append('['); + builder.append(argnames[i].to_string()); + if (i == argument_index) + builder.append('>'); + else if (i >= argc) + builder.append(']'); + } + + builder.append(')'); + return builder.build(); } } diff --git a/Applications/Spreadsheet/Spreadsheet.h b/Applications/Spreadsheet/Spreadsheet.h index f260f645be..bbef6c8116 100644 --- a/Applications/Spreadsheet/Spreadsheet.h +++ b/Applications/Spreadsheet/Spreadsheet.h @@ -147,6 +147,8 @@ public: bool columns_are_standard() const; + String generate_inline_documentation_for(StringView function, size_t argument_index); + private: explicit Sheet(Workbook&); explicit Sheet(const StringView& name, Workbook&); @@ -165,6 +167,7 @@ private: HashTable m_visited_cells_in_update; bool m_should_ignore_updates { false }; bool m_update_requested { false }; + mutable Optional m_cached_documentation; }; } diff --git a/Applications/Spreadsheet/SpreadsheetWidget.cpp b/Applications/Spreadsheet/SpreadsheetWidget.cpp index fc78f578da..6260ed9ecf 100644 --- a/Applications/Spreadsheet/SpreadsheetWidget.cpp +++ b/Applications/Spreadsheet/SpreadsheetWidget.cpp @@ -80,6 +80,19 @@ SpreadsheetWidget::SpreadsheetWidget(NonnullRefPtrVector&& sheets, bool s m_cell_value_editor = cell_value_editor; m_current_cell_label = current_cell_label; + m_inline_documentation_window = GUI::Window::construct(window()); + m_inline_documentation_window->set_rect(m_cell_value_editor->rect().translated(0, m_cell_value_editor->height() + 7).inflated(6, 6)); + m_inline_documentation_window->set_window_type(GUI::WindowType::Tooltip); + m_inline_documentation_window->set_resizable(false); + auto& inline_widget = m_inline_documentation_window->set_main_widget(); + inline_widget.set_fill_with_background_color(true); + inline_widget.set_layout().set_margins({ 4, 4, 4, 4 }); + inline_widget.set_frame_shape(Gfx::FrameShape::Box); + m_inline_documentation_label = inline_widget.add(); + m_inline_documentation_label->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fill); + m_inline_documentation_label->set_fill_with_background_color(true); + m_inline_documentation_label->set_autosize(false); + m_inline_documentation_label->set_text_alignment(Gfx::TextAlignment::CenterLeft); if (!m_workbook->has_sheets() && should_add_sheet_if_empty) m_workbook->add_sheet("Sheet 1"); @@ -109,6 +122,13 @@ SpreadsheetWidget::SpreadsheetWidget(NonnullRefPtrVector&& sheets, bool s setup_tabs(m_workbook->sheets()); } +void SpreadsheetWidget::resize_event(GUI::ResizeEvent& event) +{ + GUI::Widget::resize_event(event); + if (m_inline_documentation_window && m_cell_value_editor && window()) + m_inline_documentation_window->set_rect(m_cell_value_editor->screen_relative_rect().translated(0, m_cell_value_editor->height() + 7).inflated(6, 6)); +} + void SpreadsheetWidget::setup_tabs(NonnullRefPtrVector new_sheets) { RefPtr first_tab_widget; @@ -137,7 +157,11 @@ void SpreadsheetWidget::setup_tabs(NonnullRefPtrVector new_sheets) m_cell_value_editor->on_change = nullptr; m_cell_value_editor->set_text(cell.source()); m_cell_value_editor->on_change = [&] { - cell.set_data(m_cell_value_editor->text()); + auto text = m_cell_value_editor->text(); + // FIXME: Lines? + auto offset = m_cell_value_editor->cursor().column(); + try_generate_tip_for_input_expression(text, offset); + cell.set_data(move(text)); m_selected_view->sheet().update(); update(); }; @@ -163,8 +187,12 @@ void SpreadsheetWidget::setup_tabs(NonnullRefPtrVector new_sheets) m_cell_value_editor->on_focusout = [this] { m_should_change_selected_cells = false; }; m_cell_value_editor->on_change = [cells = move(cells), this] { if (m_should_change_selected_cells) { + auto text = m_cell_value_editor->text(); + // FIXME: Lines? + auto offset = m_cell_value_editor->cursor().column(); + try_generate_tip_for_input_expression(text, offset); for (auto* cell : cells) - cell->set_data(m_cell_value_editor->text()); + cell->set_data(text); m_selected_view->sheet().update(); update(); } @@ -194,6 +222,30 @@ void SpreadsheetWidget::setup_tabs(NonnullRefPtrVector new_sheets) }; } +void SpreadsheetWidget::try_generate_tip_for_input_expression(StringView source, size_t cursor_offset) +{ + m_inline_documentation_window->set_rect(m_cell_value_editor->screen_relative_rect().translated(0, m_cell_value_editor->height() + 7).inflated(6, 6)); + if (!m_selected_view || !source.starts_with('=')) { + m_inline_documentation_window->hide(); + return; + } + auto maybe_function_and_argument = get_function_and_argument_index(source.substring_view(0, cursor_offset)); + if (!maybe_function_and_argument.has_value()) { + m_inline_documentation_window->hide(); + return; + } + + auto& [name, index] = maybe_function_and_argument.value(); + auto& sheet = m_selected_view->sheet(); + auto text = sheet.generate_inline_documentation_for(name, index); + if (text.is_empty()) { + m_inline_documentation_window->hide(); + } else { + m_inline_documentation_label->set_text(move(text)); + m_inline_documentation_window->show(); + } +} + void SpreadsheetWidget::save(const StringView& filename) { auto result = m_workbook->save(filename); diff --git a/Applications/Spreadsheet/SpreadsheetWidget.h b/Applications/Spreadsheet/SpreadsheetWidget.h index c13531fc2a..b29fc557d6 100644 --- a/Applications/Spreadsheet/SpreadsheetWidget.h +++ b/Applications/Spreadsheet/SpreadsheetWidget.h @@ -62,13 +62,19 @@ public: } private: + virtual void resize_event(GUI::ResizeEvent&) override; + explicit SpreadsheetWidget(NonnullRefPtrVector&& sheets = {}, bool should_add_sheet_if_empty = true); void setup_tabs(NonnullRefPtrVector new_sheets); + void try_generate_tip_for_input_expression(StringView source, size_t offset); + SpreadsheetView* m_selected_view { nullptr }; RefPtr m_current_cell_label; RefPtr m_cell_value_editor; + RefPtr m_inline_documentation_window; + RefPtr m_inline_documentation_label; RefPtr m_tab_widget; RefPtr m_tab_context_menu; RefPtr m_tab_context_menu_sheet_view;