char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
688e96e
main
1use std:: borrow:: Cow ; 2use std:: cell:: RefCell ; 3 4use nu_ansi_term::{ Color as AnsiColor , Style }; 5use reedline::{ 6Color , PromptEditMode , PromptHistorySearch , PromptHistorySearchStatus , Reedline , StyledText , 7ValidationResult , 8}; 9use tree_sitter_highlight::{ Highlight , HighlightConfiguration , HighlightEvent }; 10 11pub ( super ) fn create () ->Reedline { 12Reedline :: create () 13. with_highlighter ( Box :: new ( Highlighter :: new ())) 14. with_validator ( Box :: new ( Validator )) 15} 16 17pub ( super ) struct Prompt ; 18 19impl reedline:: Prompt for Prompt { 20fn render_prompt_left ( & self ) ->Cow < ' _ , str > { 21Cow :: Borrowed ( "" ) 22} 23 24fn render_prompt_right ( & self ) ->Cow < ' _ , str > { 25Cow :: Borrowed ( "" ) 26} 27 28fn render_prompt_indicator ( & self , _mode : PromptEditMode ) ->Cow < ' _ , str > { 29Cow :: Borrowed ( "> " ) 30} 31 32fn render_prompt_multiline_indicator ( & self ) ->Cow < ' _ , str > { 33Cow :: Borrowed ( "… " ) 34} 35 36fn render_prompt_history_search_indicator ( & self , search : PromptHistorySearch ) ->Cow < ' _ , str > { 37let failing =match search. status { 38PromptHistorySearchStatus :: Passing =>"" , 39PromptHistorySearchStatus :: Failing =>"failing " , 40}; 41Cow :: Owned ( format! ( "({failing}reverse-search: {}) " , search. term )) 42} 43 44fn get_indicator_color ( & self ) ->Color { 45Color :: Green 46} 47 48fn get_prompt_multiline_color ( & self ) ->AnsiColor { 49AnsiColor :: DarkGray 50} 51} 52 53struct Validator ; 54 55impl reedline:: Validator for Validator { 56fn validate ( & self , line : & str ) ->ValidationResult { 57if super :: is_incomplete ( & format! ( "{line}\n" )) { 58ValidationResult :: Incomplete 59} else { 60ValidationResult :: Complete 61} 62} 63} 64 65struct Highlighter { 66config : HighlightConfiguration , 67styles : Vec < Style >, 68highlighter : RefCell < tree_sitter_highlight:: Highlighter >, 69} 70 71impl Highlighter { 72fn new () ->Self { 73// Plain ANSI colors so the terminal's own palette decides how they look. 74let theme =[ 75( "comment" , Style :: new (). fg ( AnsiColor :: DarkGray )), 76( "keyword" , Style :: new (). fg ( AnsiColor :: Purple )), 77( "string" , Style :: new (). fg ( AnsiColor :: Green )), 78( "string.special" , Style :: new (). fg ( AnsiColor :: Red )), 79( "number" , Style :: new (). fg ( AnsiColor :: Yellow )), 80( "constant.builtin" , Style :: new (). fg ( AnsiColor :: Yellow )), 81( "function" , Style :: new (). fg ( AnsiColor :: Blue )), 82( "constructor" , Style :: new (). fg ( AnsiColor :: Cyan )), 83( "variable.builtin" , Style :: new (). fg ( AnsiColor :: Red )), 84( "embedded" , Style :: new ()), 85]; 86let mut config =HighlightConfiguration :: new ( 87 tree_sitter_javascript:: LANGUAGE . into (), 88"javascript" , 89 tree_sitter_javascript:: HIGHLIGHT_QUERY , 90"" , 91 tree_sitter_javascript:: LOCALS_QUERY , 92) 93. expect ( "bundled JavaScript highlight queries are valid" ); 94 config. configure ( & theme. map ( |( name, _) | name)); 95Self { 96 config, 97styles : theme. map ( |( _, style) | style). to_vec (), 98highlighter : RefCell :: new ( tree_sitter_highlight:: Highlighter :: new ()), 99} 100} 101} 102 103impl reedline:: Highlighter for Highlighter { 104fn highlight ( & self , line : & str , _cursor : usize ) ->StyledText { 105let mut text =StyledText :: new (); 106let mut highlighter =self . highlighter . borrow_mut (); 107let events = highlighter 108. highlight ( & self . config , line. as_bytes (), None , None , |_|None ) 109. and_then ( |events| events. collect ::< Result < Vec < _ >, _ >>()); 110let Ok ( events) = eventselse { 111 text. push (( Style :: new (), line. to_owned ())); 112return text; 113}; 114let mut styles =Vec :: new (); 115for eventin events{ 116match event{ 117HighlightEvent :: HighlightStart ( Highlight ( index)) =>{ 118 styles. push ( self . styles [ index]); 119} 120HighlightEvent :: HighlightEnd =>{ 121 styles. pop (); 122} 123HighlightEvent :: Source { start, end} =>{ 124let style = styles. last (). copied (). unwrap_or_default (); 125 text. push (( style, line[ start..end]. to_owned ())); 126} 127} 128} 129 text 130} 131} 132 133# [ cfg ( test )] 134mod tests{ 135use super :: Highlighter ; 136use nu_ansi_term:: Style ; 137use reedline:: Highlighter as _; 138 139fn style_of ( line : & str , fragment : & str ) ->Style { 140let text =Highlighter :: new (). highlight ( line, 0 ); 141 text. buffer 142. iter () 143. find ( |( _, text) | text. contains ( fragment)) 144. unwrap_or_else ( ||panic! ( "{fragment:?} is not in {line:?}" )) 145. 0 146} 147 148# [ test ] 149fn highlighting_preserves_partial_and_unicode_input () { 150let highlighter =Highlighter :: new (); 151for linein [ 152"" , 153"const café = \"☕\" +" , 154"function greet() {\n return `hi ${name" , 155"/* unterminated" , 156"import * as TID from \"jsr:@atcute/tid\"" , 157")}]" , 158] { 159assert_eq! ( highlighter. highlight ( line, 0 ). raw_string (), line); 160} 161} 162 163# [ test ] 164fn literals_and_keywords_are_distinguished_from_identifiers () { 165let line ="const answer = \"forty\" + 2" ; 166let plain =style_of ( line, "answer" ); 167assert_ne! ( style_of ( line, "const" ), plain); 168assert_ne! ( style_of ( line, "\"forty\"" ), plain); 169assert_ne! ( style_of ( line, "2" ), plain); 170} 171 172# [ test ] 173fn template_substitutions_are_not_styled_as_strings () { 174let line ="`total: ${count}`" ; 175assert_ne! ( style_of ( line, "count" ), style_of ( line, "`total: " )); 176} 177}