2020-03-30 21:29:04 +04:30
/*
* Copyright ( c ) 2018 - 2020 , Andreas Kling < kling @ serenityos . org >
* All rights reserved .
*
* Redistribution and use in source and binary forms , with or without
* modification , are permitted provided that the following conditions are met :
*
* 1. Redistributions of source code must retain the above copyright notice , this
* list of conditions and the following disclaimer .
*
* 2. Redistributions in binary form must reproduce the above copyright notice ,
* this list of conditions and the following disclaimer in the documentation
* and / or other materials provided with the distribution .
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS " AS IS "
* AND ANY EXPRESS OR IMPLIED WARRANTIES , INCLUDING , BUT NOT LIMITED TO , THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED . IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT , INDIRECT , INCIDENTAL , SPECIAL , EXEMPLARY , OR CONSEQUENTIAL
* DAMAGES ( INCLUDING , BUT NOT LIMITED TO , PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES ; LOSS OF USE , DATA , OR PROFITS ; OR BUSINESS INTERRUPTION ) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY , WHETHER IN CONTRACT , STRICT LIABILITY ,
* OR TORT ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE , EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE .
*/
2020-03-31 13:34:57 +02:00
# include "Editor.h"
2020-04-01 11:24:10 +02:00
# include <AK/StringBuilder.h>
2020-05-18 13:47:34 +04:30
# include <AK/Utf32View.h>
# include <AK/Utf8View.h>
2020-03-30 21:29:04 +04:30
# include <ctype.h>
# include <stdio.h>
# include <sys/ioctl.h>
2020-04-29 01:47:41 +04:30
# include <sys/select.h>
# include <sys/time.h>
2020-03-30 21:29:04 +04:30
# include <unistd.h>
2020-05-21 20:07:09 +01:00
// #define SUGGESTIONS_DEBUG
2020-03-31 13:34:06 +02:00
namespace Line {
2020-04-30 08:19:47 +04:30
Editor : : Editor ( Configuration configuration )
2020-05-19 08:42:01 +04:30
: m_configuration ( move ( configuration ) )
2020-03-30 21:29:04 +04:30
{
2020-04-30 08:19:47 +04:30
m_always_refresh = configuration . refresh_behaviour = = Configuration : : RefreshBehaviour : : Eager ;
2020-04-05 06:41:33 +04:30
m_pending_chars = ByteBuffer : : create_uninitialized ( 0 ) ;
2020-03-30 21:29:04 +04:30
struct winsize ws ;
2020-04-11 13:29:55 +04:30
if ( ioctl ( STDOUT_FILENO , TIOCGWINSZ , & ws ) < 0 ) {
2020-03-30 21:29:04 +04:30
m_num_columns = 80 ;
2020-04-11 13:29:55 +04:30
m_num_lines = 25 ;
} else {
2020-03-30 21:29:04 +04:30
m_num_columns = ws . ws_col ;
2020-04-11 13:29:55 +04:30
m_num_lines = ws . ws_row ;
}
2020-03-30 21:29:04 +04:30
}
2020-03-31 13:34:06 +02:00
Editor : : ~ Editor ( )
2020-03-30 21:29:04 +04:30
{
2020-04-02 01:29:48 +04:30
if ( m_initialized )
2020-04-29 01:46:19 +04:30
restore ( ) ;
2020-03-30 21:29:04 +04:30
}
2020-03-31 13:34:06 +02:00
void Editor : : add_to_history ( const String & line )
2020-03-30 21:29:04 +04:30
{
if ( ( m_history . size ( ) + 1 ) > m_history_capacity )
m_history . take_first ( ) ;
m_history . append ( line ) ;
}
2020-03-31 13:34:06 +02:00
void Editor : : clear_line ( )
2020-03-30 21:29:04 +04:30
{
for ( size_t i = 0 ; i < m_cursor ; + + i )
fputc ( 0x8 , stdout ) ;
fputs ( " \033 [K " , stdout ) ;
fflush ( stdout ) ;
m_buffer . clear ( ) ;
m_cursor = 0 ;
2020-04-20 17:20:31 +04:30
m_inline_search_cursor = m_cursor ;
2020-03-30 21:29:04 +04:30
}
2020-05-21 18:06:43 +04:30
void Editor : : insert ( const Utf32View & string )
{
for ( size_t i = 0 ; i < string . length ( ) ; + + i )
insert ( string . codepoints ( ) [ i ] ) ;
}
2020-03-31 13:34:06 +02:00
void Editor : : insert ( const String & string )
2020-03-30 21:29:04 +04:30
{
2020-05-18 13:47:34 +04:30
for ( auto ch : Utf8View { string } )
2020-04-11 13:29:55 +04:30
insert ( ch ) ;
2020-03-30 21:29:04 +04:30
}
2020-05-18 13:47:34 +04:30
void Editor : : insert ( const u32 cp )
2020-03-30 21:29:04 +04:30
{
2020-05-18 13:47:34 +04:30
StringBuilder builder ;
builder . append ( Utf32View ( & cp , 1 ) ) ;
auto str = builder . build ( ) ;
m_pending_chars . append ( str . characters ( ) , str . length ( ) ) ;
2020-05-18 02:25:58 +04:30
readjust_anchored_styles ( m_cursor , ModificationKind : : Insertion ) ;
2020-03-30 21:29:04 +04:30
if ( m_cursor = = m_buffer . size ( ) ) {
2020-05-18 13:47:34 +04:30
m_buffer . append ( cp ) ;
2020-03-30 21:29:04 +04:30
m_cursor = m_buffer . size ( ) ;
2020-04-20 17:20:31 +04:30
m_inline_search_cursor = m_cursor ;
2020-03-30 21:29:04 +04:30
return ;
}
2020-05-18 13:47:34 +04:30
m_buffer . insert ( m_cursor , cp ) ;
2020-04-05 06:41:33 +04:30
+ + m_chars_inserted_in_the_middle ;
2020-03-30 21:29:04 +04:30
+ + m_cursor ;
2020-04-20 17:20:31 +04:30
m_inline_search_cursor = m_cursor ;
2020-03-30 21:29:04 +04:30
}
2020-03-31 13:37:01 +02:00
void Editor : : register_character_input_callback ( char ch , Function < bool ( Editor & ) > callback )
2020-03-30 21:29:04 +04:30
{
if ( m_key_callbacks . contains ( ch ) ) {
dbg ( ) < < " Key callback registered twice for " < < ch ;
ASSERT_NOT_REACHED ( ) ;
}
m_key_callbacks . set ( ch , make < KeyCallback > ( move ( callback ) ) ) ;
}
2020-05-18 21:35:01 +04:30
static size_t codepoint_length_in_utf8 ( u32 codepoint )
{
if ( codepoint < = 0x7f )
return 1 ;
if ( codepoint < = 0x07ff )
return 2 ;
if ( codepoint < = 0xffff )
return 3 ;
if ( codepoint < = 0x10ffff )
return 4 ;
return 3 ;
}
2020-05-21 15:15:56 +04:30
// buffer [ 0 1 2 3 . . . A . . . B . . . M . . . N ]
// ^ ^ ^ ^
// | | | +- end of buffer
// | | +- scan offset = M
// | +- range end = M - B
// +- range start = M - A
// This method converts a byte range defined by [start_byte_offset, end_byte_offset] to a codepoint range [M - A, M - B] as shown in the diagram above.
// If `reverse' is true, A and B are before M, if not, A and B are after M.
Editor : : CodepointRange Editor : : byte_offset_range_to_codepoint_offset_range ( size_t start_byte_offset , size_t end_byte_offset , size_t scan_codepoint_offset , bool reverse ) const
{
size_t byte_offset = 0 ;
size_t codepoint_offset = scan_codepoint_offset + ( reverse ? 1 : 0 ) ;
CodepointRange range ;
for ( ; ; ) {
if ( ! reverse ) {
if ( codepoint_offset > = m_buffer . size ( ) )
break ;
} else {
if ( codepoint_offset = = 0 )
break ;
}
if ( byte_offset > end_byte_offset )
break ;
if ( byte_offset < start_byte_offset )
+ + range . start ;
if ( byte_offset < end_byte_offset )
+ + range . end ;
byte_offset + = codepoint_length_in_utf8 ( m_buffer [ reverse ? - - codepoint_offset : codepoint_offset + + ] ) ;
}
return range ;
}
2020-04-05 06:41:33 +04:30
void Editor : : stylize ( const Span & span , const Style & style )
{
2020-05-18 02:25:58 +04:30
if ( style . is_empty ( ) )
return ;
2020-05-18 21:35:01 +04:30
auto start = span . beginning ( ) ;
auto end = span . end ( ) ;
if ( span . mode ( ) = = Span : : ByteOriented ) {
2020-05-21 15:15:56 +04:30
auto offsets = byte_offset_range_to_codepoint_offset_range ( start , end , 0 ) ;
2020-05-18 21:35:01 +04:30
2020-05-21 15:15:56 +04:30
start = offsets . start ;
end = offsets . end ;
2020-05-18 21:35:01 +04:30
}
2020-05-18 02:25:58 +04:30
auto & spans_starting = style . is_anchored ( ) ? m_anchored_spans_starting : m_spans_starting ;
auto & spans_ending = style . is_anchored ( ) ? m_anchored_spans_ending : m_spans_ending ;
auto starting_map = spans_starting . get ( start ) . value_or ( { } ) ;
2020-04-05 06:41:33 +04:30
2020-05-18 21:35:01 +04:30
if ( ! starting_map . contains ( end ) )
2020-04-05 06:41:33 +04:30
m_refresh_needed = true ;
2020-05-18 21:35:01 +04:30
starting_map . set ( end , style ) ;
2020-04-05 06:41:33 +04:30
2020-05-18 02:25:58 +04:30
spans_starting . set ( start , starting_map ) ;
2020-04-05 06:41:33 +04:30
2020-05-18 02:25:58 +04:30
auto ending_map = spans_ending . get ( end ) . value_or ( { } ) ;
2020-04-05 06:41:33 +04:30
2020-05-18 21:35:01 +04:30
if ( ! ending_map . contains ( start ) )
2020-04-05 06:41:33 +04:30
m_refresh_needed = true ;
2020-05-18 21:35:01 +04:30
ending_map . set ( start , style ) ;
2020-04-05 06:41:33 +04:30
2020-05-18 02:25:58 +04:30
spans_ending . set ( end , ending_map ) ;
2020-04-05 06:41:33 +04:30
}
2020-05-21 15:15:56 +04:30
void Editor : : suggest ( size_t invariant_offset , size_t static_offset , Span : : Mode offset_mode ) const
{
m_next_suggestion_index = 0 ;
auto internal_static_offset = static_offset ;
auto internal_invariant_offset = invariant_offset ;
if ( offset_mode = = Span : : Mode : : ByteOriented ) {
// FIXME: We're assuming that invariant_offset points to the end of the available data
// this is not necessarily true, but is true in most cases.
auto offsets = byte_offset_range_to_codepoint_offset_range ( internal_static_offset , internal_invariant_offset + internal_static_offset , m_cursor - 1 , true ) ;
internal_static_offset = offsets . start ;
internal_invariant_offset = offsets . end - offsets . start ;
}
m_next_suggestion_static_offset = internal_static_offset ;
m_next_suggestion_invariant_offset = internal_invariant_offset ;
}
2020-03-31 13:34:06 +02:00
String Editor : : get_line ( const String & prompt )
2020-03-30 21:29:04 +04:30
{
2020-04-29 01:46:19 +04:30
initialize ( ) ;
2020-04-29 00:31:22 +04:30
m_is_editing = true ;
2020-04-09 07:44:04 +04:30
set_prompt ( prompt ) ;
reset ( ) ;
2020-04-11 17:22:24 +04:30
set_origin ( ) ;
2020-05-18 02:25:58 +04:30
strip_styles ( true ) ;
2020-03-30 21:29:04 +04:30
m_history_cursor = m_history . size ( ) ;
for ( ; ; ) {
2020-04-19 23:34:58 +04:30
if ( m_always_refresh )
m_refresh_needed = true ;
2020-04-09 07:44:04 +04:30
refresh_display ( ) ;
2020-04-19 23:34:58 +04:30
if ( m_finish ) {
m_finish = false ;
printf ( " \n " ) ;
fflush ( stdout ) ;
2020-05-18 13:47:34 +04:30
auto string = line ( ) ;
2020-04-19 23:34:58 +04:30
m_buffer . clear ( ) ;
2020-04-29 00:31:22 +04:30
m_is_editing = false ;
2020-04-29 01:46:19 +04:30
restore ( ) ;
2020-04-19 23:34:58 +04:30
return string ;
}
2020-03-30 21:29:04 +04:30
char keybuf [ 16 ] ;
2020-05-18 13:47:34 +04:30
ssize_t nread = 0 ;
if ( ! m_incomplete_data . size ( ) )
nread = read ( 0 , keybuf , sizeof ( keybuf ) ) ;
2020-03-30 21:29:04 +04:30
if ( nread < 0 ) {
if ( errno = = EINTR ) {
2020-04-20 17:53:24 +04:30
if ( ! m_was_interrupted ) {
if ( m_was_resized )
continue ;
2020-04-22 14:18:17 +04:30
finish ( ) ;
continue ;
}
2020-04-20 17:53:24 +04:30
2020-04-22 14:18:17 +04:30
m_was_interrupted = false ;
2020-04-19 23:34:58 +04:30
2020-04-22 14:18:17 +04:30
if ( ! m_buffer . is_empty ( ) )
printf ( " ^C " ) ;
2020-03-30 21:29:04 +04:30
2020-04-22 14:18:17 +04:30
m_buffer . clear ( ) ;
m_cursor = 0 ;
2020-05-13 14:22:47 +04:30
if ( on_interrupt_handled )
on_interrupt_handled ( ) ;
2020-04-22 14:18:17 +04:30
m_refresh_needed = true ;
2020-04-19 23:34:58 +04:30
continue ;
2020-03-30 21:29:04 +04:30
}
perror ( " read failed " ) ;
// FIXME: exit()ing here is a bit off. Should communicate failure to caller somehow instead.
exit ( 2 ) ;
}
2020-05-18 13:47:34 +04:30
m_incomplete_data . append ( keybuf , nread ) ;
nread = m_incomplete_data . size ( ) ;
// FIXME: exit()ing here is a bit off. Should communicate failure to caller somehow instead.
if ( nread = = 0 )
exit ( 0 ) ;
2020-04-11 19:35:39 +01:00
auto reverse_tab = false ;
auto increment_suggestion_index = [ & ] {
2020-04-15 18:26:11 +04:30
if ( m_suggestions . size ( ) )
m_next_suggestion_index = ( m_next_suggestion_index + 1 ) % m_suggestions . size ( ) ;
else
m_next_suggestion_index = 0 ;
2020-04-11 19:35:39 +01:00
} ;
auto decrement_suggestion_index = [ & ] {
if ( m_next_suggestion_index = = 0 )
m_next_suggestion_index = m_suggestions . size ( ) ;
m_next_suggestion_index - - ;
} ;
2020-04-20 22:05:03 +04:30
auto ctrl_held = false ;
2020-05-18 13:47:34 +04:30
// discard starting bytes until they make sense as utf-8
size_t valid_bytes = 0 ;
while ( nread ) {
Utf8View { StringView { m_incomplete_data . data ( ) , ( size_t ) nread } } . validate ( valid_bytes ) ;
if ( valid_bytes )
break ;
m_incomplete_data . take_first ( ) ;
- - nread ;
}
Utf8View input_view { StringView { m_incomplete_data . data ( ) , valid_bytes } } ;
2020-05-18 14:47:19 +04:30
size_t consumed_codepoints = 0 ;
2020-05-18 13:47:34 +04:30
2020-05-18 14:47:19 +04:30
for ( auto codepoint : input_view ) {
if ( m_finish )
break ;
+ + consumed_codepoints ;
if ( codepoint = = 0 )
2020-03-30 21:29:04 +04:30
continue ;
switch ( m_state ) {
case InputState : : ExpectBracket :
2020-05-18 14:47:19 +04:30
if ( codepoint = = ' [ ' ) {
2020-03-30 21:29:04 +04:30
m_state = InputState : : ExpectFinal ;
continue ;
} else {
m_state = InputState : : Free ;
break ;
}
case InputState : : ExpectFinal :
2020-05-18 14:47:19 +04:30
switch ( codepoint ) {
2020-04-20 22:05:03 +04:30
case ' O ' : // mod_ctrl
ctrl_held = true ;
continue ;
2020-03-30 21:29:04 +04:30
case ' A ' : // up
2020-04-20 17:20:31 +04:30
{
m_searching_backwards = true ;
auto inline_search_cursor = m_inline_search_cursor ;
2020-05-18 13:47:34 +04:30
StringBuilder builder ;
builder . append ( Utf32View { m_buffer . data ( ) , inline_search_cursor } ) ;
String search_phrase = builder . to_string ( ) ;
2020-04-20 17:20:31 +04:30
if ( search ( search_phrase , true , true ) ) {
+ + m_search_offset ;
} else {
insert ( search_phrase ) ;
2020-04-10 09:35:53 +04:30
}
2020-04-20 17:20:31 +04:30
m_inline_search_cursor = inline_search_cursor ;
2020-03-30 21:29:04 +04:30
m_state = InputState : : Free ;
2020-04-20 22:05:03 +04:30
ctrl_held = false ;
2020-03-30 21:29:04 +04:30
continue ;
2020-04-20 17:20:31 +04:30
}
2020-03-30 21:29:04 +04:30
case ' B ' : // down
2020-04-20 17:20:31 +04:30
{
auto inline_search_cursor = m_inline_search_cursor ;
2020-05-18 13:47:34 +04:30
StringBuilder builder ;
builder . append ( Utf32View { m_buffer . data ( ) , inline_search_cursor } ) ;
String search_phrase = builder . to_string ( ) ;
2020-04-20 17:20:31 +04:30
auto search_changed_directions = m_searching_backwards ;
m_searching_backwards = false ;
if ( m_search_offset > 0 ) {
m_search_offset - = 1 + search_changed_directions ;
if ( ! search ( search_phrase , true , true ) ) {
insert ( search_phrase ) ;
}
} else {
m_search_offset = 0 ;
m_cursor = 0 ;
2020-04-10 09:35:53 +04:30
m_buffer . clear ( ) ;
2020-04-20 17:20:31 +04:30
insert ( search_phrase ) ;
2020-04-10 09:35:53 +04:30
m_refresh_needed = true ;
}
2020-04-20 17:20:31 +04:30
m_inline_search_cursor = inline_search_cursor ;
2020-03-30 21:29:04 +04:30
m_state = InputState : : Free ;
2020-04-20 22:05:03 +04:30
ctrl_held = false ;
2020-03-30 21:29:04 +04:30
continue ;
2020-04-20 17:20:31 +04:30
}
2020-03-30 21:29:04 +04:30
case ' D ' : // left
if ( m_cursor > 0 ) {
2020-04-20 22:05:03 +04:30
if ( ctrl_held ) {
auto skipped_at_least_one_character = false ;
for ( ; ; ) {
if ( m_cursor = = 0 )
break ;
if ( skipped_at_least_one_character & & isspace ( m_buffer [ m_cursor - 1 ] ) ) // stop *after* a space, but only if it changes the position
break ;
skipped_at_least_one_character = true ;
- - m_cursor ;
}
} else {
- - m_cursor ;
}
2020-03-30 21:29:04 +04:30
}
2020-04-20 17:20:31 +04:30
m_inline_search_cursor = m_cursor ;
2020-03-30 21:29:04 +04:30
m_state = InputState : : Free ;
2020-04-20 22:05:03 +04:30
ctrl_held = false ;
2020-03-30 21:29:04 +04:30
continue ;
case ' C ' : // right
if ( m_cursor < m_buffer . size ( ) ) {
2020-04-20 22:05:03 +04:30
if ( ctrl_held ) {
// temporarily put a space at the end of our buffer
// this greatly simplifies the logic below
m_buffer . append ( ' ' ) ;
for ( ; ; ) {
if ( m_cursor > = m_buffer . size ( ) )
break ;
if ( isspace ( m_buffer [ + + m_cursor ] ) )
break ;
}
m_buffer . take_last ( ) ;
} else {
+ + m_cursor ;
}
2020-03-30 21:29:04 +04:30
}
2020-04-20 17:20:31 +04:30
m_inline_search_cursor = m_cursor ;
m_search_offset = 0 ;
2020-03-30 21:29:04 +04:30
m_state = InputState : : Free ;
2020-04-20 22:05:03 +04:30
ctrl_held = false ;
2020-03-30 21:29:04 +04:30
continue ;
case ' H ' :
2020-04-09 07:44:04 +04:30
m_cursor = 0 ;
2020-04-20 17:20:31 +04:30
m_inline_search_cursor = m_cursor ;
m_search_offset = 0 ;
2020-03-30 21:29:04 +04:30
m_state = InputState : : Free ;
2020-04-20 22:05:03 +04:30
ctrl_held = false ;
2020-03-30 21:29:04 +04:30
continue ;
case ' F ' :
2020-04-09 07:44:04 +04:30
m_cursor = m_buffer . size ( ) ;
2020-03-30 21:29:04 +04:30
m_state = InputState : : Free ;
2020-04-20 17:20:31 +04:30
m_inline_search_cursor = m_cursor ;
m_search_offset = 0 ;
2020-04-20 22:05:03 +04:30
ctrl_held = false ;
2020-03-30 21:29:04 +04:30
continue ;
2020-04-11 19:35:39 +01:00
case ' Z ' : // shift+tab
reverse_tab = true ;
m_state = InputState : : Free ;
2020-04-20 22:05:03 +04:30
ctrl_held = false ;
2020-04-11 19:35:39 +01:00
break ;
2020-03-30 21:29:04 +04:30
case ' 3 ' :
2020-04-13 14:51:34 +04:30
if ( m_cursor = = m_buffer . size ( ) ) {
fputc ( ' \a ' , stdout ) ;
fflush ( stdout ) ;
continue ;
}
2020-05-18 02:25:58 +04:30
remove_at_index ( m_cursor ) ;
2020-04-13 14:51:34 +04:30
m_refresh_needed = true ;
2020-04-20 17:20:31 +04:30
m_search_offset = 0 ;
2020-03-30 21:29:04 +04:30
m_state = InputState : : ExpectTerminator ;
2020-04-20 22:05:03 +04:30
ctrl_held = false ;
2020-03-30 21:29:04 +04:30
continue ;
default :
2020-05-18 14:47:19 +04:30
dbgprintf ( " Shell: Unhandled final: %02x (%c) \r \n " , codepoint , codepoint ) ;
2020-03-30 21:29:04 +04:30
m_state = InputState : : Free ;
2020-04-20 22:05:03 +04:30
ctrl_held = false ;
2020-03-30 21:29:04 +04:30
continue ;
}
break ;
case InputState : : ExpectTerminator :
m_state = InputState : : Free ;
continue ;
case InputState : : Free :
2020-05-18 14:47:19 +04:30
if ( codepoint = = 27 ) {
2020-03-30 21:29:04 +04:30
m_state = InputState : : ExpectBracket ;
continue ;
}
break ;
}
2020-05-18 14:47:19 +04:30
auto cb = m_key_callbacks . get ( codepoint ) ;
2020-03-30 21:29:04 +04:30
if ( cb . has_value ( ) ) {
if ( ! cb . value ( ) - > callback ( * this ) ) {
continue ;
}
}
2020-04-20 17:20:31 +04:30
m_search_offset = 0 ; // reset search offset on any key
2020-05-18 14:47:19 +04:30
if ( codepoint = = ' \t ' | | reverse_tab ) {
2020-05-19 08:42:01 +04:30
if ( ! on_tab_complete )
2020-03-30 21:29:04 +04:30
continue ;
2020-04-11 19:35:39 +01:00
// reverse tab can count as regular tab here
2020-03-30 21:29:04 +04:30
m_times_tab_pressed + + ;
2020-05-21 05:14:34 +04:30
int token_start = m_cursor ;
2020-05-18 02:25:58 +04:30
2020-04-11 19:02:15 +04:30
// ask for completions only on the first tab
2020-04-19 15:55:17 +04:30
// and scan for the largest common prefix to display
2020-04-11 19:02:15 +04:30
// further tabs simply show the cached completions
if ( m_times_tab_pressed = = 1 ) {
2020-05-19 08:42:01 +04:30
m_suggestions = on_tab_complete ( * this ) ;
2020-04-19 15:55:17 +04:30
size_t common_suggestion_prefix { 0 } ;
if ( m_suggestions . size ( ) = = 1 ) {
2020-05-21 18:06:43 +04:30
m_largest_common_suggestion_prefix_length = m_suggestions [ 0 ] . text_view . length ( ) ;
2020-04-19 15:55:17 +04:30
} else if ( m_suggestions . size ( ) ) {
2020-05-21 18:06:43 +04:30
u32 last_valid_suggestion_codepoint ;
2020-04-19 15:55:17 +04:30
for ( ; ; + + common_suggestion_prefix ) {
2020-05-21 18:06:43 +04:30
if ( m_suggestions [ 0 ] . text_view . length ( ) < = common_suggestion_prefix )
2020-04-19 15:55:17 +04:30
goto no_more_commons ;
2020-05-21 18:06:43 +04:30
last_valid_suggestion_codepoint = m_suggestions [ 0 ] . text_view . codepoints ( ) [ common_suggestion_prefix ] ;
2020-04-19 15:55:17 +04:30
2020-05-21 18:06:43 +04:30
for ( auto & suggestion : m_suggestions ) {
if ( suggestion . text_view . length ( ) < = common_suggestion_prefix | | suggestion . text_view . codepoints ( ) [ common_suggestion_prefix ] ! = last_valid_suggestion_codepoint ) {
2020-04-19 15:55:17 +04:30
goto no_more_commons ;
}
}
}
no_more_commons : ;
m_largest_common_suggestion_prefix_length = common_suggestion_prefix ;
} else {
m_largest_common_suggestion_prefix_length = 0 ;
// there are no suggestions, beep~
putchar ( ' \a ' ) ;
fflush ( stdout ) ;
}
2020-04-14 22:18:56 +04:30
m_prompt_lines_at_suggestion_initiation = num_lines ( ) ;
2020-04-11 19:02:15 +04:30
}
2020-04-11 19:35:39 +01:00
// Adjust already incremented / decremented index when switching tab direction
if ( reverse_tab & & m_tab_direction ! = TabDirection : : Backward ) {
decrement_suggestion_index ( ) ;
decrement_suggestion_index ( ) ;
m_tab_direction = TabDirection : : Backward ;
}
if ( ! reverse_tab & & m_tab_direction ! = TabDirection : : Forward ) {
increment_suggestion_index ( ) ;
increment_suggestion_index ( ) ;
m_tab_direction = TabDirection : : Forward ;
}
reverse_tab = false ;
2020-04-11 19:02:15 +04:30
auto current_suggestion_index = m_next_suggestion_index ;
if ( m_next_suggestion_index < m_suggestions . size ( ) ) {
2020-04-20 22:05:31 +04:30
auto can_complete = m_next_suggestion_invariant_offset < = m_largest_common_suggestion_prefix_length ;
2020-04-19 18:32:28 +04:30
if ( ! m_last_shown_suggestion . text . is_null ( ) ) {
2020-04-19 15:55:17 +04:30
size_t actual_offset ;
size_t shown_length = m_last_shown_suggestion_display_length ;
switch ( m_times_tab_pressed ) {
case 1 :
actual_offset = m_cursor ;
break ;
case 2 :
actual_offset = m_cursor - m_largest_common_suggestion_prefix_length + m_next_suggestion_invariant_offset ;
if ( can_complete )
2020-05-21 18:06:43 +04:30
shown_length = m_largest_common_suggestion_prefix_length + m_last_shown_suggestion . trivia_view . length ( ) ;
2020-04-19 15:55:17 +04:30
break ;
default :
if ( m_last_shown_suggestion_display_length = = 0 )
actual_offset = m_cursor ;
else
actual_offset = m_cursor - m_last_shown_suggestion_display_length + m_next_suggestion_invariant_offset ;
break ;
}
for ( size_t i = m_next_suggestion_invariant_offset ; i < shown_length ; + + i )
2020-05-18 02:25:58 +04:30
remove_at_index ( actual_offset ) ;
2020-05-21 18:06:43 +04:30
2020-04-11 19:02:15 +04:30
m_cursor = actual_offset ;
2020-04-20 17:20:31 +04:30
m_inline_search_cursor = m_cursor ;
2020-04-11 19:02:15 +04:30
m_refresh_needed = true ;
}
m_last_shown_suggestion = m_suggestions [ m_next_suggestion_index ] ;
2020-05-21 18:06:43 +04:30
if ( m_last_shown_suggestion_display_length )
m_last_shown_suggestion . token_start_index = token_start - m_next_suggestion_static_offset - m_last_shown_suggestion_display_length ;
else
m_last_shown_suggestion . token_start_index = token_start - m_next_suggestion_static_offset - m_next_suggestion_invariant_offset ;
m_last_shown_suggestion_display_length = m_last_shown_suggestion . text_view . length ( ) ;
2020-04-19 15:55:17 +04:30
m_last_shown_suggestion_was_complete = true ;
if ( m_times_tab_pressed = = 1 ) {
// This is the first time, so only auto-complete *if possible*
if ( can_complete ) {
2020-05-21 18:06:43 +04:30
insert ( m_last_shown_suggestion . text_view . substring_view ( m_next_suggestion_invariant_offset , m_largest_common_suggestion_prefix_length - m_next_suggestion_invariant_offset ) ) ;
2020-04-19 15:55:17 +04:30
m_last_shown_suggestion_display_length = m_largest_common_suggestion_prefix_length ;
// do not increment the suggestion index, as the first tab should only be a *peek*
if ( m_suggestions . size ( ) = = 1 ) {
// if there's one suggestion, commit and forget
m_times_tab_pressed = 0 ;
2020-04-19 18:32:28 +04:30
// add in the trivia of the last selected suggestion
2020-05-21 18:06:43 +04:30
insert ( m_last_shown_suggestion . trivia_view ) ;
2020-05-21 05:14:34 +04:30
m_last_shown_suggestion_display_length = 0 ;
readjust_anchored_styles ( m_last_shown_suggestion . token_start_index , ModificationKind : : ForcedOverlapRemoval ) ;
2020-05-18 02:25:58 +04:30
stylize ( { m_last_shown_suggestion . token_start_index , m_cursor , Span : : Mode : : CodepointOriented } , m_last_shown_suggestion . style ) ;
2020-04-19 15:55:17 +04:30
}
} else {
m_last_shown_suggestion_display_length = 0 ;
}
+ + m_times_tab_pressed ;
m_last_shown_suggestion_was_complete = false ;
} else {
2020-05-21 18:06:43 +04:30
insert ( m_last_shown_suggestion . text_view . substring_view ( m_next_suggestion_invariant_offset , m_last_shown_suggestion . text_view . length ( ) - m_next_suggestion_invariant_offset ) ) ;
2020-04-19 18:32:28 +04:30
// add in the trivia of the last selected suggestion
2020-05-21 18:06:43 +04:30
insert ( m_last_shown_suggestion . trivia_view ) ;
m_last_shown_suggestion_display_length + = m_last_shown_suggestion . trivia_view . length ( ) ;
2020-04-19 15:55:17 +04:30
if ( m_tab_direction = = TabDirection : : Forward )
increment_suggestion_index ( ) ;
else
decrement_suggestion_index ( ) ;
}
2020-04-11 19:02:15 +04:30
} else {
m_next_suggestion_index = 0 ;
}
2020-03-30 21:29:04 +04:30
2020-04-11 19:02:15 +04:30
if ( m_times_tab_pressed > 1 & & ! m_suggestions . is_empty ( ) ) {
2020-03-30 21:29:04 +04:30
size_t longest_suggestion_length = 0 ;
2020-05-21 18:06:43 +04:30
size_t longest_suggestion_byte_length = 0 ;
2020-05-11 11:55:42 +04:30
size_t start_index = 0 ;
2020-03-30 21:29:04 +04:30
2020-04-14 22:18:56 +04:30
for ( auto & suggestion : m_suggestions ) {
2020-05-12 03:29:59 +04:30
if ( start_index + + < m_last_displayed_suggestion_index )
2020-05-11 11:55:42 +04:30
continue ;
2020-05-21 18:06:43 +04:30
longest_suggestion_length = max ( longest_suggestion_length , suggestion . text_view . length ( ) ) ;
longest_suggestion_byte_length = max ( longest_suggestion_byte_length , suggestion . text_string . length ( ) ) ;
2020-04-14 22:18:56 +04:30
}
2020-03-30 21:29:04 +04:30
size_t num_printed = 0 ;
2020-04-11 13:29:55 +04:30
size_t lines_used { 1 } ;
2020-04-11 19:02:15 +04:30
size_t index { 0 } ;
2020-04-14 22:18:56 +04:30
vt_save_cursor ( ) ;
vt_clear_lines ( 0 , m_lines_used_for_last_suggestions ) ;
vt_restore_cursor ( ) ;
auto spans_entire_line { false } ;
auto max_line_count = ( m_cached_prompt_length + longest_suggestion_length + m_num_columns - 1 ) / m_num_columns ;
if ( longest_suggestion_length > = m_num_columns - 2 ) {
spans_entire_line = true ;
// we should make enough space for the biggest entry in
// the suggestion list to fit in the prompt line
auto start = max_line_count - m_prompt_lines_at_suggestion_initiation ;
for ( size_t i = start ; i < max_line_count ; + + i ) {
putchar ( ' \n ' ) ;
}
lines_used + = max_line_count ;
longest_suggestion_length = 0 ;
}
vt_move_absolute ( max_line_count + m_origin_x , 1 ) ;
2020-04-11 19:02:15 +04:30
for ( auto & suggestion : m_suggestions ) {
2020-05-11 11:55:42 +04:30
if ( index < m_last_displayed_suggestion_index ) {
+ + index ;
continue ;
}
2020-05-21 18:06:43 +04:30
size_t next_column = num_printed + suggestion . text_view . length ( ) + longest_suggestion_length + 2 ;
2020-03-30 21:29:04 +04:30
if ( next_column > m_num_columns ) {
2020-05-21 18:06:43 +04:30
auto lines = ( suggestion . text_view . length ( ) + m_num_columns - 1 ) / m_num_columns ;
2020-04-14 22:18:56 +04:30
lines_used + = lines ;
2020-03-30 21:29:04 +04:30
putchar ( ' \n ' ) ;
num_printed = 0 ;
}
2020-04-11 19:02:15 +04:30
// show just enough suggestions to fill up the screen
// without moving the prompt out of view
2020-04-14 22:18:56 +04:30
if ( lines_used + m_prompt_lines_at_suggestion_initiation > = m_num_lines )
2020-04-11 19:02:15 +04:30
break ;
2020-04-19 15:55:17 +04:30
// only apply colour to the selection if something is *actually* added to the buffer
if ( m_last_shown_suggestion_was_complete & & index = = current_suggestion_index ) {
2020-05-10 12:27:36 +04:30
vt_apply_style ( { Style : : Foreground ( Style : : XtermColor : : Blue ) } ) ;
2020-04-11 19:02:15 +04:30
fflush ( stdout ) ;
}
2020-04-14 22:18:56 +04:30
if ( spans_entire_line ) {
num_printed + = m_num_columns ;
2020-05-21 18:06:43 +04:30
fprintf ( stderr , " %s " , suggestion . text_string . characters ( ) ) ;
2020-04-14 22:18:56 +04:30
} else {
2020-05-21 18:06:43 +04:30
fprintf ( stderr , " %-*s " , static_cast < int > ( longest_suggestion_byte_length ) + 2 , suggestion . text_string . characters ( ) ) ;
num_printed + = longest_suggestion_length + 2 ;
2020-04-14 22:18:56 +04:30
}
2020-04-11 19:02:15 +04:30
2020-04-19 15:55:17 +04:30
if ( m_last_shown_suggestion_was_complete & & index = = current_suggestion_index ) {
2020-05-18 02:25:58 +04:30
vt_apply_style ( Style : : reset_style ( ) ) ;
2020-04-11 19:02:15 +04:30
fflush ( stdout ) ;
}
2020-05-11 11:55:42 +04:30
2020-04-11 19:02:15 +04:30
+ + index ;
2020-03-30 21:29:04 +04:30
}
2020-04-11 19:02:15 +04:30
m_lines_used_for_last_suggestions = lines_used ;
2020-03-30 21:29:04 +04:30
2020-05-11 11:55:42 +04:30
// if we filled the screen, move back the origin
2020-04-11 13:29:55 +04:30
if ( m_origin_x + lines_used > = m_num_lines ) {
m_origin_x = m_num_lines - lines_used ;
}
2020-05-11 11:55:42 +04:30
- - index ;
// cycle pages of suggestions
if ( index = = current_suggestion_index )
m_last_displayed_suggestion_index = index ;
if ( m_last_displayed_suggestion_index > = m_suggestions . size ( ) - 1 )
m_last_displayed_suggestion_index = 0 ;
2020-03-30 21:29:04 +04:30
}
2020-04-12 22:24:21 +04:30
if ( m_suggestions . size ( ) < 2 ) {
// we have none, or just one suggestion
// we should just commit that and continue
// after it, as if it were auto-completed
2020-05-21 15:15:56 +04:30
suggest ( 0 , 0 , Span : : CodepointOriented ) ;
2020-04-12 22:24:21 +04:30
m_last_shown_suggestion = String : : empty ( ) ;
2020-04-19 15:55:17 +04:30
m_last_shown_suggestion_display_length = 0 ;
2020-04-12 22:24:21 +04:30
m_suggestions . clear ( ) ;
m_times_tab_pressed = 0 ;
2020-05-11 11:55:42 +04:30
m_last_displayed_suggestion_index = 0 ;
2020-04-12 22:24:21 +04:30
}
2020-03-30 21:29:04 +04:30
continue ;
}
2020-04-11 13:29:55 +04:30
if ( m_times_tab_pressed ) {
2020-05-18 02:25:58 +04:30
// Apply the style of the last suggestion
2020-05-21 05:14:34 +04:30
readjust_anchored_styles ( m_last_shown_suggestion . token_start_index , ModificationKind : : ForcedOverlapRemoval ) ;
2020-05-18 02:25:58 +04:30
stylize ( { m_last_shown_suggestion . token_start_index , m_cursor , Span : : Mode : : CodepointOriented } , m_last_shown_suggestion . style ) ;
2020-04-11 13:29:55 +04:30
// we probably have some suggestions drawn
// let's clean them up
if ( m_lines_used_for_last_suggestions ) {
2020-04-11 17:22:24 +04:30
vt_clear_lines ( 0 , m_lines_used_for_last_suggestions ) ;
2020-04-11 19:02:15 +04:30
reposition_cursor ( ) ;
2020-04-11 13:29:55 +04:30
m_refresh_needed = true ;
m_lines_used_for_last_suggestions = 0 ;
}
2020-04-19 15:55:17 +04:30
m_last_shown_suggestion_display_length = 0 ;
2020-04-11 19:02:15 +04:30
m_last_shown_suggestion = String : : empty ( ) ;
2020-05-11 11:55:42 +04:30
m_last_displayed_suggestion_index = 0 ;
2020-04-11 19:02:15 +04:30
m_suggestions . clear ( ) ;
2020-05-21 15:15:56 +04:30
suggest ( 0 , 0 , Span : : CodepointOriented ) ;
2020-04-11 13:29:55 +04:30
}
2020-03-30 21:29:04 +04:30
m_times_tab_pressed = 0 ; // Safe to say if we get here, the user didn't press TAB
auto do_backspace = [ & ] {
2020-04-19 23:34:58 +04:30
if ( m_is_searching ) {
return ;
}
2020-03-30 21:29:04 +04:30
if ( m_cursor = = 0 ) {
fputc ( ' \a ' , stdout ) ;
fflush ( stdout ) ;
return ;
}
2020-05-18 02:25:58 +04:30
remove_at_index ( m_cursor - 1 ) ;
2020-03-30 21:29:04 +04:30
- - m_cursor ;
2020-04-20 17:20:31 +04:30
m_inline_search_cursor = m_cursor ;
2020-04-09 07:44:04 +04:30
// we will have to redraw :(
m_refresh_needed = true ;
2020-03-30 21:29:04 +04:30
} ;
2020-05-18 14:47:19 +04:30
if ( codepoint = = 8 | | codepoint = = m_termios . c_cc [ VERASE ] ) {
2020-03-30 21:29:04 +04:30
do_backspace ( ) ;
continue ;
}
2020-05-18 14:47:19 +04:30
if ( codepoint = = m_termios . c_cc [ VWERASE ] ) {
2020-03-30 21:29:04 +04:30
bool has_seen_nonspace = false ;
while ( m_cursor > 0 ) {
if ( isspace ( m_buffer [ m_cursor - 1 ] ) ) {
if ( has_seen_nonspace )
break ;
} else {
has_seen_nonspace = true ;
}
do_backspace ( ) ;
}
continue ;
}
2020-05-18 14:47:19 +04:30
if ( codepoint = = m_termios . c_cc [ VKILL ] ) {
2020-04-09 07:44:04 +04:30
for ( size_t i = 0 ; i < m_cursor ; + + i )
2020-05-18 02:25:58 +04:30
remove_at_index ( 0 ) ;
2020-04-09 07:44:04 +04:30
m_cursor = 0 ;
m_refresh_needed = true ;
2020-03-30 21:29:04 +04:30
continue ;
}
2020-04-19 15:55:17 +04:30
// ^L
2020-05-18 14:47:19 +04:30
if ( codepoint = = 0xc ) {
2020-03-30 21:29:04 +04:30
printf ( " \033 [3J \033 [H \033 [2J " ) ; // Clear screen.
2020-04-09 07:44:04 +04:30
vt_move_absolute ( 1 , 1 ) ;
2020-04-11 17:22:24 +04:30
m_origin_x = 1 ;
m_origin_y = 1 ;
2020-04-09 07:44:04 +04:30
m_refresh_needed = true ;
2020-03-30 21:29:04 +04:30
continue ;
}
2020-04-19 15:55:17 +04:30
// ^A
2020-05-18 14:47:19 +04:30
if ( codepoint = = 0x01 ) {
2020-04-09 07:44:04 +04:30
m_cursor = 0 ;
2020-03-30 21:29:04 +04:30
continue ;
}
2020-04-19 23:34:58 +04:30
// ^R
2020-05-18 14:47:19 +04:30
if ( codepoint = = 0x12 ) {
2020-04-19 23:34:58 +04:30
if ( m_is_searching ) {
// how did we get here?
ASSERT_NOT_REACHED ( ) ;
} else {
m_is_searching = true ;
m_search_offset = 0 ;
2020-04-20 17:53:24 +04:30
m_pre_search_buffer . clear ( ) ;
2020-05-18 14:47:19 +04:30
for ( auto codepoint : m_buffer )
m_pre_search_buffer . append ( codepoint ) ;
2020-04-19 23:34:58 +04:30
m_pre_search_cursor = m_cursor ;
2020-04-30 08:19:47 +04:30
m_search_editor = make < Editor > ( Configuration { Configuration : : Eager , m_configuration . split_mechanism } ) ; // Has anyone seen 'Inception'?
2020-04-19 23:34:58 +04:30
m_search_editor - > on_display_refresh = [ this ] ( Editor & search_editor ) {
2020-05-18 13:47:34 +04:30
StringBuilder builder ;
builder . append ( Utf32View { search_editor . buffer ( ) . data ( ) , search_editor . buffer ( ) . size ( ) } ) ;
search ( builder . build ( ) ) ;
2020-04-19 23:34:58 +04:30
refresh_display ( ) ;
return ;
} ;
// whenever the search editor gets a ^R, cycle between history entries
m_search_editor - > register_character_input_callback ( 0x12 , [ this ] ( Editor & search_editor ) {
+ + m_search_offset ;
search_editor . m_refresh_needed = true ;
return false ; // Do not process this key event
} ) ;
// whenever the search editor gets a backspace, cycle back between history entries
// unless we're at the zeroth entry, in which case, allow the deletion
m_search_editor - > register_character_input_callback ( m_termios . c_cc [ VERASE ] , [ this ] ( Editor & search_editor ) {
if ( m_search_offset > 0 ) {
- - m_search_offset ;
search_editor . m_refresh_needed = true ;
return false ; // Do not process this key event
}
return true ;
} ) ;
2020-05-02 08:12:29 +04:30
// ^L - This is a source of issues, as the search editor refreshes first,
// and we end up with the wrong order of prompts, so we will first refresh
// ourselves, then refresh the search editor, and then tell him not to process
// this event
m_search_editor - > register_character_input_callback ( 0x0c , [ this ] ( auto & search_editor ) {
printf ( " \033 [3J \033 [H \033 [2J " ) ; // Clear screen.
// refresh our own prompt
m_origin_x = 1 ;
m_origin_y = 1 ;
m_refresh_needed = true ;
refresh_display ( ) ;
// move the search prompt below ours
// and tell it to redraw itself
search_editor . m_origin_x = 2 ;
search_editor . m_origin_y = 1 ;
search_editor . m_refresh_needed = true ;
return false ;
} ) ;
2020-04-19 23:34:58 +04:30
// quit without clearing the current buffer
m_search_editor - > register_character_input_callback ( ' \t ' , [ this ] ( Editor & search_editor ) {
search_editor . finish ( ) ;
m_reset_buffer_on_search_end = false ;
return false ;
} ) ;
printf ( " \n " ) ;
fflush ( stdout ) ;
auto search_prompt = " \x1b [32msearch: \x1b [0m " ;
auto search_string = m_search_editor - > get_line ( search_prompt ) ;
2020-04-20 17:53:24 +04:30
2020-04-19 23:34:58 +04:30
m_search_editor = nullptr ;
m_is_searching = false ;
m_search_offset = 0 ;
// manually cleanup the search line
reposition_cursor ( ) ;
2020-05-21 18:06:43 +04:30
auto search_string_codepoint_length = Utf8View { search_string } . length_in_codepoints ( ) ;
vt_clear_lines ( 0 , ( search_string_codepoint_length + actual_rendered_string_length ( search_prompt ) + m_num_columns - 1 ) / m_num_columns ) ;
2020-04-19 23:34:58 +04:30
reposition_cursor ( ) ;
2020-05-21 18:06:43 +04:30
if ( ! m_reset_buffer_on_search_end | | search_string_codepoint_length = = 0 ) {
2020-04-19 23:34:58 +04:30
// if the entry was empty, or we purposely quit without a newline,
// do not return anything
// instead, just end the search
end_search ( ) ;
continue ;
}
// return the string
finish ( ) ;
continue ;
}
continue ;
}
2020-04-19 15:55:17 +04:30
// Normally ^D
2020-05-18 14:47:19 +04:30
if ( codepoint = = m_termios . c_cc [ VEOF ] ) {
2020-03-30 21:29:04 +04:30
if ( m_buffer . is_empty ( ) ) {
printf ( " <EOF> \n " ) ;
2020-04-19 23:34:58 +04:30
if ( ! m_always_refresh ) // this is a little off, but it'll do for now
exit ( 0 ) ;
2020-03-30 21:29:04 +04:30
}
continue ;
}
2020-04-19 15:55:17 +04:30
// ^E
2020-05-18 14:47:19 +04:30
if ( codepoint = = 0x05 ) {
2020-04-09 07:44:04 +04:30
m_cursor = m_buffer . size ( ) ;
2020-03-30 21:29:04 +04:30
continue ;
}
2020-05-18 14:47:19 +04:30
if ( codepoint = = ' \n ' ) {
2020-04-19 23:34:58 +04:30
finish ( ) ;
continue ;
2020-03-30 21:29:04 +04:30
}
2020-05-18 14:47:19 +04:30
insert ( codepoint ) ;
2020-03-30 21:29:04 +04:30
}
2020-05-18 13:47:34 +04:30
2020-05-18 14:47:19 +04:30
if ( consumed_codepoints = = m_incomplete_data . size ( ) ) {
2020-05-18 13:47:34 +04:30
m_incomplete_data . clear ( ) ;
} else {
2020-05-18 14:47:19 +04:30
for ( size_t i = 0 ; i < consumed_codepoints ; + + i )
2020-05-18 13:47:34 +04:30
m_incomplete_data . take_first ( ) ;
}
2020-03-30 21:29:04 +04:30
}
}
2020-04-20 17:20:31 +04:30
bool Editor : : search ( const StringView & phrase , bool allow_empty , bool from_beginning )
{
int last_matching_offset = - 1 ;
// do not search for empty strings
if ( allow_empty | | phrase . length ( ) > 0 ) {
size_t search_offset = m_search_offset ;
for ( size_t i = m_history_cursor ; i > 0 ; - - i ) {
auto contains = from_beginning ? m_history [ i - 1 ] . starts_with ( phrase ) : m_history [ i - 1 ] . contains ( phrase ) ;
if ( contains ) {
last_matching_offset = i - 1 ;
if ( search_offset = = 0 )
break ;
- - search_offset ;
}
}
if ( last_matching_offset = = - 1 ) {
fputc ( ' \a ' , stdout ) ;
fflush ( stdout ) ;
}
}
m_buffer . clear ( ) ;
m_cursor = 0 ;
if ( last_matching_offset > = 0 ) {
insert ( m_history [ last_matching_offset ] ) ;
}
// always needed
m_refresh_needed = true ;
return last_matching_offset > = 0 ;
}
2020-04-11 14:06:46 +04:30
void Editor : : recalculate_origin ( )
{
// changing the columns can affect our origin if
// the new size is smaller than our prompt, which would
// cause said prompt to take up more space, so we should
// compensate for that
if ( m_cached_prompt_length > = m_num_columns ) {
auto added_lines = ( m_cached_prompt_length + 1 ) / m_num_columns - 1 ;
m_origin_x + = added_lines ;
}
// we also need to recalculate our cursor position
// but that will be calculated and applied at the next
// refresh cycle
}
2020-04-19 23:34:58 +04:30
void Editor : : cleanup ( )
2020-04-05 06:41:33 +04:30
{
2020-04-19 23:34:58 +04:30
vt_move_relative ( 0 , m_pending_chars . size ( ) - m_chars_inserted_in_the_middle ) ;
auto current_line = cursor_line ( ) ;
vt_clear_lines ( current_line - 1 , num_lines ( ) - current_line ) ;
vt_move_relative ( - num_lines ( ) + 1 , - offset_in_line ( ) - m_old_prompt_length - m_pending_chars . size ( ) + m_chars_inserted_in_the_middle ) ;
} ;
2020-04-10 09:53:22 +04:30
2020-04-19 23:34:58 +04:30
void Editor : : refresh_display ( )
{
2020-04-10 09:53:22 +04:30
auto has_cleaned_up = false ;
// someone changed the window size, figure it out
// and react to it, we might need to redraw
if ( m_was_resized ) {
auto previous_num_columns = m_num_columns ;
struct winsize ws ;
2020-04-11 13:29:55 +04:30
if ( ioctl ( STDOUT_FILENO , TIOCGWINSZ , & ws ) < 0 ) {
2020-04-10 09:53:22 +04:30
m_num_columns = 80 ;
2020-04-11 13:29:55 +04:30
m_num_lines = 25 ;
} else {
2020-04-10 09:53:22 +04:30
m_num_columns = ws . ws_col ;
2020-04-11 13:29:55 +04:30
m_num_lines = ws . ws_row ;
}
2020-04-10 09:53:22 +04:30
if ( previous_num_columns ! = m_num_columns ) {
// we need to cleanup and redo everything
m_cached_prompt_valid = false ;
m_refresh_needed = true ;
swap ( previous_num_columns , m_num_columns ) ;
2020-04-11 14:06:46 +04:30
recalculate_origin ( ) ;
2020-04-10 09:53:22 +04:30
cleanup ( ) ;
swap ( previous_num_columns , m_num_columns ) ;
has_cleaned_up = true ;
}
}
2020-04-09 07:44:04 +04:30
// do not call hook on pure cursor movement
if ( m_cached_prompt_valid & & ! m_refresh_needed & & m_pending_chars . size ( ) = = 0 ) {
// probably just moving around
reposition_cursor ( ) ;
m_cached_buffer_size = m_buffer . size ( ) ;
return ;
}
2020-04-05 06:41:33 +04:30
if ( on_display_refresh )
on_display_refresh ( * this ) ;
2020-04-09 07:44:04 +04:30
if ( m_cached_prompt_valid ) {
if ( ! m_refresh_needed & & m_cursor = = m_buffer . size ( ) ) {
// just write the characters out and continue
// no need to refresh the entire line
char null = 0 ;
m_pending_chars . append ( & null , 1 ) ;
fputs ( ( char * ) m_pending_chars . data ( ) , stdout ) ;
m_pending_chars . clear ( ) ;
m_drawn_cursor = m_cursor ;
m_cached_buffer_size = m_buffer . size ( ) ;
fflush ( stdout ) ;
return ;
}
2020-04-05 06:41:33 +04:30
}
// ouch, reflow entire line
// FIXME: handle multiline stuff
2020-04-10 09:53:22 +04:30
if ( ! has_cleaned_up ) {
cleanup ( ) ;
}
2020-04-11 17:22:24 +04:30
vt_move_absolute ( m_origin_x , m_origin_y ) ;
2020-04-09 07:44:04 +04:30
fputs ( m_new_prompt . characters ( ) , stdout ) ;
2020-04-05 06:41:33 +04:30
vt_clear_to_end_of_line ( ) ;
HashMap < u32 , Style > empty_styles { } ;
2020-05-18 13:47:34 +04:30
StringBuilder builder ;
2020-04-05 06:41:33 +04:30
for ( size_t i = 0 ; i < m_buffer . size ( ) ; + + i ) {
auto ends = m_spans_ending . get ( i ) . value_or ( empty_styles ) ;
auto starts = m_spans_starting . get ( i ) . value_or ( empty_styles ) ;
2020-05-18 02:25:58 +04:30
auto anchored_ends = m_anchored_spans_ending . get ( i ) . value_or ( empty_styles ) ;
auto anchored_starts = m_anchored_spans_starting . get ( i ) . value_or ( empty_styles ) ;
if ( ends . size ( ) | | anchored_ends . size ( ) ) {
Style style ;
for ( auto & applicable_style : ends )
style . unify_with ( applicable_style . value ) ;
for ( auto & applicable_style : anchored_ends )
style . unify_with ( applicable_style . value ) ;
// Disable any style that should be turned off
vt_apply_style ( style , false ) ;
2020-04-05 06:41:33 +04:30
// go back to defaults
2020-05-18 02:25:58 +04:30
style = find_applicable_style ( i ) ;
vt_apply_style ( style , true ) ;
2020-04-05 06:41:33 +04:30
}
2020-05-18 02:25:58 +04:30
if ( starts . size ( ) | | anchored_starts . size ( ) ) {
Style style ;
for ( auto & applicable_style : starts )
style . unify_with ( applicable_style . value ) ;
for ( auto & applicable_style : anchored_starts )
style . unify_with ( applicable_style . value ) ;
2020-04-05 06:41:33 +04:30
// set new options
2020-05-18 02:25:58 +04:30
vt_apply_style ( style , true ) ;
2020-04-05 06:41:33 +04:30
}
2020-05-18 13:47:34 +04:30
builder . clear ( ) ;
builder . append ( Utf32View { & m_buffer [ i ] , 1 } ) ;
fputs ( builder . to_string ( ) . characters ( ) , stdout ) ;
2020-04-05 06:41:33 +04:30
}
2020-05-18 02:25:58 +04:30
vt_apply_style ( Style : : reset_style ( ) ) ; // don't bleed to EOL
2020-04-05 06:41:33 +04:30
m_pending_chars . clear ( ) ;
m_refresh_needed = false ;
2020-04-09 07:44:04 +04:30
m_cached_buffer_size = m_buffer . size ( ) ;
2020-04-05 06:41:33 +04:30
m_chars_inserted_in_the_middle = 0 ;
2020-04-09 07:44:04 +04:30
if ( ! m_cached_prompt_valid ) {
m_cached_prompt_valid = true ;
}
reposition_cursor ( ) ;
fflush ( stdout ) ;
}
2020-05-18 02:25:58 +04:30
void Editor : : strip_styles ( bool strip_anchored )
{
m_spans_starting . clear ( ) ;
m_spans_ending . clear ( ) ;
if ( strip_anchored ) {
m_anchored_spans_starting . clear ( ) ;
m_anchored_spans_ending . clear ( ) ;
}
m_refresh_needed = true ;
}
2020-04-09 07:44:04 +04:30
void Editor : : reposition_cursor ( )
{
m_drawn_cursor = m_cursor ;
auto line = cursor_line ( ) - 1 ;
auto column = offset_in_line ( ) ;
vt_move_absolute ( line + m_origin_x , column + m_origin_y ) ;
}
void Editor : : vt_move_absolute ( u32 x , u32 y )
{
printf ( " \033 [%d;%dH " , x , y ) ;
fflush ( stdout ) ;
2020-04-05 06:41:33 +04:30
}
void Editor : : vt_move_relative ( int x , int y )
{
char x_op = ' A ' , y_op = ' D ' ;
2020-04-09 07:44:04 +04:30
2020-04-05 06:41:33 +04:30
if ( x > 0 )
x_op = ' B ' ;
else
x = - x ;
if ( y > 0 )
y_op = ' C ' ;
else
y = - y ;
if ( x > 0 )
printf ( " \033 [%d%c " , x , x_op ) ;
if ( y > 0 )
printf ( " \033 [%d%c " , y , y_op ) ;
}
Style Editor : : find_applicable_style ( size_t offset ) const
{
2020-05-18 02:25:58 +04:30
// walk through our styles and merge all that fit in the offset
Style style ;
auto unify = [ & ] ( auto & entry ) {
if ( entry . key > = offset )
return ;
2020-04-05 06:41:33 +04:30
for ( auto & style_value : entry . value ) {
if ( style_value . key < = offset )
2020-05-18 02:25:58 +04:30
return ;
style . unify_with ( style_value . value ) ;
2020-04-05 06:41:33 +04:30
}
2020-05-18 02:25:58 +04:30
} ;
for ( auto & entry : m_spans_starting ) {
unify ( entry ) ;
2020-04-05 06:41:33 +04:30
}
2020-05-18 02:25:58 +04:30
for ( auto & entry : m_anchored_spans_starting ) {
unify ( entry ) ;
}
return style ;
2020-04-05 06:41:33 +04:30
}
2020-05-10 12:27:36 +04:30
String Style : : Background : : to_vt_escape ( ) const
{
2020-05-18 02:25:58 +04:30
if ( is_default ( ) )
return " " ;
2020-05-10 12:27:36 +04:30
if ( m_is_rgb ) {
return String : : format ( " \033 [48;2;%d;%d;%dm " , m_rgb_color [ 0 ] , m_rgb_color [ 1 ] , m_rgb_color [ 2 ] ) ;
} else {
return String : : format ( " \033 [%dm " , ( u8 ) m_xterm_color + 40 ) ;
}
}
String Style : : Foreground : : to_vt_escape ( ) const
{
2020-05-18 02:25:58 +04:30
if ( is_default ( ) )
return " " ;
2020-05-10 12:27:36 +04:30
if ( m_is_rgb ) {
return String : : format ( " \033 [38;2;%d;%d;%dm " , m_rgb_color [ 0 ] , m_rgb_color [ 1 ] , m_rgb_color [ 2 ] ) ;
} else {
return String : : format ( " \033 [%dm " , ( u8 ) m_xterm_color + 30 ) ;
}
}
2020-05-18 02:25:58 +04:30
String Style : : Hyperlink : : to_vt_escape ( bool starting ) const
{
if ( is_empty ( ) )
return " " ;
return String : : format ( " \033 ]8;;%s \033 \\ " , starting ? m_link . characters ( ) : " " ) ;
}
void Style : : unify_with ( const Style & other , bool prefer_other )
{
// unify colors
if ( prefer_other | | m_background . is_default ( ) )
m_background = other . background ( ) ;
if ( prefer_other | | m_foreground . is_default ( ) )
m_foreground = other . foreground ( ) ;
// unify graphic renditions
if ( other . bold ( ) )
set ( Bold ) ;
if ( other . italic ( ) )
set ( Italic ) ;
if ( other . underline ( ) )
set ( Underline ) ;
// unify links
if ( prefer_other | | m_hyperlink . is_empty ( ) )
m_hyperlink = other . hyperlink ( ) ;
}
String Style : : to_string ( ) const
{
StringBuilder builder ;
builder . append ( " Style { " ) ;
if ( ! m_foreground . is_default ( ) ) {
builder . append ( " Foreground( " ) ;
if ( m_foreground . m_is_rgb ) {
builder . join ( " , " , m_foreground . m_rgb_color ) ;
} else {
builder . appendf ( " (XtermColor) %d " , m_foreground . m_xterm_color ) ;
}
builder . append ( " ), " ) ;
}
if ( ! m_background . is_default ( ) ) {
builder . append ( " Background( " ) ;
if ( m_background . m_is_rgb ) {
builder . join ( ' ' , m_background . m_rgb_color ) ;
} else {
builder . appendf ( " (XtermColor) %d " , m_background . m_xterm_color ) ;
}
builder . append ( " ), " ) ;
}
if ( bold ( ) )
builder . append ( " Bold, " ) ;
if ( underline ( ) )
builder . append ( " Underline, " ) ;
if ( italic ( ) )
builder . append ( " Italic, " ) ;
if ( ! m_hyperlink . is_empty ( ) )
builder . appendf ( " Hyperlink( \" %s \" ), " , m_hyperlink . m_link . characters ( ) ) ;
builder . append ( " } " ) ;
return builder . build ( ) ;
}
void Editor : : vt_apply_style ( const Style & style , bool is_starting )
2020-04-05 06:41:33 +04:30
{
2020-05-18 02:25:58 +04:30
if ( is_starting ) {
printf (
" \033 [%d;%d;%dm%s%s%s " ,
style . bold ( ) ? 1 : 22 ,
style . underline ( ) ? 4 : 24 ,
style . italic ( ) ? 3 : 23 ,
style . background ( ) . to_vt_escape ( ) . characters ( ) ,
style . foreground ( ) . to_vt_escape ( ) . characters ( ) ,
style . hyperlink ( ) . to_vt_escape ( true ) . characters ( ) ) ;
} else {
printf ( " %s " , style . hyperlink ( ) . to_vt_escape ( false ) . characters ( ) ) ;
}
2020-04-05 06:41:33 +04:30
}
void Editor : : vt_clear_lines ( size_t count_above , size_t count_below )
{
// go down count_below lines
if ( count_below > 0 )
printf ( " \033 [%dB " , ( int ) count_below ) ;
// then clear lines going upwards
2020-04-09 07:44:04 +04:30
for ( size_t i = count_below + count_above ; i > 0 ; - - i )
fputs ( i = = 1 ? " \033 [2K " : " \033 [2K \033 [A " , stdout ) ;
2020-04-05 06:41:33 +04:30
}
2020-03-31 13:34:06 +02:00
void Editor : : vt_save_cursor ( )
2020-03-30 21:29:04 +04:30
{
fputs ( " \033 [s " , stdout ) ;
fflush ( stdout ) ;
}
2020-03-31 13:34:06 +02:00
void Editor : : vt_restore_cursor ( )
2020-03-30 21:29:04 +04:30
{
fputs ( " \033 [u " , stdout ) ;
fflush ( stdout ) ;
}
2020-03-31 13:34:06 +02:00
void Editor : : vt_clear_to_end_of_line ( )
2020-03-30 21:29:04 +04:30
{
fputs ( " \033 [K " , stdout ) ;
fflush ( stdout ) ;
}
2020-04-09 07:44:04 +04:30
size_t Editor : : actual_rendered_string_length ( const StringView & string ) const
{
size_t length { 0 } ;
enum VTState {
Free = 1 ,
Escape = 3 ,
Bracket = 5 ,
BracketArgsSemi = 7 ,
Title = 9 ,
} state { Free } ;
2020-05-18 13:47:34 +04:30
Utf8View view { string } ;
auto it = view . begin ( ) ;
for ( size_t i = 0 ; i < view . length_in_codepoints ( ) ; + + i , + + it ) {
auto c = * it ;
2020-04-09 07:44:04 +04:30
switch ( state ) {
case Free :
if ( c = = ' \x1b ' ) {
// escape
state = Escape ;
continue ;
}
2020-05-10 11:30:38 +04:30
if ( c = = ' \r ' | | c = = ' \n ' ) {
// reset length to 0, since we either overwrite, or are on a newline
length = 0 ;
continue ;
}
2020-04-09 07:44:04 +04:30
// FIXME: This will not support anything sophisticated
+ + length ;
break ;
case Escape :
if ( c = = ' ] ' ) {
2020-05-18 13:47:34 +04:30
+ + i ;
+ + it ;
if ( * it = = ' 0 ' )
2020-04-09 07:44:04 +04:30
state = Title ;
continue ;
}
if ( c = = ' [ ' ) {
state = Bracket ;
continue ;
}
// FIXME: This does not support non-VT (aside from set-title) escapes
break ;
case Bracket :
if ( isdigit ( c ) ) {
state = BracketArgsSemi ;
continue ;
}
break ;
case BracketArgsSemi :
if ( c = = ' ; ' ) {
state = Bracket ;
continue ;
}
if ( ! isdigit ( c ) )
state = Free ;
break ;
case Title :
if ( c = = 7 )
state = Free ;
break ;
}
}
return length ;
}
Vector < size_t , 2 > Editor : : vt_dsr ( )
{
2020-04-29 01:47:41 +04:30
char buf [ 16 ] ;
u32 length { 0 } ;
// read whatever junk there is before talking to the terminal
2020-05-18 13:47:34 +04:30
// and insert them later when we're reading user input
2020-04-29 01:47:41 +04:30
bool more_junk_to_read { false } ;
timeval timeout { 0 , 0 } ;
fd_set readfds ;
FD_ZERO ( & readfds ) ;
FD_SET ( 0 , & readfds ) ;
do {
more_junk_to_read = false ;
( void ) select ( 1 , & readfds , nullptr , nullptr , & timeout ) ;
if ( FD_ISSET ( 0 , & readfds ) ) {
2020-04-29 00:03:19 +02:00
auto nread = read ( 0 , buf , 16 ) ;
2020-05-18 13:47:34 +04:30
m_incomplete_data . append ( buf , nread ) ;
2020-04-29 01:47:41 +04:30
more_junk_to_read = true ;
}
} while ( more_junk_to_read ) ;
2020-04-09 07:44:04 +04:30
fputs ( " \033 [6n " , stdout ) ;
fflush ( stdout ) ;
do {
auto nread = read ( 0 , buf + length , 16 - length ) ;
if ( nread < 0 ) {
2020-04-10 12:58:27 +02:00
if ( errno = = 0 ) {
2020-04-10 09:53:22 +04:30
// ????
continue ;
}
2020-04-09 07:44:04 +04:30
dbg ( ) < < " Error while reading DSR: " < < strerror ( errno ) ;
2020-04-10 09:53:22 +04:30
return { 1 , 1 } ;
2020-04-09 07:44:04 +04:30
}
if ( nread = = 0 ) {
dbg ( ) < < " Terminal DSR issue; received no response " ;
2020-04-10 09:53:22 +04:30
return { 1 , 1 } ;
2020-04-09 07:44:04 +04:30
}
length + = nread ;
} while ( buf [ length - 1 ] ! = ' R ' & & length < 16 ) ;
2020-04-10 09:53:22 +04:30
size_t x { 1 } , y { 1 } ;
2020-04-09 07:44:04 +04:30
if ( buf [ 0 ] = = ' \033 ' & & buf [ 1 ] = = ' [ ' ) {
auto parts = StringView ( buf + 2 , length - 3 ) . split_view ( ' ; ' ) ;
bool ok ;
x = parts [ 0 ] . to_int ( ok ) ;
if ( ! ok ) {
dbg ( ) < < " Terminal DSR issue; received garbage x " ;
}
y = parts [ 1 ] . to_int ( ok ) ;
if ( ! ok ) {
dbg ( ) < < " Terminal DSR issue; received garbage y " ;
}
}
return { x , y } ;
}
2020-05-18 13:47:34 +04:30
2020-05-19 08:42:01 +04:30
String Editor : : line ( size_t up_to_index ) const
2020-05-18 13:47:34 +04:30
{
StringBuilder builder ;
2020-05-19 08:42:01 +04:30
builder . append ( Utf32View { m_buffer . data ( ) , min ( m_buffer . size ( ) , up_to_index ) } ) ;
2020-05-18 13:47:34 +04:30
return builder . build ( ) ;
}
2020-05-19 08:42:01 +04:30
bool Editor : : should_break_token ( Vector < u32 , 1024 > & buffer , size_t index )
{
switch ( m_configuration . split_mechanism ) {
case Configuration : : TokenSplitMechanism : : Spaces :
return buffer [ index ] = = ' ' ;
case Configuration : : TokenSplitMechanism : : UnescapedSpaces :
return buffer [ index ] = = ' ' & & ( index = = 0 | | buffer [ index - 1 ] ! = ' \\ ' ) ;
}
ASSERT_NOT_REACHED ( ) ;
return true ;
} ;
2020-05-18 02:25:58 +04:30
void Editor : : remove_at_index ( size_t index )
{
// see if we have any anchored styles, and reposition them if needed
readjust_anchored_styles ( index , ModificationKind : : Removal ) ;
m_buffer . remove ( index ) ;
}
void Editor : : readjust_anchored_styles ( size_t hint_index , ModificationKind modification )
{
struct Anchor {
Span old_span ;
Span new_span ;
Style style ;
} ;
Vector < Anchor > anchors_to_relocate ;
auto index_shift = modification = = ModificationKind : : Insertion ? 1 : - 1 ;
2020-05-21 05:14:34 +04:30
auto forced_removal = modification = = ModificationKind : : ForcedOverlapRemoval ;
2020-05-18 02:25:58 +04:30
for ( auto & start_entry : m_anchored_spans_starting ) {
for ( auto & end_entry : start_entry . value ) {
2020-05-21 05:14:34 +04:30
if ( forced_removal ) {
2020-05-21 15:15:56 +04:30
if ( start_entry . key < = hint_index & & end_entry . key > hint_index ) {
2020-05-21 05:14:34 +04:30
// remove any overlapping regions
continue ;
}
}
2020-05-18 02:25:58 +04:30
if ( start_entry . key > = hint_index ) {
if ( start_entry . key = = hint_index & & end_entry . key = = hint_index + 1 & & modification = = ModificationKind : : Removal ) {
// remove the anchor, as all its text was wiped
continue ;
}
// shift everything
anchors_to_relocate . append ( { { start_entry . key , end_entry . key , Span : : Mode : : CodepointOriented } , { start_entry . key + index_shift , end_entry . key + index_shift , Span : : Mode : : CodepointOriented } , end_entry . value } ) ;
continue ;
}
if ( end_entry . key > hint_index ) {
// shift just the end
anchors_to_relocate . append ( { { start_entry . key , end_entry . key , Span : : Mode : : CodepointOriented } , { start_entry . key , end_entry . key + index_shift , Span : : Mode : : CodepointOriented } , end_entry . value } ) ;
continue ;
}
anchors_to_relocate . append ( { { start_entry . key , end_entry . key , Span : : Mode : : CodepointOriented } , { start_entry . key , end_entry . key , Span : : Mode : : CodepointOriented } , end_entry . value } ) ;
}
}
m_anchored_spans_ending . clear ( ) ;
m_anchored_spans_starting . clear ( ) ;
// pass over the relocations and update the stale entries
for ( auto & relocation : anchors_to_relocate ) {
stylize ( relocation . new_span , relocation . style ) ;
}
}
2020-03-31 13:34:06 +02:00
}