ConsoleClient.cpp 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. /*
  2. * Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <AK/StringBuilder.h>
  7. #include <LibJS/MarkupGenerator.h>
  8. #include <LibWebView/ConsoleClient.h>
  9. #include <LibWebView/ViewImplementation.h>
  10. namespace WebView {
  11. static constexpr auto CONSOLE_HTML = "data:text/html,<html style=\"font: 10pt monospace;\"></html>"sv;
  12. // FIXME: It should be sufficient to scrollTo a y value of document.documentElement.offsetHeight,
  13. // but due to an unknown bug offsetHeight seems to not be properly updated after spamming
  14. // a lot of document changes.
  15. //
  16. // The setTimeout makes the scrollTo async and allows the DOM to be updated.
  17. static constexpr auto SCROLL_TO_BOTTOM = "setTimeout(function() { window.scrollTo(0, 1_000_000_000); }, 0);"sv;
  18. ConsoleClient::ConsoleClient(ViewImplementation& content_web_view, ViewImplementation& console_web_view)
  19. : m_content_web_view(content_web_view)
  20. , m_console_web_view(console_web_view)
  21. {
  22. m_content_web_view.on_received_console_message = [this](auto message_index) {
  23. handle_console_message(message_index);
  24. };
  25. m_content_web_view.on_received_console_messages = [this](auto start_index, auto const& message_types, auto const& messages) {
  26. handle_console_messages(start_index, message_types, messages);
  27. };
  28. // Wait until our output WebView is loaded, and then request any messages that occurred before we existed.
  29. m_console_web_view.on_load_finish = [this](auto const&) {
  30. m_content_web_view.js_console_request_messages(0);
  31. };
  32. m_console_web_view.use_native_user_style_sheet();
  33. m_console_web_view.load(CONSOLE_HTML);
  34. }
  35. ConsoleClient::~ConsoleClient()
  36. {
  37. m_content_web_view.on_received_console_message = nullptr;
  38. m_content_web_view.on_received_console_messages = nullptr;
  39. }
  40. void ConsoleClient::execute(String script)
  41. {
  42. print_source(script);
  43. m_content_web_view.js_console_input(script.to_deprecated_string());
  44. if (m_history.is_empty() || m_history.last() != script) {
  45. m_history.append(move(script));
  46. m_history_index = m_history.size();
  47. }
  48. }
  49. Optional<String> ConsoleClient::previous_history_item()
  50. {
  51. if (m_history_index == 0)
  52. return {};
  53. --m_history_index;
  54. return m_history.at(m_history_index);
  55. }
  56. Optional<String> ConsoleClient::next_history_item()
  57. {
  58. if (m_history.is_empty())
  59. return {};
  60. auto last_index = m_history.size() - 1;
  61. if (m_history_index < last_index) {
  62. ++m_history_index;
  63. return m_history.at(m_history_index);
  64. }
  65. if (m_history_index == last_index) {
  66. ++m_history_index;
  67. return String {};
  68. }
  69. return {};
  70. }
  71. void ConsoleClient::clear()
  72. {
  73. m_console_web_view.run_javascript("document.body.innerHTML = \"\";"sv);
  74. m_group_stack.clear();
  75. }
  76. void ConsoleClient::reset()
  77. {
  78. clear();
  79. m_highest_notified_message_index = -1;
  80. m_highest_received_message_index = -1;
  81. m_waiting_for_messages = false;
  82. }
  83. void ConsoleClient::handle_console_message(i32 message_index)
  84. {
  85. if (message_index <= m_highest_received_message_index) {
  86. dbgln("Notified about console message we already have");
  87. return;
  88. }
  89. if (message_index <= m_highest_notified_message_index) {
  90. dbgln("Notified about console message we're already aware of");
  91. return;
  92. }
  93. m_highest_notified_message_index = message_index;
  94. if (!m_waiting_for_messages)
  95. request_console_messages();
  96. }
  97. void ConsoleClient::handle_console_messages(i32 start_index, ReadonlySpan<DeprecatedString> message_types, ReadonlySpan<DeprecatedString> messages)
  98. {
  99. auto end_index = start_index + static_cast<i32>(message_types.size()) - 1;
  100. if (end_index <= m_highest_received_message_index) {
  101. dbgln("Received old console messages");
  102. return;
  103. }
  104. for (size_t i = 0; i < message_types.size(); ++i) {
  105. auto const& type = message_types[i];
  106. auto const& message = messages[i];
  107. if (type == "html"sv)
  108. print_html(message);
  109. else if (type == "clear"sv)
  110. clear();
  111. else if (type == "group"sv)
  112. begin_group(message, true);
  113. else if (type == "groupCollapsed"sv)
  114. begin_group(message, false);
  115. else if (type == "groupEnd"sv)
  116. end_group();
  117. else
  118. VERIFY_NOT_REACHED();
  119. }
  120. m_highest_received_message_index = end_index;
  121. m_waiting_for_messages = false;
  122. if (m_highest_received_message_index < m_highest_notified_message_index)
  123. request_console_messages();
  124. }
  125. void ConsoleClient::print_source(StringView source)
  126. {
  127. StringBuilder builder;
  128. builder.append("<span class=\"repl-indicator\">&gt; </span>"sv);
  129. builder.append(MUST(JS::MarkupGenerator::html_from_source(source)));
  130. print_html(builder.string_view());
  131. }
  132. void ConsoleClient::print_html(StringView html)
  133. {
  134. StringBuilder builder;
  135. if (m_group_stack.is_empty())
  136. builder.append("var parentGroup = document.body;"sv);
  137. else
  138. builder.appendff("var parentGroup = document.getElementById(\"group_{}\");", m_group_stack.last().id);
  139. builder.append(R"~~~(
  140. var p = document.createElement("p");
  141. p.innerHTML = ")~~~"sv);
  142. builder.append_escaped_for_json(html);
  143. builder.append(R"~~~("
  144. parentGroup.appendChild(p);
  145. )~~~"sv);
  146. builder.append(SCROLL_TO_BOTTOM);
  147. m_console_web_view.run_javascript(builder.string_view());
  148. }
  149. void ConsoleClient::request_console_messages()
  150. {
  151. VERIFY(!m_waiting_for_messages);
  152. m_content_web_view.js_console_request_messages(m_highest_received_message_index + 1);
  153. m_waiting_for_messages = true;
  154. }
  155. void ConsoleClient::begin_group(StringView label, bool start_expanded)
  156. {
  157. StringBuilder builder;
  158. if (m_group_stack.is_empty())
  159. builder.append("var parentGroup = document.body;"sv);
  160. else
  161. builder.appendff("var parentGroup = document.getElementById(\"group_{}\");", m_group_stack.last().id);
  162. Group group;
  163. group.id = m_next_group_id++;
  164. group.label = label;
  165. builder.appendff(R"~~~(
  166. var group = document.createElement("details");
  167. group.id = "group_{}";
  168. var label = document.createElement("summary");
  169. label.innerHTML = ")~~~",
  170. group.id);
  171. builder.append_escaped_for_json(label);
  172. builder.append(R"~~~(";
  173. group.appendChild(label);
  174. parentGroup.appendChild(group);
  175. )~~~"sv);
  176. if (start_expanded)
  177. builder.append("group.open = true;"sv);
  178. builder.append(SCROLL_TO_BOTTOM);
  179. m_console_web_view.run_javascript(builder.string_view());
  180. m_group_stack.append(group);
  181. }
  182. void ConsoleClient::end_group()
  183. {
  184. m_group_stack.take_last();
  185. }
  186. }