Browse Source

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.
AnotherTest 4 năm trước cách đây
mục cha
commit
f606e78556

+ 73 - 0
Applications/Spreadsheet/JSIntegration.cpp

@@ -27,6 +27,7 @@
 #include "JSIntegration.h"
 #include "Spreadsheet.h"
 #include "Workbook.h"
+#include <LibJS/Lexer.h>
 #include <LibJS/Runtime/Error.h>
 #include <LibJS/Runtime/GlobalObject.h>
 #include <LibJS/Runtime/Object.h>
@@ -34,6 +35,78 @@
 
 namespace Spreadsheet {
 
+Optional<FunctionAndArgumentIndex> get_function_and_argument_index(StringView source)
+{
+    JS::Lexer lexer { source };
+    // Track <identifier> <OpenParen>'s, and how many complete expressions are inside the parenthesised expression.
+    Vector<size_t> state;
+    StringView last_name;
+    Vector<StringView> 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)
 {

+ 6 - 0
Applications/Spreadsheet/JSIntegration.h

@@ -32,6 +32,12 @@
 
 namespace Spreadsheet {
 
+struct FunctionAndArgumentIndex {
+    String function_name;
+    size_t argument_index { 0 };
+};
+Optional<FunctionAndArgumentIndex> get_function_and_argument_index(StringView source);
+
 class SheetGlobalObject final : public JS::GlobalObject {
     JS_OBJECT(SheetGlobalObject, JS::GlobalObject);
 

+ 38 - 1
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();
 }
 
 }

+ 3 - 0
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<Cell*> m_visited_cells_in_update;
     bool m_should_ignore_updates { false };
     bool m_update_requested { false };
+    mutable Optional<JsonObject> m_cached_documentation;
 };
 
 }

+ 54 - 2
Applications/Spreadsheet/SpreadsheetWidget.cpp

@@ -80,6 +80,19 @@ SpreadsheetWidget::SpreadsheetWidget(NonnullRefPtrVector<Sheet>&& 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<GUI::Frame>();
+    inline_widget.set_fill_with_background_color(true);
+    inline_widget.set_layout<GUI::VerticalBoxLayout>().set_margins({ 4, 4, 4, 4 });
+    inline_widget.set_frame_shape(Gfx::FrameShape::Box);
+    m_inline_documentation_label = inline_widget.add<GUI::Label>();
+    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<Sheet>&& 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<Sheet> new_sheets)
 {
     RefPtr<GUI::Widget> first_tab_widget;
@@ -137,7 +157,11 @@ void SpreadsheetWidget::setup_tabs(NonnullRefPtrVector<Sheet> 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<Sheet> 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<Sheet> 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);

+ 6 - 0
Applications/Spreadsheet/SpreadsheetWidget.h

@@ -62,13 +62,19 @@ public:
     }
 
 private:
+    virtual void resize_event(GUI::ResizeEvent&) override;
+
     explicit SpreadsheetWidget(NonnullRefPtrVector<Sheet>&& sheets = {}, bool should_add_sheet_if_empty = true);
 
     void setup_tabs(NonnullRefPtrVector<Sheet> new_sheets);
 
+    void try_generate_tip_for_input_expression(StringView source, size_t offset);
+
     SpreadsheetView* m_selected_view { nullptr };
     RefPtr<GUI::Label> m_current_cell_label;
     RefPtr<GUI::TextEditor> m_cell_value_editor;
+    RefPtr<GUI::Window> m_inline_documentation_window;
+    RefPtr<GUI::Label> m_inline_documentation_label;
     RefPtr<GUI::TabWidget> m_tab_widget;
     RefPtr<GUI::Menu> m_tab_context_menu;
     RefPtr<SpreadsheetView> m_tab_context_menu_sheet_view;