mirror of
https://github.com/SerenityOS/serenity.git
synced 2025-01-23 18:02:05 -05:00
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.
This commit is contained in:
parent
aa5b43a2bc
commit
f606e78556
6 changed files with 180 additions and 3 deletions
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue