Editor.cpp 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072
  1. /*
  2. * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
  3. * All rights reserved.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. *
  8. * 1. Redistributions of source code must retain the above copyright notice, this
  9. * list of conditions and the following disclaimer.
  10. *
  11. * 2. Redistributions in binary form must reproduce the above copyright notice,
  12. * this list of conditions and the following disclaimer in the documentation
  13. * and/or other materials provided with the distribution.
  14. *
  15. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  16. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  17. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  18. * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
  19. * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  20. * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  21. * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  22. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  23. * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  24. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  25. */
  26. #include "Editor.h"
  27. #include <AK/StringBuilder.h>
  28. #include <ctype.h>
  29. #include <stdio.h>
  30. #include <sys/ioctl.h>
  31. #include <unistd.h>
  32. namespace Line {
  33. Editor::Editor(bool always_refresh)
  34. {
  35. m_always_refresh = always_refresh;
  36. m_pending_chars = ByteBuffer::create_uninitialized(0);
  37. struct winsize ws;
  38. if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) < 0) {
  39. m_num_columns = 80;
  40. m_num_lines = 25;
  41. } else {
  42. m_num_columns = ws.ws_col;
  43. m_num_lines = ws.ws_row;
  44. }
  45. }
  46. Editor::~Editor()
  47. {
  48. if (m_initialized)
  49. tcsetattr(0, TCSANOW, &m_default_termios);
  50. }
  51. void Editor::add_to_history(const String& line)
  52. {
  53. if ((m_history.size() + 1) > m_history_capacity)
  54. m_history.take_first();
  55. m_history.append(line);
  56. }
  57. void Editor::clear_line()
  58. {
  59. for (size_t i = 0; i < m_cursor; ++i)
  60. fputc(0x8, stdout);
  61. fputs("\033[K", stdout);
  62. fflush(stdout);
  63. m_buffer.clear();
  64. m_cursor = 0;
  65. m_inline_search_cursor = m_cursor;
  66. }
  67. void Editor::insert(const String& string)
  68. {
  69. for (auto ch : string)
  70. insert(ch);
  71. }
  72. void Editor::insert(const char ch)
  73. {
  74. m_pending_chars.append(&ch, 1);
  75. if (m_cursor == m_buffer.size()) {
  76. m_buffer.append(ch);
  77. m_cursor = m_buffer.size();
  78. m_inline_search_cursor = m_cursor;
  79. return;
  80. }
  81. m_buffer.insert(m_cursor, ch);
  82. ++m_chars_inserted_in_the_middle;
  83. ++m_cursor;
  84. m_inline_search_cursor = m_cursor;
  85. }
  86. void Editor::register_character_input_callback(char ch, Function<bool(Editor&)> callback)
  87. {
  88. if (m_key_callbacks.contains(ch)) {
  89. dbg() << "Key callback registered twice for " << ch;
  90. ASSERT_NOT_REACHED();
  91. }
  92. m_key_callbacks.set(ch, make<KeyCallback>(move(callback)));
  93. }
  94. void Editor::stylize(const Span& span, const Style& style)
  95. {
  96. auto starting_map = m_spans_starting.get(span.beginning()).value_or({});
  97. if (!starting_map.contains(span.end()))
  98. m_refresh_needed = true;
  99. starting_map.set(span.end(), style);
  100. m_spans_starting.set(span.beginning(), starting_map);
  101. auto ending_map = m_spans_ending.get(span.end()).value_or({});
  102. if (!ending_map.contains(span.beginning()))
  103. m_refresh_needed = true;
  104. ending_map.set(span.beginning(), style);
  105. m_spans_ending.set(span.end(), ending_map);
  106. }
  107. String Editor::get_line(const String& prompt)
  108. {
  109. m_is_editing = true;
  110. set_prompt(prompt);
  111. reset();
  112. set_origin();
  113. m_history_cursor = m_history.size();
  114. for (;;) {
  115. if (m_always_refresh)
  116. m_refresh_needed = true;
  117. refresh_display();
  118. if (m_finish) {
  119. m_finish = false;
  120. printf("\n");
  121. fflush(stdout);
  122. auto string = String::copy(m_buffer);
  123. m_buffer.clear();
  124. m_is_editing = false;
  125. return string;
  126. }
  127. char keybuf[16];
  128. ssize_t nread = read(0, keybuf, sizeof(keybuf));
  129. // FIXME: exit()ing here is a bit off. Should communicate failure to caller somehow instead.
  130. if (nread == 0)
  131. exit(0);
  132. if (nread < 0) {
  133. if (errno == EINTR) {
  134. if (!m_was_interrupted) {
  135. if (m_was_resized)
  136. continue;
  137. finish();
  138. continue;
  139. }
  140. m_was_interrupted = false;
  141. if (!m_buffer.is_empty())
  142. printf("^C");
  143. m_buffer.clear();
  144. m_cursor = 0;
  145. m_refresh_needed = true;
  146. continue;
  147. }
  148. perror("read failed");
  149. // FIXME: exit()ing here is a bit off. Should communicate failure to caller somehow instead.
  150. exit(2);
  151. }
  152. auto reverse_tab = false;
  153. auto increment_suggestion_index = [&] {
  154. if (m_suggestions.size())
  155. m_next_suggestion_index = (m_next_suggestion_index + 1) % m_suggestions.size();
  156. else
  157. m_next_suggestion_index = 0;
  158. };
  159. auto decrement_suggestion_index = [&] {
  160. if (m_next_suggestion_index == 0)
  161. m_next_suggestion_index = m_suggestions.size();
  162. m_next_suggestion_index--;
  163. };
  164. auto ctrl_held = false;
  165. for (ssize_t i = 0; i < nread; ++i) {
  166. char ch = keybuf[i];
  167. if (ch == 0)
  168. continue;
  169. switch (m_state) {
  170. case InputState::ExpectBracket:
  171. if (ch == '[') {
  172. m_state = InputState::ExpectFinal;
  173. continue;
  174. } else {
  175. m_state = InputState::Free;
  176. break;
  177. }
  178. case InputState::ExpectFinal:
  179. switch (ch) {
  180. case 'O': // mod_ctrl
  181. ctrl_held = true;
  182. continue;
  183. case 'A': // up
  184. {
  185. m_searching_backwards = true;
  186. auto inline_search_cursor = m_inline_search_cursor;
  187. String search_phrase { m_buffer.data(), inline_search_cursor };
  188. if (search(search_phrase, true, true)) {
  189. ++m_search_offset;
  190. } else {
  191. insert(search_phrase);
  192. }
  193. m_inline_search_cursor = inline_search_cursor;
  194. m_state = InputState::Free;
  195. ctrl_held = false;
  196. continue;
  197. }
  198. case 'B': // down
  199. {
  200. auto inline_search_cursor = m_inline_search_cursor;
  201. String search_phrase { m_buffer.data(), inline_search_cursor };
  202. auto search_changed_directions = m_searching_backwards;
  203. m_searching_backwards = false;
  204. if (m_search_offset > 0) {
  205. m_search_offset -= 1 + search_changed_directions;
  206. if (!search(search_phrase, true, true)) {
  207. insert(search_phrase);
  208. }
  209. } else {
  210. m_search_offset = 0;
  211. m_cursor = 0;
  212. m_buffer.clear();
  213. insert(search_phrase);
  214. m_refresh_needed = true;
  215. }
  216. m_inline_search_cursor = inline_search_cursor;
  217. m_state = InputState::Free;
  218. ctrl_held = false;
  219. continue;
  220. }
  221. case 'D': // left
  222. if (m_cursor > 0) {
  223. if (ctrl_held) {
  224. auto skipped_at_least_one_character = false;
  225. for (;;) {
  226. if (m_cursor == 0)
  227. break;
  228. if (skipped_at_least_one_character && isspace(m_buffer[m_cursor - 1])) // stop *after* a space, but only if it changes the position
  229. break;
  230. skipped_at_least_one_character = true;
  231. --m_cursor;
  232. }
  233. } else {
  234. --m_cursor;
  235. }
  236. }
  237. m_inline_search_cursor = m_cursor;
  238. m_state = InputState::Free;
  239. ctrl_held = false;
  240. continue;
  241. case 'C': // right
  242. if (m_cursor < m_buffer.size()) {
  243. if (ctrl_held) {
  244. // temporarily put a space at the end of our buffer
  245. // this greatly simplifies the logic below
  246. m_buffer.append(' ');
  247. for (;;) {
  248. if (m_cursor >= m_buffer.size())
  249. break;
  250. if (isspace(m_buffer[++m_cursor]))
  251. break;
  252. }
  253. m_buffer.take_last();
  254. } else {
  255. ++m_cursor;
  256. }
  257. }
  258. m_inline_search_cursor = m_cursor;
  259. m_search_offset = 0;
  260. m_state = InputState::Free;
  261. ctrl_held = false;
  262. continue;
  263. case 'H':
  264. m_cursor = 0;
  265. m_inline_search_cursor = m_cursor;
  266. m_search_offset = 0;
  267. m_state = InputState::Free;
  268. ctrl_held = false;
  269. continue;
  270. case 'F':
  271. m_cursor = m_buffer.size();
  272. m_state = InputState::Free;
  273. m_inline_search_cursor = m_cursor;
  274. m_search_offset = 0;
  275. ctrl_held = false;
  276. continue;
  277. case 'Z': // shift+tab
  278. reverse_tab = true;
  279. m_state = InputState::Free;
  280. ctrl_held = false;
  281. break;
  282. case '3':
  283. if (m_cursor == m_buffer.size()) {
  284. fputc('\a', stdout);
  285. fflush(stdout);
  286. continue;
  287. }
  288. m_buffer.remove(m_cursor);
  289. m_refresh_needed = true;
  290. m_search_offset = 0;
  291. m_state = InputState::ExpectTerminator;
  292. ctrl_held = false;
  293. continue;
  294. default:
  295. dbgprintf("Shell: Unhandled final: %02x (%c)\r\n", ch, ch);
  296. m_state = InputState::Free;
  297. ctrl_held = false;
  298. continue;
  299. }
  300. break;
  301. case InputState::ExpectTerminator:
  302. m_state = InputState::Free;
  303. continue;
  304. case InputState::Free:
  305. if (ch == 27) {
  306. m_state = InputState::ExpectBracket;
  307. continue;
  308. }
  309. break;
  310. }
  311. auto cb = m_key_callbacks.get(ch);
  312. if (cb.has_value()) {
  313. if (!cb.value()->callback(*this)) {
  314. continue;
  315. }
  316. }
  317. m_search_offset = 0; // reset search offset on any key
  318. if (ch == '\t' || reverse_tab) {
  319. if (!on_tab_complete_first_token || !on_tab_complete_other_token)
  320. continue;
  321. bool is_empty_token = m_cursor == 0 || m_buffer[m_cursor - 1] == ' ';
  322. // reverse tab can count as regular tab here
  323. m_times_tab_pressed++;
  324. int token_start = m_cursor - 1;
  325. if (!is_empty_token) {
  326. while (token_start >= 0 && m_buffer[token_start] != ' ')
  327. --token_start;
  328. ++token_start;
  329. }
  330. bool is_first_token = true;
  331. for (int i = token_start - 1; i >= 0; --i) {
  332. if (m_buffer[i] != ' ') {
  333. is_first_token = false;
  334. break;
  335. }
  336. }
  337. String token = is_empty_token ? String() : String(&m_buffer[token_start], m_cursor - token_start);
  338. // ask for completions only on the first tab
  339. // and scan for the largest common prefix to display
  340. // further tabs simply show the cached completions
  341. if (m_times_tab_pressed == 1) {
  342. if (is_first_token)
  343. m_suggestions = on_tab_complete_first_token(token);
  344. else
  345. m_suggestions = on_tab_complete_other_token(token);
  346. size_t common_suggestion_prefix { 0 };
  347. if (m_suggestions.size() == 1) {
  348. m_largest_common_suggestion_prefix_length = m_suggestions[0].text.length();
  349. } else if (m_suggestions.size()) {
  350. char last_valid_suggestion_char;
  351. for (;; ++common_suggestion_prefix) {
  352. if (m_suggestions[0].text.length() <= common_suggestion_prefix)
  353. goto no_more_commons;
  354. last_valid_suggestion_char = m_suggestions[0].text[common_suggestion_prefix];
  355. for (const auto& suggestion : m_suggestions) {
  356. if (suggestion.text.length() <= common_suggestion_prefix || suggestion.text[common_suggestion_prefix] != last_valid_suggestion_char) {
  357. goto no_more_commons;
  358. }
  359. }
  360. }
  361. no_more_commons:;
  362. m_largest_common_suggestion_prefix_length = common_suggestion_prefix;
  363. } else {
  364. m_largest_common_suggestion_prefix_length = 0;
  365. // there are no suggestions, beep~
  366. putchar('\a');
  367. fflush(stdout);
  368. }
  369. m_prompt_lines_at_suggestion_initiation = num_lines();
  370. }
  371. // Adjust already incremented / decremented index when switching tab direction
  372. if (reverse_tab && m_tab_direction != TabDirection::Backward) {
  373. decrement_suggestion_index();
  374. decrement_suggestion_index();
  375. m_tab_direction = TabDirection::Backward;
  376. }
  377. if (!reverse_tab && m_tab_direction != TabDirection::Forward) {
  378. increment_suggestion_index();
  379. increment_suggestion_index();
  380. m_tab_direction = TabDirection::Forward;
  381. }
  382. reverse_tab = false;
  383. auto current_suggestion_index = m_next_suggestion_index;
  384. if (m_next_suggestion_index < m_suggestions.size()) {
  385. auto can_complete = m_next_suggestion_invariant_offset <= m_largest_common_suggestion_prefix_length;
  386. if (!m_last_shown_suggestion.text.is_null()) {
  387. size_t actual_offset;
  388. size_t shown_length = m_last_shown_suggestion_display_length;
  389. switch (m_times_tab_pressed) {
  390. case 1:
  391. actual_offset = m_cursor;
  392. break;
  393. case 2:
  394. actual_offset = m_cursor - m_largest_common_suggestion_prefix_length + m_next_suggestion_invariant_offset;
  395. if (can_complete)
  396. shown_length = m_largest_common_suggestion_prefix_length + m_last_shown_suggestion.trailing_trivia.length();
  397. break;
  398. default:
  399. if (m_last_shown_suggestion_display_length == 0)
  400. actual_offset = m_cursor;
  401. else
  402. actual_offset = m_cursor - m_last_shown_suggestion_display_length + m_next_suggestion_invariant_offset;
  403. break;
  404. }
  405. for (size_t i = m_next_suggestion_invariant_offset; i < shown_length; ++i)
  406. m_buffer.remove(actual_offset);
  407. m_cursor = actual_offset;
  408. m_inline_search_cursor = m_cursor;
  409. m_refresh_needed = true;
  410. }
  411. m_last_shown_suggestion = m_suggestions[m_next_suggestion_index];
  412. m_last_shown_suggestion_display_length = m_last_shown_suggestion.text.length();
  413. m_last_shown_suggestion_was_complete = true;
  414. if (m_times_tab_pressed == 1) {
  415. // This is the first time, so only auto-complete *if possible*
  416. if (can_complete) {
  417. insert(m_last_shown_suggestion.text.substring_view(m_next_suggestion_invariant_offset, m_largest_common_suggestion_prefix_length - m_next_suggestion_invariant_offset));
  418. m_last_shown_suggestion_display_length = m_largest_common_suggestion_prefix_length;
  419. // do not increment the suggestion index, as the first tab should only be a *peek*
  420. if (m_suggestions.size() == 1) {
  421. // if there's one suggestion, commit and forget
  422. m_times_tab_pressed = 0;
  423. // add in the trivia of the last selected suggestion
  424. insert(m_last_shown_suggestion.trailing_trivia);
  425. m_last_shown_suggestion_display_length += m_last_shown_suggestion.trailing_trivia.length();
  426. }
  427. } else {
  428. m_last_shown_suggestion_display_length = 0;
  429. }
  430. ++m_times_tab_pressed;
  431. m_last_shown_suggestion_was_complete = false;
  432. } else {
  433. insert(m_last_shown_suggestion.text.substring_view(m_next_suggestion_invariant_offset, m_last_shown_suggestion.text.length() - m_next_suggestion_invariant_offset));
  434. // add in the trivia of the last selected suggestion
  435. insert(m_last_shown_suggestion.trailing_trivia);
  436. m_last_shown_suggestion_display_length += m_last_shown_suggestion.trailing_trivia.length();
  437. if (m_tab_direction == TabDirection::Forward)
  438. increment_suggestion_index();
  439. else
  440. decrement_suggestion_index();
  441. }
  442. } else {
  443. m_next_suggestion_index = 0;
  444. }
  445. if (m_times_tab_pressed > 1 && !m_suggestions.is_empty()) {
  446. size_t longest_suggestion_length = 0;
  447. for (auto& suggestion : m_suggestions) {
  448. longest_suggestion_length = max(longest_suggestion_length, suggestion.text.length());
  449. }
  450. size_t num_printed = 0;
  451. size_t lines_used { 1 };
  452. size_t index { 0 };
  453. vt_save_cursor();
  454. vt_clear_lines(0, m_lines_used_for_last_suggestions);
  455. vt_restore_cursor();
  456. auto spans_entire_line { false };
  457. auto max_line_count = (m_cached_prompt_length + longest_suggestion_length + m_num_columns - 1) / m_num_columns;
  458. if (longest_suggestion_length >= m_num_columns - 2) {
  459. spans_entire_line = true;
  460. // we should make enough space for the biggest entry in
  461. // the suggestion list to fit in the prompt line
  462. auto start = max_line_count - m_prompt_lines_at_suggestion_initiation;
  463. for (size_t i = start; i < max_line_count; ++i) {
  464. putchar('\n');
  465. }
  466. lines_used += max_line_count;
  467. longest_suggestion_length = 0;
  468. }
  469. vt_move_absolute(max_line_count + m_origin_x, 1);
  470. for (auto& suggestion : m_suggestions) {
  471. size_t next_column = num_printed + suggestion.text.length() + longest_suggestion_length + 2;
  472. if (next_column > m_num_columns) {
  473. auto lines = (suggestion.text.length() + m_num_columns - 1) / m_num_columns;
  474. lines_used += lines;
  475. putchar('\n');
  476. num_printed = 0;
  477. }
  478. // show just enough suggestions to fill up the screen
  479. // without moving the prompt out of view
  480. if (lines_used + m_prompt_lines_at_suggestion_initiation >= m_num_lines)
  481. break;
  482. // only apply colour to the selection if something is *actually* added to the buffer
  483. if (m_last_shown_suggestion_was_complete && index == current_suggestion_index) {
  484. vt_apply_style({ Style::Foreground(Style::Color::Blue) });
  485. fflush(stdout);
  486. }
  487. if (spans_entire_line) {
  488. num_printed += m_num_columns;
  489. fprintf(stderr, "%s", suggestion.text.characters());
  490. } else {
  491. num_printed += fprintf(stderr, "%-*s", static_cast<int>(longest_suggestion_length) + 2, suggestion.text.characters());
  492. }
  493. if (m_last_shown_suggestion_was_complete && index == current_suggestion_index) {
  494. vt_apply_style({});
  495. fflush(stdout);
  496. }
  497. ++index;
  498. }
  499. m_lines_used_for_last_suggestions = lines_used;
  500. // adjust for the case that we scroll up after writing the suggestions
  501. if (m_origin_x + lines_used >= m_num_lines) {
  502. m_origin_x = m_num_lines - lines_used;
  503. }
  504. reposition_cursor();
  505. }
  506. if (m_suggestions.size() < 2) {
  507. // we have none, or just one suggestion
  508. // we should just commit that and continue
  509. // after it, as if it were auto-completed
  510. suggest(0, 0);
  511. m_last_shown_suggestion = String::empty();
  512. m_last_shown_suggestion_display_length = 0;
  513. m_suggestions.clear();
  514. m_times_tab_pressed = 0;
  515. }
  516. continue;
  517. }
  518. if (m_times_tab_pressed) {
  519. // we probably have some suggestions drawn
  520. // let's clean them up
  521. if (m_lines_used_for_last_suggestions) {
  522. vt_clear_lines(0, m_lines_used_for_last_suggestions);
  523. reposition_cursor();
  524. m_refresh_needed = true;
  525. m_lines_used_for_last_suggestions = 0;
  526. }
  527. m_last_shown_suggestion_display_length = 0;
  528. m_last_shown_suggestion = String::empty();
  529. m_suggestions.clear();
  530. suggest(0, 0);
  531. }
  532. m_times_tab_pressed = 0; // Safe to say if we get here, the user didn't press TAB
  533. auto do_backspace = [&] {
  534. if (m_is_searching) {
  535. return;
  536. }
  537. if (m_cursor == 0) {
  538. fputc('\a', stdout);
  539. fflush(stdout);
  540. return;
  541. }
  542. m_buffer.remove(m_cursor - 1);
  543. --m_cursor;
  544. m_inline_search_cursor = m_cursor;
  545. // we will have to redraw :(
  546. m_refresh_needed = true;
  547. };
  548. if (ch == 8 || ch == m_termios.c_cc[VERASE]) {
  549. do_backspace();
  550. continue;
  551. }
  552. if (ch == m_termios.c_cc[VWERASE]) {
  553. bool has_seen_nonspace = false;
  554. while (m_cursor > 0) {
  555. if (isspace(m_buffer[m_cursor - 1])) {
  556. if (has_seen_nonspace)
  557. break;
  558. } else {
  559. has_seen_nonspace = true;
  560. }
  561. do_backspace();
  562. }
  563. continue;
  564. }
  565. if (ch == m_termios.c_cc[VKILL]) {
  566. for (size_t i = 0; i < m_cursor; ++i)
  567. m_buffer.remove(0);
  568. m_cursor = 0;
  569. m_refresh_needed = true;
  570. continue;
  571. }
  572. // ^L
  573. if (ch == 0xc) {
  574. printf("\033[3J\033[H\033[2J"); // Clear screen.
  575. vt_move_absolute(1, 1);
  576. m_origin_x = 1;
  577. m_origin_y = 1;
  578. m_refresh_needed = true;
  579. continue;
  580. }
  581. // ^A
  582. if (ch == 0x01) {
  583. m_cursor = 0;
  584. continue;
  585. }
  586. // ^R
  587. if (ch == 0x12) {
  588. if (m_is_searching) {
  589. // how did we get here?
  590. ASSERT_NOT_REACHED();
  591. } else {
  592. m_is_searching = true;
  593. m_search_offset = 0;
  594. m_pre_search_buffer.clear();
  595. for (auto ch : m_buffer)
  596. m_pre_search_buffer.append(ch);
  597. m_pre_search_cursor = m_cursor;
  598. m_search_editor = make<Editor>(true); // Has anyone seen 'Inception'?
  599. m_search_editor->initialize();
  600. m_search_editor->on_display_refresh = [this](Editor& search_editor) {
  601. search(StringView { search_editor.buffer().data(), search_editor.buffer().size() });
  602. refresh_display();
  603. return;
  604. };
  605. // whenever the search editor gets a ^R, cycle between history entries
  606. m_search_editor->register_character_input_callback(0x12, [this](Editor& search_editor) {
  607. ++m_search_offset;
  608. search_editor.m_refresh_needed = true;
  609. return false; // Do not process this key event
  610. });
  611. // whenever the search editor gets a backspace, cycle back between history entries
  612. // unless we're at the zeroth entry, in which case, allow the deletion
  613. m_search_editor->register_character_input_callback(m_termios.c_cc[VERASE], [this](Editor& search_editor) {
  614. if (m_search_offset > 0) {
  615. --m_search_offset;
  616. search_editor.m_refresh_needed = true;
  617. return false; // Do not process this key event
  618. }
  619. return true;
  620. });
  621. // quit without clearing the current buffer
  622. m_search_editor->register_character_input_callback('\t', [this](Editor& search_editor) {
  623. search_editor.finish();
  624. m_reset_buffer_on_search_end = false;
  625. return false;
  626. });
  627. printf("\n");
  628. fflush(stdout);
  629. auto search_prompt = "\x1b[32msearch:\x1b[0m ";
  630. auto search_string = m_search_editor->get_line(search_prompt);
  631. m_search_editor = nullptr;
  632. m_is_searching = false;
  633. m_search_offset = 0;
  634. // manually cleanup the search line
  635. reposition_cursor();
  636. vt_clear_lines(0, (search_string.length() + actual_rendered_string_length(search_prompt) + m_num_columns - 1) / m_num_columns);
  637. reposition_cursor();
  638. if (!m_reset_buffer_on_search_end || search_string.length() == 0) {
  639. // if the entry was empty, or we purposely quit without a newline,
  640. // do not return anything
  641. // instead, just end the search
  642. end_search();
  643. continue;
  644. }
  645. // return the string
  646. finish();
  647. continue;
  648. }
  649. continue;
  650. }
  651. // Normally ^D
  652. if (ch == m_termios.c_cc[VEOF]) {
  653. if (m_buffer.is_empty()) {
  654. printf("<EOF>\n");
  655. if (!m_always_refresh) // this is a little off, but it'll do for now
  656. exit(0);
  657. }
  658. continue;
  659. }
  660. // ^E
  661. if (ch == 0x05) {
  662. m_cursor = m_buffer.size();
  663. continue;
  664. }
  665. if (ch == '\n') {
  666. finish();
  667. continue;
  668. }
  669. insert(ch);
  670. }
  671. }
  672. }
  673. bool Editor::search(const StringView& phrase, bool allow_empty, bool from_beginning)
  674. {
  675. int last_matching_offset = -1;
  676. // do not search for empty strings
  677. if (allow_empty || phrase.length() > 0) {
  678. size_t search_offset = m_search_offset;
  679. for (size_t i = m_history_cursor; i > 0; --i) {
  680. auto contains = from_beginning ? m_history[i - 1].starts_with(phrase) : m_history[i - 1].contains(phrase);
  681. if (contains) {
  682. last_matching_offset = i - 1;
  683. if (search_offset == 0)
  684. break;
  685. --search_offset;
  686. }
  687. }
  688. if (last_matching_offset == -1) {
  689. fputc('\a', stdout);
  690. fflush(stdout);
  691. }
  692. }
  693. m_buffer.clear();
  694. m_cursor = 0;
  695. if (last_matching_offset >= 0) {
  696. insert(m_history[last_matching_offset]);
  697. }
  698. // always needed
  699. m_refresh_needed = true;
  700. return last_matching_offset >= 0;
  701. }
  702. void Editor::recalculate_origin()
  703. {
  704. // changing the columns can affect our origin if
  705. // the new size is smaller than our prompt, which would
  706. // cause said prompt to take up more space, so we should
  707. // compensate for that
  708. if (m_cached_prompt_length >= m_num_columns) {
  709. auto added_lines = (m_cached_prompt_length + 1) / m_num_columns - 1;
  710. m_origin_x += added_lines;
  711. }
  712. // we also need to recalculate our cursor position
  713. // but that will be calculated and applied at the next
  714. // refresh cycle
  715. }
  716. void Editor::cleanup()
  717. {
  718. vt_move_relative(0, m_pending_chars.size() - m_chars_inserted_in_the_middle);
  719. auto current_line = cursor_line();
  720. vt_clear_lines(current_line - 1, num_lines() - current_line);
  721. vt_move_relative(-num_lines() + 1, -offset_in_line() - m_old_prompt_length - m_pending_chars.size() + m_chars_inserted_in_the_middle);
  722. };
  723. void Editor::refresh_display()
  724. {
  725. auto has_cleaned_up = false;
  726. // someone changed the window size, figure it out
  727. // and react to it, we might need to redraw
  728. if (m_was_resized) {
  729. auto previous_num_columns = m_num_columns;
  730. struct winsize ws;
  731. if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) < 0) {
  732. m_num_columns = 80;
  733. m_num_lines = 25;
  734. } else {
  735. m_num_columns = ws.ws_col;
  736. m_num_lines = ws.ws_row;
  737. }
  738. if (previous_num_columns != m_num_columns) {
  739. // we need to cleanup and redo everything
  740. m_cached_prompt_valid = false;
  741. m_refresh_needed = true;
  742. swap(previous_num_columns, m_num_columns);
  743. recalculate_origin();
  744. cleanup();
  745. swap(previous_num_columns, m_num_columns);
  746. has_cleaned_up = true;
  747. }
  748. }
  749. // do not call hook on pure cursor movement
  750. if (m_cached_prompt_valid && !m_refresh_needed && m_pending_chars.size() == 0) {
  751. // probably just moving around
  752. reposition_cursor();
  753. m_cached_buffer_size = m_buffer.size();
  754. return;
  755. }
  756. if (on_display_refresh)
  757. on_display_refresh(*this);
  758. if (m_cached_prompt_valid) {
  759. if (!m_refresh_needed && m_cursor == m_buffer.size()) {
  760. // just write the characters out and continue
  761. // no need to refresh the entire line
  762. char null = 0;
  763. m_pending_chars.append(&null, 1);
  764. fputs((char*)m_pending_chars.data(), stdout);
  765. m_pending_chars.clear();
  766. m_drawn_cursor = m_cursor;
  767. m_cached_buffer_size = m_buffer.size();
  768. fflush(stdout);
  769. return;
  770. }
  771. }
  772. // ouch, reflow entire line
  773. // FIXME: handle multiline stuff
  774. if (!has_cleaned_up) {
  775. cleanup();
  776. }
  777. vt_move_absolute(m_origin_x, m_origin_y);
  778. fputs(m_new_prompt.characters(), stdout);
  779. vt_clear_to_end_of_line();
  780. HashMap<u32, Style> empty_styles {};
  781. for (size_t i = 0; i < m_buffer.size(); ++i) {
  782. auto ends = m_spans_ending.get(i).value_or(empty_styles);
  783. auto starts = m_spans_starting.get(i).value_or(empty_styles);
  784. if (ends.size()) {
  785. // go back to defaults
  786. vt_apply_style(find_applicable_style(i));
  787. }
  788. if (starts.size()) {
  789. // set new options
  790. vt_apply_style(starts.begin()->value); // apply some random style that starts here
  791. }
  792. fputc(m_buffer[i], stdout);
  793. }
  794. vt_apply_style({}); // don't bleed to EOL
  795. m_pending_chars.clear();
  796. m_refresh_needed = false;
  797. m_cached_buffer_size = m_buffer.size();
  798. m_chars_inserted_in_the_middle = 0;
  799. if (!m_cached_prompt_valid) {
  800. m_cached_prompt_valid = true;
  801. }
  802. reposition_cursor();
  803. fflush(stdout);
  804. }
  805. void Editor::reposition_cursor()
  806. {
  807. m_drawn_cursor = m_cursor;
  808. auto line = cursor_line() - 1;
  809. auto column = offset_in_line();
  810. vt_move_absolute(line + m_origin_x, column + m_origin_y);
  811. }
  812. void Editor::vt_move_absolute(u32 x, u32 y)
  813. {
  814. printf("\033[%d;%dH", x, y);
  815. fflush(stdout);
  816. }
  817. void Editor::vt_move_relative(int x, int y)
  818. {
  819. char x_op = 'A', y_op = 'D';
  820. if (x > 0)
  821. x_op = 'B';
  822. else
  823. x = -x;
  824. if (y > 0)
  825. y_op = 'C';
  826. else
  827. y = -y;
  828. if (x > 0)
  829. printf("\033[%d%c", x, x_op);
  830. if (y > 0)
  831. printf("\033[%d%c", y, y_op);
  832. }
  833. Style Editor::find_applicable_style(size_t offset) const
  834. {
  835. // walk through our styles and find one that fits in the offset
  836. for (auto& entry : m_spans_starting) {
  837. if (entry.key > offset)
  838. continue;
  839. for (auto& style_value : entry.value) {
  840. if (style_value.key <= offset)
  841. continue;
  842. return style_value.value;
  843. }
  844. }
  845. return {};
  846. }
  847. void Editor::vt_apply_style(const Style& style)
  848. {
  849. printf(
  850. "\033[%d;%d;%d;%d;%dm",
  851. style.bold() ? 1 : 22,
  852. style.underline() ? 4 : 24,
  853. style.italic() ? 3 : 23,
  854. (int)style.foreground() + 30,
  855. (int)style.background() + 40);
  856. }
  857. void Editor::vt_clear_lines(size_t count_above, size_t count_below)
  858. {
  859. // go down count_below lines
  860. if (count_below > 0)
  861. printf("\033[%dB", (int)count_below);
  862. // then clear lines going upwards
  863. for (size_t i = count_below + count_above; i > 0; --i)
  864. fputs(i == 1 ? "\033[2K" : "\033[2K\033[A", stdout);
  865. }
  866. void Editor::vt_save_cursor()
  867. {
  868. fputs("\033[s", stdout);
  869. fflush(stdout);
  870. }
  871. void Editor::vt_restore_cursor()
  872. {
  873. fputs("\033[u", stdout);
  874. fflush(stdout);
  875. }
  876. void Editor::vt_clear_to_end_of_line()
  877. {
  878. fputs("\033[K", stdout);
  879. fflush(stdout);
  880. }
  881. size_t Editor::actual_rendered_string_length(const StringView& string) const
  882. {
  883. size_t length { 0 };
  884. enum VTState {
  885. Free = 1,
  886. Escape = 3,
  887. Bracket = 5,
  888. BracketArgsSemi = 7,
  889. Title = 9,
  890. } state { Free };
  891. for (size_t i = 0; i < string.length(); ++i) {
  892. auto c = string[i];
  893. switch (state) {
  894. case Free:
  895. if (c == '\x1b') {
  896. // escape
  897. state = Escape;
  898. continue;
  899. }
  900. // FIXME: This will not support anything sophisticated
  901. ++length;
  902. break;
  903. case Escape:
  904. if (c == ']') {
  905. if (string.length() > i && string[i + 1] == '0')
  906. state = Title;
  907. continue;
  908. }
  909. if (c == '[') {
  910. state = Bracket;
  911. continue;
  912. }
  913. // FIXME: This does not support non-VT (aside from set-title) escapes
  914. break;
  915. case Bracket:
  916. if (isdigit(c)) {
  917. state = BracketArgsSemi;
  918. continue;
  919. }
  920. break;
  921. case BracketArgsSemi:
  922. if (c == ';') {
  923. state = Bracket;
  924. continue;
  925. }
  926. if (!isdigit(c))
  927. state = Free;
  928. break;
  929. case Title:
  930. if (c == 7)
  931. state = Free;
  932. break;
  933. }
  934. }
  935. return length;
  936. }
  937. Vector<size_t, 2> Editor::vt_dsr()
  938. {
  939. fputs("\033[6n", stdout);
  940. fflush(stdout);
  941. char buf[16];
  942. u32 length { 0 };
  943. do {
  944. auto nread = read(0, buf + length, 16 - length);
  945. if (nread < 0) {
  946. if (errno == 0) {
  947. // ????
  948. continue;
  949. }
  950. dbg() << "Error while reading DSR: " << strerror(errno);
  951. return { 1, 1 };
  952. }
  953. if (nread == 0) {
  954. dbg() << "Terminal DSR issue; received no response";
  955. return { 1, 1 };
  956. }
  957. length += nread;
  958. } while (buf[length - 1] != 'R' && length < 16);
  959. size_t x { 1 }, y { 1 };
  960. if (buf[0] == '\033' && buf[1] == '[') {
  961. auto parts = StringView(buf + 2, length - 3).split_view(';');
  962. bool ok;
  963. x = parts[0].to_int(ok);
  964. if (!ok) {
  965. dbg() << "Terminal DSR issue; received garbage x";
  966. }
  967. y = parts[1].to_int(ok);
  968. if (!ok) {
  969. dbg() << "Terminal DSR issue; received garbage y";
  970. }
  971. }
  972. return { x, y };
  973. }
  974. }