@ -26,6 +26,7 @@ use helix_view::{
} ;
} ;
use anyhow ::{ anyhow , bail , ensure , Context as _ } ;
use anyhow ::{ anyhow , bail , ensure , Context as _ } ;
use fuzzy_matcher ::FuzzyMatcher ;
use helix_lsp ::{
use helix_lsp ::{
block_on , lsp ,
block_on , lsp ,
util ::{ lsp_pos_to_pos , lsp_range_to_range , pos_to_lsp_pos , range_to_lsp_range } ,
util ::{ lsp_pos_to_pos , lsp_range_to_range , pos_to_lsp_pos , range_to_lsp_range } ,
@ -266,6 +267,7 @@ impl MappableCommand {
change_selection_noyank , "Change selection (delete and enter insert mode, without yanking)" ,
change_selection_noyank , "Change selection (delete and enter insert mode, without yanking)" ,
collapse_selection , "Collapse selection onto a single cursor" ,
collapse_selection , "Collapse selection onto a single cursor" ,
flip_selections , "Flip selection cursor and anchor" ,
flip_selections , "Flip selection cursor and anchor" ,
ensure_selections_forward , "Ensure the selection is in forward direction" ,
insert_mode , "Insert before selection" ,
insert_mode , "Insert before selection" ,
append_mode , "Insert after selection (append)" ,
append_mode , "Insert after selection (append)" ,
command_mode , "Enter command mode" ,
command_mode , "Enter command mode" ,
@ -287,7 +289,7 @@ impl MappableCommand {
add_newline_below , "Add newline below" ,
add_newline_below , "Add newline below" ,
goto_type_definition , "Goto type definition" ,
goto_type_definition , "Goto type definition" ,
goto_implementation , "Goto implementation" ,
goto_implementation , "Goto implementation" ,
goto_file_start , "Goto file start/line ",
goto_file_start , "Goto line number <n> else file start",
goto_file_end , "Goto file end" ,
goto_file_end , "Goto file end" ,
goto_file , "Goto files in selection" ,
goto_file , "Goto files in selection" ,
goto_file_hsplit , "Goto files in selection (hsplit)" ,
goto_file_hsplit , "Goto files in selection (hsplit)" ,
@ -360,6 +362,7 @@ impl MappableCommand {
rotate_selection_contents_forward , "Rotate selection contents forward" ,
rotate_selection_contents_forward , "Rotate selection contents forward" ,
rotate_selection_contents_backward , "Rotate selections contents backward" ,
rotate_selection_contents_backward , "Rotate selections contents backward" ,
expand_selection , "Expand selection to parent syntax node" ,
expand_selection , "Expand selection to parent syntax node" ,
shrink_selection , "Shrink selection to previously expanded syntax node" ,
jump_forward , "Jump forward on jumplist" ,
jump_forward , "Jump forward on jumplist" ,
jump_backward , "Jump backward on jumplist" ,
jump_backward , "Jump backward on jumplist" ,
save_selection , "Save the current selection to the jumplist" ,
save_selection , "Save the current selection to the jumplist" ,
@ -396,7 +399,7 @@ impl MappableCommand {
increment , "Increment" ,
increment , "Increment" ,
decrement , "Decrement" ,
decrement , "Decrement" ,
record_macro , "Record macro" ,
record_macro , "Record macro" ,
play_macro, "P lay macro",
replay_macro, "Rep lay macro",
) ;
) ;
}
}
@ -1280,6 +1283,8 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
. max ( view . offset . row + scrolloff )
. max ( view . offset . row + scrolloff )
. min ( last_line . saturating_sub ( scrolloff ) ) ;
. min ( last_line . saturating_sub ( scrolloff ) ) ;
// If cursor needs moving, replace primary selection
if line ! = cursor . row {
let head = pos_at_coords ( text , Position ::new ( line , cursor . col ) , true ) ; // this func will properly truncate to line end
let head = pos_at_coords ( text , Position ::new ( line , cursor . col ) , true ) ; // this func will properly truncate to line end
let anchor = if doc . mode = = Mode ::Select {
let anchor = if doc . mode = = Mode ::Select {
@ -1288,8 +1293,13 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
head
head
} ;
} ;
// TODO: only manipulate main selection
// replace primary selection with an empty selection at cursor pos
doc . set_selection ( view . id , Selection ::single ( anchor , head ) ) ;
let prim_sel = Range ::new ( anchor , head ) ;
let mut sel = doc . selection ( view . id ) . clone ( ) ;
let idx = sel . primary_index ( ) ;
sel = sel . replace ( idx , prim_sel ) ;
doc . set_selection ( view . id , sel ) ;
}
}
}
fn page_up ( cx : & mut Context ) {
fn page_up ( cx : & mut Context ) {
@ -1543,7 +1553,7 @@ fn searcher(cx: &mut Context, direction: Direction) {
let reg = cx . register . unwrap_or ( '/' ) ;
let reg = cx . register . unwrap_or ( '/' ) ;
let scrolloff = cx . editor . config . scrolloff ;
let scrolloff = cx . editor . config . scrolloff ;
let ( _ , doc ) = current ! ( cx . editor ) ;
let doc = do c! ( cx . editor ) ;
// TODO: could probably share with select_on_matches?
// TODO: could probably share with select_on_matches?
@ -1630,7 +1640,7 @@ fn search_selection(cx: &mut Context) {
let query = doc . selection ( view . id ) . primary ( ) . fragment ( contents ) ;
let query = doc . selection ( view . id ) . primary ( ) . fragment ( contents ) ;
let regex = regex ::escape ( & query ) ;
let regex = regex ::escape ( & query ) ;
cx . editor . registers . get_mut ( '/' ) . push ( regex ) ;
cx . editor . registers . get_mut ( '/' ) . push ( regex ) ;
let msg = format! ( "register '{}' set to '{}'" , ' \\ ', query ) ;
let msg = format! ( "register '{}' set to '{}'" , ' / ', query ) ;
cx . editor . set_status ( msg ) ;
cx . editor . set_status ( msg ) ;
}
}
@ -1904,7 +1914,21 @@ fn flip_selections(cx: &mut Context) {
let selection = doc
let selection = doc
. selection ( view . id )
. selection ( view . id )
. clone ( )
. clone ( )
. transform ( | range | Range ::new ( range . head , range . anchor ) ) ;
. transform ( | range | range . flip ( ) ) ;
doc . set_selection ( view . id , selection ) ;
}
fn ensure_selections_forward ( cx : & mut Context ) {
let ( view , doc ) = current ! ( cx . editor ) ;
let selection = doc
. selection ( view . id )
. clone ( )
. transform ( | r | match r . direction ( ) {
Direction ::Forward = > r ,
Direction ::Backward = > r . flip ( ) ,
} ) ;
doc . set_selection ( view . id , selection ) ;
doc . set_selection ( view . id , selection ) ;
}
}
@ -1938,7 +1962,7 @@ fn append_mode(cx: &mut Context) {
if ! last_range . is_empty ( ) & & last_range . head = = end {
if ! last_range . is_empty ( ) & & last_range . head = = end {
let transaction = Transaction ::change (
let transaction = Transaction ::change (
doc . text ( ) ,
doc . text ( ) ,
std ::array ::IntoIter ::new ( [ ( end , end , Some ( doc . line_ending . as_str ( ) . into ( ) ) ) ] ) ,
[ ( end , end , Some ( doc . line_ending . as_str ( ) . into ( ) ) ) ] . into_iter ( ) ,
) ;
) ;
doc . apply ( & transaction , view . id ) ;
doc . apply ( & transaction , view . id ) ;
}
}
@ -2030,7 +2054,7 @@ pub mod cmd {
fn write_impl ( cx : & mut compositor ::Context , path : Option < & Cow < str > > ) -> anyhow ::Result < ( ) > {
fn write_impl ( cx : & mut compositor ::Context , path : Option < & Cow < str > > ) -> anyhow ::Result < ( ) > {
let jobs = & mut cx . jobs ;
let jobs = & mut cx . jobs ;
let ( _ , doc ) = curren t! ( cx . editor ) ;
let doc = do c_m ut! ( cx . editor ) ;
if let Some ( ref path ) = path {
if let Some ( ref path ) = path {
doc . set_path ( Some ( path . as_ref ( ) . as_ref ( ) ) )
doc . set_path ( Some ( path . as_ref ( ) . as_ref ( ) ) )
@ -2083,8 +2107,7 @@ pub mod cmd {
_args : & [ Cow < str > ] ,
_args : & [ Cow < str > ] ,
_event : PromptEvent ,
_event : PromptEvent ,
) -> anyhow ::Result < ( ) > {
) -> anyhow ::Result < ( ) > {
let ( _ , doc ) = current ! ( cx . editor ) ;
let doc = doc ! ( cx . editor ) ;
if let Some ( format ) = doc . format ( ) {
if let Some ( format ) = doc . format ( ) {
let callback =
let callback =
make_format_callback ( doc . id ( ) , doc . version ( ) , Modified ::LeaveModified , format ) ;
make_format_callback ( doc . id ( ) , doc . version ( ) , Modified ::LeaveModified , format ) ;
@ -2307,12 +2330,7 @@ pub mod cmd {
write_all_impl ( cx , args , event , true , true )
write_all_impl ( cx , args , event , true , true )
}
}
fn quit_all_impl (
fn quit_all_impl ( editor : & mut Editor , force : bool ) -> anyhow ::Result < ( ) > {
editor : & mut Editor ,
_args : & [ Cow < str > ] ,
_event : PromptEvent ,
force : bool ,
) -> anyhow ::Result < ( ) > {
if ! force {
if ! force {
buffers_remaining_impl ( editor ) ? ;
buffers_remaining_impl ( editor ) ? ;
}
}
@ -2328,18 +2346,18 @@ pub mod cmd {
fn quit_all (
fn quit_all (
cx : & mut compositor ::Context ,
cx : & mut compositor ::Context ,
args: & [ Cow < str > ] ,
_ args: & [ Cow < str > ] ,
event: PromptEvent ,
_ event: PromptEvent ,
) -> anyhow ::Result < ( ) > {
) -> anyhow ::Result < ( ) > {
quit_all_impl ( cx . editor , args , event , false )
quit_all_impl ( cx . editor , false )
}
}
fn force_quit_all (
fn force_quit_all (
cx : & mut compositor ::Context ,
cx : & mut compositor ::Context ,
args: & [ Cow < str > ] ,
_ args: & [ Cow < str > ] ,
event: PromptEvent ,
_ event: PromptEvent ,
) -> anyhow ::Result < ( ) > {
) -> anyhow ::Result < ( ) > {
quit_all_impl ( cx . editor , args , event , true )
quit_all_impl ( cx . editor , true )
}
}
fn cquit (
fn cquit (
@ -2353,12 +2371,21 @@ pub mod cmd {
. unwrap_or ( 1 ) ;
. unwrap_or ( 1 ) ;
cx . editor . exit_code = exit_code ;
cx . editor . exit_code = exit_code ;
let views : Vec < _ > = cx . editor . tree . views ( ) . map ( | ( view , _ ) | view . id ) . collect ( ) ;
quit_all_impl ( cx . editor , false )
for view_id in views {
cx . editor . close ( view_id ) ;
}
}
Ok ( ( ) )
fn force_cquit (
cx : & mut compositor ::Context ,
args : & [ Cow < str > ] ,
_event : PromptEvent ,
) -> anyhow ::Result < ( ) > {
let exit_code = args
. first ( )
. and_then ( | code | code . parse ::< i32 > ( ) . ok ( ) )
. unwrap_or ( 1 ) ;
cx . editor . exit_code = exit_code ;
quit_all_impl ( cx . editor , true )
}
}
fn theme (
fn theme (
@ -2393,7 +2420,7 @@ pub mod cmd {
args : & [ Cow < str > ] ,
args : & [ Cow < str > ] ,
_event : PromptEvent ,
_event : PromptEvent ,
) -> anyhow ::Result < ( ) > {
) -> anyhow ::Result < ( ) > {
let ( _ , doc ) = current ! ( cx . editor ) ;
let doc = do c! ( cx . editor ) ;
let default_sep = Cow ::Borrowed ( doc . line_ending . as_str ( ) ) ;
let default_sep = Cow ::Borrowed ( doc . line_ending . as_str ( ) ) ;
let separator = args . first ( ) . unwrap_or ( & default_sep ) ;
let separator = args . first ( ) . unwrap_or ( & default_sep ) ;
yank_joined_to_clipboard_impl ( cx . editor , separator , ClipboardType ::Clipboard )
yank_joined_to_clipboard_impl ( cx . editor , separator , ClipboardType ::Clipboard )
@ -2412,7 +2439,7 @@ pub mod cmd {
args : & [ Cow < str > ] ,
args : & [ Cow < str > ] ,
_event : PromptEvent ,
_event : PromptEvent ,
) -> anyhow ::Result < ( ) > {
) -> anyhow ::Result < ( ) > {
let ( _ , doc ) = current ! ( cx . editor ) ;
let doc = do c! ( cx . editor ) ;
let default_sep = Cow ::Borrowed ( doc . line_ending . as_str ( ) ) ;
let default_sep = Cow ::Borrowed ( doc . line_ending . as_str ( ) ) ;
let separator = args . first ( ) . unwrap_or ( & default_sep ) ;
let separator = args . first ( ) . unwrap_or ( & default_sep ) ;
yank_joined_to_clipboard_impl ( cx . editor , separator , ClipboardType ::Selection )
yank_joined_to_clipboard_impl ( cx . editor , separator , ClipboardType ::Selection )
@ -2539,7 +2566,7 @@ pub mod cmd {
args : & [ Cow < str > ] ,
args : & [ Cow < str > ] ,
_event : PromptEvent ,
_event : PromptEvent ,
) -> anyhow ::Result < ( ) > {
) -> anyhow ::Result < ( ) > {
let ( _ , doc ) = curren t! ( cx . editor ) ;
let doc = do c_m ut! ( cx . editor ) ;
if let Some ( label ) = args . first ( ) {
if let Some ( label ) = args . first ( ) {
doc . set_encoding ( label )
doc . set_encoding ( label )
} else {
} else {
@ -2637,6 +2664,86 @@ pub mod cmd {
let ( view , doc ) = current ! ( cx . editor ) ;
let ( view , doc ) = current ! ( cx . editor ) ;
view . ensure_cursor_in_view ( doc , line ) ;
view . ensure_cursor_in_view ( doc , line ) ;
Ok ( ( ) )
}
fn setting (
cx : & mut compositor ::Context ,
args : & [ Cow < str > ] ,
_event : PromptEvent ,
) -> anyhow ::Result < ( ) > {
let runtime_config = & mut cx . editor . config ;
if args . len ( ) ! = 2 {
anyhow ::bail ! ( "Bad arguments. Usage: `:set key field`" ) ;
}
let ( key , arg ) = ( & args [ 0 ] . to_lowercase ( ) , & args [ 1 ] ) ;
match key . as_ref ( ) {
"scrolloff" = > runtime_config . scrolloff = arg . parse ( ) ? ,
"scroll-lines" = > runtime_config . scroll_lines = arg . parse ( ) ? ,
"mouse" = > runtime_config . mouse = arg . parse ( ) ? ,
"line-number" = > runtime_config . line_number = arg . parse ( ) ? ,
"middle-click_paste" = > runtime_config . middle_click_paste = arg . parse ( ) ? ,
"smart-case" = > runtime_config . smart_case = arg . parse ( ) ? ,
"auto-pairs" = > runtime_config . auto_pairs = arg . parse ( ) ? ,
"auto-completion" = > runtime_config . auto_completion = arg . parse ( ) ? ,
"completion-trigger-len" = > runtime_config . completion_trigger_len = arg . parse ( ) ? ,
"auto-info" = > runtime_config . auto_info = arg . parse ( ) ? ,
"true-color" = > runtime_config . true_color = arg . parse ( ) ? ,
_ = > anyhow ::bail ! ( "Unknown key `{}`." , args [ 0 ] ) ,
}
Ok ( ( ) )
}
fn sort (
cx : & mut compositor ::Context ,
args : & [ Cow < str > ] ,
_event : PromptEvent ,
) -> anyhow ::Result < ( ) > {
sort_impl ( cx , args , false )
}
fn sort_reverse (
cx : & mut compositor ::Context ,
args : & [ Cow < str > ] ,
_event : PromptEvent ,
) -> anyhow ::Result < ( ) > {
sort_impl ( cx , args , true )
}
fn sort_impl (
cx : & mut compositor ::Context ,
_args : & [ Cow < str > ] ,
reverse : bool ,
) -> anyhow ::Result < ( ) > {
let ( view , doc ) = current ! ( cx . editor ) ;
let text = doc . text ( ) . slice ( .. ) ;
let selection = doc . selection ( view . id ) ;
let mut fragments : Vec < _ > = selection
. fragments ( text )
. map ( | fragment | Tendril ::from_slice ( & fragment ) )
. collect ( ) ;
fragments . sort_by ( match reverse {
true = > | a : & Tendril , b : & Tendril | b . cmp ( a ) ,
false = > | a : & Tendril , b : & Tendril | a . cmp ( b ) ,
} ) ;
let transaction = Transaction ::change (
doc . text ( ) ,
selection
. into_iter ( )
. zip ( fragments )
. map ( | ( s , fragment ) | ( s . from ( ) , s . to ( ) , Some ( fragment ) ) ) ,
) ;
doc . apply ( & transaction , view . id ) ;
doc . append_changes_to_history ( view . id ) ;
Ok ( ( ) )
Ok ( ( ) )
}
}
@ -2782,6 +2889,13 @@ pub mod cmd {
fun : cquit ,
fun : cquit ,
completer : None ,
completer : None ,
} ,
} ,
TypableCommand {
name : "cquit!" ,
aliases : & [ "cq!" ] ,
doc : "Quit with exit code (default 1) forcefully (ignoring unsaved changes). Accepts an optional integer exit code (:cq! 2)." ,
fun : force_cquit ,
completer : None ,
} ,
TypableCommand {
TypableCommand {
name : "theme" ,
name : "theme" ,
aliases : & [ ] ,
aliases : & [ ] ,
@ -2928,7 +3042,28 @@ pub mod cmd {
doc : "Go to line number." ,
doc : "Go to line number." ,
fun : goto_line_number ,
fun : goto_line_number ,
completer : None ,
completer : None ,
}
} ,
TypableCommand {
name : "set-option" ,
aliases : & [ "set" ] ,
doc : "Set a config option at runtime" ,
fun : setting ,
completer : Some ( completers ::setting ) ,
} ,
TypableCommand {
name : "sort" ,
aliases : & [ ] ,
doc : "Sort ranges in selection." ,
fun : sort ,
completer : None ,
} ,
TypableCommand {
name : "rsort" ,
aliases : & [ ] ,
doc : "Sort ranges in selection in reverse order." ,
fun : sort_reverse ,
completer : None ,
} ,
] ;
] ;
pub static TYPABLE_COMMAND_MAP : Lazy < HashMap < & ' static str , & ' static TypableCommand > > =
pub static TYPABLE_COMMAND_MAP : Lazy < HashMap < & ' static str , & ' static TypableCommand > > =
@ -2948,17 +3083,28 @@ fn command_mode(cx: &mut Context) {
":" . into ( ) ,
":" . into ( ) ,
Some ( ':' ) ,
Some ( ':' ) ,
| input : & str | {
| input : & str | {
static FUZZY_MATCHER : Lazy < fuzzy_matcher ::skim ::SkimMatcherV2 > =
Lazy ::new ( fuzzy_matcher ::skim ::SkimMatcherV2 ::default ) ;
// we use .this over split_whitespace() because we care about empty segments
// we use .this over split_whitespace() because we care about empty segments
let parts = input . split ( ' ' ) . collect ::< Vec < & str > > ( ) ;
let parts = input . split ( ' ' ) . collect ::< Vec < & str > > ( ) ;
// simple heuristic: if there's no just one part, complete command name.
// simple heuristic: if there's no just one part, complete command name.
// if there's a space, per command completion kicks in.
// if there's a space, per command completion kicks in.
if parts . len ( ) < = 1 {
if parts . len ( ) < = 1 {
let end = 0 .. ;
let mut matches : Vec < _ > = cmd ::TYPABLE_COMMAND_LIST
cmd ::TYPABLE_COMMAND_LIST
. iter ( )
. iter ( )
. filter ( | command | command . name . contains ( input ) )
. filter_map ( | command | {
. map ( | command | ( end . clone ( ) , Cow ::Borrowed ( command . name ) ) )
FUZZY_MATCHER
. fuzzy_match ( command . name , input )
. map ( | score | ( command . name , score ) )
} )
. collect ( ) ;
matches . sort_unstable_by_key ( | ( _file , score ) | std ::cmp ::Reverse ( * score ) ) ;
matches
. into_iter ( )
. map ( | ( name , _ ) | ( 0 .. , name . into ( ) ) )
. collect ( )
. collect ( )
} else {
} else {
let part = parts . last ( ) . unwrap ( ) ;
let part = parts . last ( ) . unwrap ( ) ;
@ -3002,7 +3148,16 @@ fn command_mode(cx: &mut Context) {
// Handle typable commands
// Handle typable commands
if let Some ( cmd ) = cmd ::TYPABLE_COMMAND_MAP . get ( parts [ 0 ] ) {
if let Some ( cmd ) = cmd ::TYPABLE_COMMAND_MAP . get ( parts [ 0 ] ) {
let args = shellwords ::shellwords ( input ) ;
let args = if cfg! ( unix ) {
shellwords ::shellwords ( input )
} else {
// Windows doesn't support POSIX, so fallback for now
parts
. into_iter ( )
. map ( | part | part . into ( ) )
. collect ::< Vec < _ > > ( )
} ;
if let Err ( e ) = ( cmd . fun ) ( cx , & args [ 1 .. ] , event ) {
if let Err ( e ) = ( cmd . fun ) ( cx , & args [ 1 .. ] , event ) {
cx . editor . set_error ( format! ( "{}" , e ) ) ;
cx . editor . set_error ( format! ( "{}" , e ) ) ;
}
}
@ -3026,7 +3181,8 @@ fn command_mode(cx: &mut Context) {
}
}
fn file_picker ( cx : & mut Context ) {
fn file_picker ( cx : & mut Context ) {
let root = find_root ( None ) . unwrap_or_else ( | | PathBuf ::from ( "./" ) ) ;
// We don't specify language markers, root will be the root of the current git repo
let root = find_root ( None , & [ ] ) . unwrap_or_else ( | | PathBuf ::from ( "./" ) ) ;
let picker = ui ::file_picker ( root , & cx . editor . config ) ;
let picker = ui ::file_picker ( root , & cx . editor . config ) ;
cx . push_layer ( Box ::new ( picker ) ) ;
cx . push_layer ( Box ::new ( picker ) ) ;
}
}
@ -3118,7 +3274,7 @@ fn symbol_picker(cx: &mut Context) {
nested_to_flat ( list , file , child ) ;
nested_to_flat ( list , file , child ) ;
}
}
}
}
let ( _ , doc ) = current ! ( cx . editor ) ;
let doc = do c! ( cx . editor ) ;
let language_server = match doc . language_server ( ) {
let language_server = match doc . language_server ( ) {
Some ( language_server ) = > language_server ,
Some ( language_server ) = > language_server ,
@ -3139,7 +3295,7 @@ fn symbol_picker(cx: &mut Context) {
let symbols = match symbols {
let symbols = match symbols {
lsp ::DocumentSymbolResponse ::Flat ( symbols ) = > symbols ,
lsp ::DocumentSymbolResponse ::Flat ( symbols ) = > symbols ,
lsp ::DocumentSymbolResponse ::Nested ( symbols ) = > {
lsp ::DocumentSymbolResponse ::Nested ( symbols ) = > {
let ( _view , doc ) = current ! ( editor ) ;
let doc = do c! ( editor ) ;
let mut flat_symbols = Vec ::new ( ) ;
let mut flat_symbols = Vec ::new ( ) ;
for symbol in symbols {
for symbol in symbols {
nested_to_flat ( & mut flat_symbols , & doc . identifier ( ) , symbol )
nested_to_flat ( & mut flat_symbols , & doc . identifier ( ) , symbol )
@ -3181,17 +3337,15 @@ fn symbol_picker(cx: &mut Context) {
}
}
fn workspace_symbol_picker ( cx : & mut Context ) {
fn workspace_symbol_picker ( cx : & mut Context ) {
let ( _ , doc ) = current ! ( cx . editor ) ;
let doc = do c! ( cx . editor ) ;
let current_path = doc . path ( ) . cloned ( ) ;
let language_server = match doc . language_server ( ) {
let language_server = match doc . language_server ( ) {
Some ( language_server ) = > language_server ,
Some ( language_server ) = > language_server ,
None = > return ,
None = > return ,
} ;
} ;
let offset_encoding = language_server . offset_encoding ( ) ;
let offset_encoding = language_server . offset_encoding ( ) ;
let future = language_server . workspace_symbols ( "" . to_string ( ) ) ;
let future = language_server . workspace_symbols ( "" . to_string ( ) ) ;
let current_path = doc_mut ! ( cx . editor ) . path ( ) . cloned ( ) ;
cx . callback (
cx . callback (
future ,
future ,
move | _editor : & mut Editor ,
move | _editor : & mut Editor ,
@ -3277,12 +3431,19 @@ pub fn code_action(cx: &mut Context) {
move | editor , code_action , _action | match code_action {
move | editor , code_action , _action | match code_action {
lsp ::CodeActionOrCommand ::Command ( command ) = > {
lsp ::CodeActionOrCommand ::Command ( command ) = > {
log ::debug ! ( "code action command: {:?}" , command ) ;
log ::debug ! ( "code action command: {:?}" , command ) ;
e ditor. set_error ( String ::from ( "Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183" ) ) ;
e xecute_lsp_command( editor , command . clone ( ) ) ;
}
}
lsp ::CodeActionOrCommand ::CodeAction ( code_action ) = > {
lsp ::CodeActionOrCommand ::CodeAction ( code_action ) = > {
log ::debug ! ( "code action: {:?}" , code_action ) ;
log ::debug ! ( "code action: {:?}" , code_action ) ;
if let Some ( ref workspace_edit ) = code_action . edit {
if let Some ( ref workspace_edit ) = code_action . edit {
apply_workspace_edit ( editor , offset_encoding , workspace_edit )
log ::debug ! ( "edit: {:?}" , workspace_edit ) ;
apply_workspace_edit ( editor , offset_encoding , workspace_edit ) ;
}
// if code action provides both edit and command first the edit
// should be applied and then the command
if let Some ( command ) = & code_action . command {
execute_lsp_command ( editor , command . clone ( ) ) ;
}
}
}
}
} ,
} ,
@ -3293,6 +3454,25 @@ pub fn code_action(cx: &mut Context) {
)
)
}
}
pub fn execute_lsp_command ( editor : & mut Editor , cmd : lsp ::Command ) {
let doc = doc ! ( editor ) ;
let language_server = match doc . language_server ( ) {
Some ( language_server ) = > language_server ,
None = > return ,
} ;
// the command is executed on the server and communicated back
// to the client asynchronously using workspace edits
let command_future = language_server . command ( cmd ) ;
tokio ::spawn ( async move {
let res = command_future . await ;
if let Err ( e ) = res {
log ::error ! ( "execute LSP command: {}" , e ) ;
}
} ) ;
}
pub fn apply_document_resource_op ( op : & lsp ::ResourceOp ) -> std ::io ::Result < ( ) > {
pub fn apply_document_resource_op ( op : & lsp ::ResourceOp ) -> std ::io ::Result < ( ) > {
use lsp ::ResourceOp ;
use lsp ::ResourceOp ;
use std ::fs ;
use std ::fs ;
@ -3346,7 +3526,7 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
}
}
}
}
fn apply_workspace_edit (
pub fn apply_workspace_edit (
editor : & mut Editor ,
editor : & mut Editor ,
offset_encoding : OffsetEncoding ,
offset_encoding : OffsetEncoding ,
workspace_edit : & lsp ::WorkspaceEdit ,
workspace_edit : & lsp ::WorkspaceEdit ,
@ -3537,22 +3717,22 @@ fn open(cx: &mut Context, open: Open) {
let mut offs = 0 ;
let mut offs = 0 ;
let mut transaction = Transaction ::change_by_selection ( contents , selection , | range | {
let mut transaction = Transaction ::change_by_selection ( contents , selection , | range | {
let line = range . cursor_line ( text ) ;
let cursor_ line = range . cursor_line ( text ) ;
let line = match open {
let new_ line = match open {
// adjust position to the end of the line (next line - 1)
// adjust position to the end of the line (next line - 1)
Open ::Below = > line + 1 ,
Open ::Below = > cursor_ line + 1 ,
// adjust position to the end of the previous line (current line - 1)
// adjust position to the end of the previous line (current line - 1)
Open ::Above = > line,
Open ::Above = > cursor_ line,
} ;
} ;
// Index to insert newlines after, as well as the char width
// Index to insert newlines after, as well as the char width
// to use to compensate for those inserted newlines.
// to use to compensate for those inserted newlines.
let ( line_end_index , line_end_offset_width ) = if line = = 0 {
let ( line_end_index , line_end_offset_width ) = if new_ line = = 0 {
( 0 , 0 )
( 0 , 0 )
} else {
} else {
(
(
line_end_char_index ( & doc . text ( ) . slice ( .. ) , line. saturating_sub ( 1 ) ) ,
line_end_char_index ( & doc . text ( ) . slice ( .. ) , new_ line. saturating_sub ( 1 ) ) ,
doc . line_ending . len_chars ( ) ,
doc . line_ending . len_chars ( ) ,
)
)
} ;
} ;
@ -3563,8 +3743,10 @@ fn open(cx: &mut Context, open: Open) {
doc . syntax ( ) ,
doc . syntax ( ) ,
text ,
text ,
line_end_index ,
line_end_index ,
new_line . saturating_sub ( 1 ) ,
true ,
true ,
) ;
)
. unwrap_or_else ( | | indent ::indent_level_for_line ( text . line ( cursor_line ) , doc . tab_width ( ) ) ) ;
let indent = doc . indent_unit ( ) . repeat ( indent_level ) ;
let indent = doc . indent_unit ( ) . repeat ( indent_level ) ;
let indent_len = indent . len ( ) ;
let indent_len = indent . len ( ) ;
let mut text = String ::with_capacity ( 1 + indent_len ) ;
let mut text = String ::with_capacity ( 1 + indent_len ) ;
@ -3610,6 +3792,7 @@ fn normal_mode(cx: &mut Context) {
doc . mode = Mode ::Normal ;
doc . mode = Mode ::Normal ;
try_restore_indent ( doc , view . id ) ;
doc . append_changes_to_history ( view . id ) ;
doc . append_changes_to_history ( view . id ) ;
// if leaving append mode, move cursor back by 1
// if leaving append mode, move cursor back by 1
@ -3627,6 +3810,40 @@ fn normal_mode(cx: &mut Context) {
}
}
}
}
fn try_restore_indent ( doc : & mut Document , view_id : ViewId ) {
use helix_core ::chars ::char_is_whitespace ;
use helix_core ::Operation ;
fn inserted_a_new_blank_line ( changes : & [ Operation ] , pos : usize , line_end_pos : usize ) -> bool {
if let [ Operation ::Retain ( move_pos ) , Operation ::Insert ( ref inserted_str ) , Operation ::Retain ( _ ) ] =
changes
{
move_pos + inserted_str . len32 ( ) as usize = = pos
& & inserted_str . starts_with ( '\n' )
& & inserted_str . chars ( ) . skip ( 1 ) . all ( char_is_whitespace )
& & pos = = line_end_pos // ensure no characters exists after current position
} else {
false
}
}
let doc_changes = doc . changes ( ) . changes ( ) ;
let text = doc . text ( ) . slice ( .. ) ;
let range = doc . selection ( view_id ) . primary ( ) ;
let pos = range . cursor ( text ) ;
let line_end_pos = line_end_char_index ( & text , range . cursor_line ( text ) ) ;
if inserted_a_new_blank_line ( doc_changes , pos , line_end_pos ) {
// Removes tailing whitespaces.
let transaction =
Transaction ::change_by_selection ( doc . text ( ) , doc . selection ( view_id ) , | range | {
let line_start_pos = text . line_to_char ( range . cursor_line ( text ) ) ;
( line_start_pos , pos , None )
} ) ;
doc . apply ( & transaction , view_id ) ;
}
}
// Store a jump on the jumplist.
// Store a jump on the jumplist.
fn push_jump ( editor : & mut Editor ) {
fn push_jump ( editor : & mut Editor ) {
let ( view , doc ) = current ! ( editor ) ;
let ( view , doc ) = current ! ( editor ) ;
@ -3994,27 +4211,21 @@ fn goto_pos(editor: &mut Editor, pos: usize) {
}
}
fn goto_first_diag ( cx : & mut Context ) {
fn goto_first_diag ( cx : & mut Context ) {
let editor = & mut cx . editor ;
let doc = doc ! ( cx . editor ) ;
let ( _ , doc ) = current ! ( editor ) ;
let pos = match doc . diagnostics ( ) . first ( ) {
let pos = match doc . diagnostics ( ) . first ( ) {
Some ( diag ) = > diag . range . start ,
Some ( diag ) = > diag . range . start ,
None = > return ,
None = > return ,
} ;
} ;
goto_pos ( cx . editor , pos ) ;
goto_pos ( editor , pos ) ;
}
}
fn goto_last_diag ( cx : & mut Context ) {
fn goto_last_diag ( cx : & mut Context ) {
let editor = & mut cx . editor ;
let doc = doc ! ( cx . editor ) ;
let ( _ , doc ) = current ! ( editor ) ;
let pos = match doc . diagnostics ( ) . last ( ) {
let pos = match doc . diagnostics ( ) . last ( ) {
Some ( diag ) = > diag . range . start ,
Some ( diag ) = > diag . range . start ,
None = > return ,
None = > return ,
} ;
} ;
goto_pos ( cx . editor , pos ) ;
goto_pos ( editor , pos ) ;
}
}
fn goto_next_diag ( cx : & mut Context ) {
fn goto_next_diag ( cx : & mut Context ) {
@ -4270,48 +4481,48 @@ pub mod insert {
} ;
} ;
let curr = contents . get_char ( pos ) . unwrap_or ( ' ' ) ;
let curr = contents . get_char ( pos ) . unwrap_or ( ' ' ) ;
// TODO: offset range.head by 1? when calculating?
let current_line = text . char_to_line ( pos ) ;
let indent_level = indent ::suggested_indent_for_pos (
let indent_level = indent ::suggested_indent_for_pos (
doc . language_config ( ) ,
doc . language_config ( ) ,
doc . syntax ( ) ,
doc . syntax ( ) ,
text ,
text ,
pos . saturating_sub ( 1 ) ,
pos ,
current_line ,
true ,
true ,
) ;
)
. unwrap_or_else ( | | {
indent ::indent_level_for_line ( text . line ( current_line ) , doc . tab_width ( ) )
} ) ;
let indent = doc . indent_unit ( ) . repeat ( indent_level ) ;
let indent = doc . indent_unit ( ) . repeat ( indent_level ) ;
let mut text = String ::with_capacity ( 1 + indent . len ( ) ) ;
let mut text = String ::new ( ) ;
// If we are between pairs (such as brackets), we want to insert an additional line which is indented one level more and place the cursor there
let new_head_pos = if helix_core ::auto_pairs ::PAIRS . contains ( & ( prev , curr ) ) {
let inner_indent = doc . indent_unit ( ) . repeat ( indent_level + 1 ) ;
text . reserve_exact ( 2 + indent . len ( ) + inner_indent . len ( ) ) ;
text . push_str ( doc . line_ending . as_str ( ) ) ;
text . push_str ( & inner_indent ) ;
let new_head_pos = pos + offs + text . chars ( ) . count ( ) ;
text . push_str ( doc . line_ending . as_str ( ) ) ;
text . push_str ( doc . line_ending . as_str ( ) ) ;
text . push_str ( & indent ) ;
text . push_str ( & indent ) ;
new_head_pos
let head = pos + offs + text . chars ( ) . count ( ) ;
// TODO: range replace or extend
// range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
// can be used with cx.mode to do replace or extend on most changes
ranges . push ( Range ::new (
if range . is_empty ( ) {
head
} else {
} else {
range . anchor + offs
text . reserve_exact ( 1 + indent . len ( ) ) ;
} ,
head ,
) ) ;
// if between a bracket pair
if helix_core ::auto_pairs ::PAIRS . contains ( & ( prev , curr ) ) {
// another newline, indent the end bracket one level less
let indent = doc . indent_unit ( ) . repeat ( indent_level . saturating_sub ( 1 ) ) ;
text . push_str ( doc . line_ending . as_str ( ) ) ;
text . push_str ( doc . line_ending . as_str ( ) ) ;
text . push_str ( & indent ) ;
text . push_str ( & indent ) ;
}
pos + offs + text . chars ( ) . count ( )
} ;
// TODO: range replace or extend
// range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
// can be used with cx.mode to do replace or extend on most changes
ranges . push ( Range ::new ( new_head_pos , new_head_pos ) ) ;
offs + = text . chars ( ) . count ( ) ;
offs + = text . chars ( ) . count ( ) ;
( pos , pos , Some ( text . into ( ) ) )
( pos , pos , Some ( text . into ( ) ) )
} ) ;
} ) ;
transaction = transaction . with_selection ( Selection ::new ( ranges , selection . primary_index ( ) ) ) ;
transaction = transaction . with_selection ( Selection ::new ( ranges , selection . primary_index ( ) ) ) ;
//
doc . apply ( & transaction , view . id ) ;
doc . apply ( & transaction , view . id ) ;
}
}
@ -5079,7 +5290,7 @@ pub fn completion(cx: &mut Context) {
move | editor : & mut Editor ,
move | editor : & mut Editor ,
compositor : & mut Compositor ,
compositor : & mut Compositor ,
response : Option < lsp ::CompletionResponse > | {
response : Option < lsp ::CompletionResponse > | {
let ( _ , doc ) = current ! ( editor ) ;
let doc = do c! ( editor ) ;
if doc . mode ( ) ! = Mode ::Insert {
if doc . mode ( ) ! = Mode ::Insert {
// we're not in insert mode anymore
// we're not in insert mode anymore
return ;
return ;
@ -5257,6 +5468,7 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) {
doc . apply ( & transaction , view . id ) ;
doc . apply ( & transaction , view . id ) ;
doc . append_changes_to_history ( view . id ) ;
doc . append_changes_to_history ( view . id ) ;
}
}
fn rotate_selection_contents_forward ( cx : & mut Context ) {
fn rotate_selection_contents_forward ( cx : & mut Context ) {
rotate_selection_contents ( cx , Direction ::Forward )
rotate_selection_contents ( cx , Direction ::Forward )
}
}
@ -5272,7 +5484,39 @@ fn expand_selection(cx: &mut Context) {
if let Some ( syntax ) = doc . syntax ( ) {
if let Some ( syntax ) = doc . syntax ( ) {
let text = doc . text ( ) . slice ( .. ) ;
let text = doc . text ( ) . slice ( .. ) ;
let selection = object ::expand_selection ( syntax , text , doc . selection ( view . id ) ) ;
let current_selection = doc . selection ( view . id ) ;
// save current selection so it can be restored using shrink_selection
view . object_selections . push ( current_selection . clone ( ) ) ;
let selection = object ::expand_selection ( syntax , text , current_selection ) ;
doc . set_selection ( view . id , selection ) ;
}
} ;
motion ( cx . editor ) ;
cx . editor . last_motion = Some ( Motion ( Box ::new ( motion ) ) ) ;
}
fn shrink_selection ( cx : & mut Context ) {
let motion = | editor : & mut Editor | {
let ( view , doc ) = current ! ( editor ) ;
let current_selection = doc . selection ( view . id ) ;
// try to restore previous selection
if let Some ( prev_selection ) = view . object_selections . pop ( ) {
if current_selection . contains ( & prev_selection ) {
// allow shrinking the selection only if current selection contains the previous object selection
doc . set_selection ( view . id , prev_selection ) ;
return ;
} else {
// clear existing selection as they can't be shrinked to anyway
view . object_selections . clear ( ) ;
}
}
// if not previous selection, shrink to first child
if let Some ( syntax ) = doc . syntax ( ) {
let text = doc . text ( ) . slice ( .. ) ;
let selection = object ::shrink_selection ( syntax , text , current_selection ) ;
doc . set_selection ( view . id , selection ) ;
doc . set_selection ( view . id , selection ) ;
}
}
} ;
} ;
@ -5920,42 +6164,42 @@ fn record_macro(cx: &mut Context) {
keys . pop ( ) ;
keys . pop ( ) ;
let s = keys
let s = keys
. into_iter ( )
. into_iter ( )
. map ( | key | format! ( "{}" , key ) )
. map ( | key | {
. collect ::< Vec < _ > > ( )
let s = key . to_string ( ) ;
. join ( " " ) ;
if s . chars ( ) . count ( ) = = 1 {
s
} else {
format! ( "<{}>" , s )
}
} )
. collect ::< String > ( ) ;
cx . editor . registers . get_mut ( reg ) . write ( vec! [ s ] ) ;
cx . editor . registers . get_mut ( reg ) . write ( vec! [ s ] ) ;
cx . editor
cx . editor
. set_status ( format! ( "Recorded to register {}" , reg ) ) ;
. set_status ( format! ( "Recorded to register [ {}] ", reg ) ) ;
} else {
} else {
let reg = cx . register . take ( ) . unwrap_or ( '@' ) ;
let reg = cx . register . take ( ) . unwrap_or ( '@' ) ;
cx . editor . macro_recording = Some ( ( reg , Vec ::new ( ) ) ) ;
cx . editor . macro_recording = Some ( ( reg , Vec ::new ( ) ) ) ;
cx . editor
cx . editor
. set_status ( format! ( "Recording to register {}", reg ) ) ;
. set_status ( format! ( "Recording to register [ {}] ", reg ) ) ;
}
}
}
}
fn play_macro( cx : & mut Context ) {
fn re play_macro( cx : & mut Context ) {
let reg = cx . register . unwrap_or ( '@' ) ;
let reg = cx . register . unwrap_or ( '@' ) ;
let keys = match cx
let keys : Vec < KeyEvent > = if let Some ( [ keys_str ] ) = cx . editor . registers . read ( reg ) {
. editor
match helix_view ::input ::parse_macro ( keys_str ) {
. registers
. get ( reg )
. and_then ( | reg | reg . read ( ) . get ( 0 ) )
. context ( "Register empty" )
. and_then ( | s | {
s . split_whitespace ( )
. map ( str ::parse ::< KeyEvent > )
. collect ::< Result < Vec < _ > , _ > > ( )
. context ( "Failed to parse macro" )
} ) {
Ok ( keys ) = > keys ,
Ok ( keys ) = > keys ,
Err ( e ) = > {
Err ( err ) = > {
cx . editor . set_error ( format! ( " {}", e ) ) ;
cx . editor . set_error ( format! ( "Invalid macro: {}" , err ) ) ;
return ;
return ;
}
}
}
} else {
cx . editor . set_error ( format! ( "Register [{}] empty" , reg ) ) ;
return ;
} ;
} ;
let count = cx . count ( ) ;
let count = cx . count ( ) ;
cx . callback = Some ( Box ::new (
cx . callback = Some ( Box ::new (
move | compositor : & mut Compositor , cx : & mut compositor ::Context | {
move | compositor : & mut Compositor , cx : & mut compositor ::Context | {
for _ in 0 .. count {
for _ in 0 .. count {