2024-04-09 22:28:54 +08:00
use futures_util ::{ stream ::FuturesOrdered , FutureExt } ;
2022-02-18 12:58:18 +08:00
use helix_lsp ::{
2022-06-30 17:16:18 +08:00
block_on ,
2023-02-11 14:50:01 +08:00
lsp ::{
self , CodeAction , CodeActionOrCommand , CodeActionTriggerKind , DiagnosticSeverity ,
NumberOrString ,
} ,
2023-03-08 09:51:29 +08:00
util ::{ diagnostic_to_lsp_diagnostic , lsp_range_to_range , range_to_lsp_range } ,
2024-04-08 08:46:32 +08:00
Client , LanguageServerId , OffsetEncoding ,
2022-02-18 12:58:18 +08:00
} ;
2022-05-24 00:10:48 +08:00
use tokio_stream ::StreamExt ;
2022-12-25 13:54:09 +08:00
use tui ::{
text ::{ Span , Spans } ,
widgets ::Row ,
} ;
2022-02-18 12:58:18 +08:00
2023-12-01 07:03:27 +08:00
use super ::{ align_view , push_jump , Align , Context , Editor } ;
2022-02-18 12:58:18 +08:00
2024-01-17 02:59:48 +08:00
use helix_core ::{ syntax ::LanguageServerFeature , text_annotations ::InlineAnnotation , Selection } ;
use helix_stdx ::path ;
2023-03-11 10:32:14 +08:00
use helix_view ::{
2023-12-01 07:03:27 +08:00
document ::{ DocumentInlayHints , DocumentInlayHintsId } ,
2023-03-11 10:32:14 +08:00
editor ::Action ,
2023-12-01 07:03:27 +08:00
handlers ::lsp ::SignatureHelpInvoked ,
2023-03-11 10:32:14 +08:00
theme ::Style ,
Document , View ,
} ;
2022-02-18 12:58:18 +08:00
use crate ::{
compositor ::{ self , Compositor } ,
2022-05-24 00:10:48 +08:00
job ::Callback ,
2023-12-01 07:03:27 +08:00
ui ::{ self , overlay ::overlaid , DynamicPicker , FileLocation , Picker , Popup , PromptEvent } ,
2022-02-18 12:58:18 +08:00
} ;
2022-10-16 01:16:17 +08:00
use std ::{
2023-03-20 08:18:08 +08:00
cmp ::Ordering ,
collections ::{ BTreeMap , HashSet } ,
fmt ::Write ,
future ::Future ,
2023-08-28 02:32:17 +08:00
path ::{ Path , PathBuf } ,
2022-10-16 01:16:17 +08:00
} ;
2022-02-18 12:58:18 +08:00
2023-03-21 00:44:04 +08:00
/// Gets the first language server that is attached to a document which supports a specific feature.
/// If there is no configured language server that supports the feature, this displays a status message.
/// Using this macro in a context where the editor automatically queries the LSP
/// (instead of when the user explicitly does so via a keybind like `gd`)
2023-11-25 20:55:49 +08:00
/// will spam the "No configured language server supports \<feature>" status message confusingly.
2023-03-21 00:44:04 +08:00
#[ macro_export ]
macro_rules ! language_server_with_feature {
( $editor :expr , $doc :expr , $feature :expr ) = > { {
let language_server = $doc . language_servers_with_feature ( $feature ) . next ( ) ;
match language_server {
Some ( language_server ) = > language_server ,
None = > {
$editor . set_status ( format! (
" No configured language server supports {} " ,
$feature
) ) ;
return ;
}
}
} } ;
}
2022-07-02 19:21:27 +08:00
impl ui ::menu ::Item for lsp ::Location {
/// Current working directory.
type Data = PathBuf ;
2022-12-25 13:54:09 +08:00
fn format ( & self , cwdir : & Self ::Data ) -> Row {
2022-10-16 01:16:17 +08:00
// The preallocation here will overallocate a few characters since it will account for the
// URL's scheme, which is not used most of the time since that scheme will be "file://".
// Those extra chars will be used to avoid allocating when writing the line number (in the
// common case where it has 5 digits or less, which should be enough for a cast majority
// of usages).
let mut res = String ::with_capacity ( self . uri . as_str ( ) . len ( ) ) ;
if self . uri . scheme ( ) = = " file " {
// With the preallocation above and UTF-8 paths already, this closure will do one (1)
// allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`.
let mut write_path_to_res = | | -> Option < ( ) > {
let path = self . uri . to_file_path ( ) . ok ( ) ? ;
2022-11-04 20:01:17 +08:00
res . push_str ( & path . strip_prefix ( cwdir ) . unwrap_or ( & path ) . to_string_lossy ( ) ) ;
2022-10-16 01:16:17 +08:00
Some ( ( ) )
} ;
write_path_to_res ( ) ;
} else {
// Never allocates since we declared the string with this capacity already.
res . push_str ( self . uri . as_str ( ) ) ;
}
// Most commonly, this will not allocate, especially on Unix systems where the root prefix
// is a simple `/` and not `C:\` (with whatever drive letter)
2023-04-03 22:22:43 +08:00
write! ( & mut res , " :{} " , self . range . start . line + 1 )
2022-10-16 01:16:17 +08:00
. expect ( " Will only failed if allocating fail " ) ;
res . into ( )
2022-07-02 19:21:27 +08:00
}
}
2022-05-24 00:10:48 +08:00
struct SymbolInformationItem {
symbol : lsp ::SymbolInformation ,
offset_encoding : OffsetEncoding ,
}
impl ui ::menu ::Item for SymbolInformationItem {
2022-07-02 19:21:27 +08:00
/// Path to currently focussed document
type Data = Option < lsp ::Url > ;
2022-12-25 13:54:09 +08:00
fn format ( & self , current_doc_path : & Self ::Data ) -> Row {
2022-05-24 00:10:48 +08:00
if current_doc_path . as_ref ( ) = = Some ( & self . symbol . location . uri ) {
self . symbol . name . as_str ( ) . into ( )
2022-07-02 19:21:27 +08:00
} else {
2022-05-24 00:10:48 +08:00
match self . symbol . location . uri . to_file_path ( ) {
2022-07-02 19:21:27 +08:00
Ok ( path ) = > {
2022-10-16 01:16:17 +08:00
let get_relative_path = path ::get_relative_path ( path . as_path ( ) ) ;
2022-05-24 00:10:48 +08:00
format! (
" {} ({}) " ,
& self . symbol . name ,
get_relative_path . to_string_lossy ( )
)
. into ( )
2022-07-02 19:21:27 +08:00
}
2022-05-24 00:10:48 +08:00
Err ( _ ) = > format! ( " {} ( {} ) " , & self . symbol . name , & self . symbol . location . uri ) . into ( ) ,
2022-07-02 19:21:27 +08:00
}
}
}
}
struct DiagnosticStyles {
hint : Style ,
info : Style ,
warning : Style ,
error : Style ,
}
struct PickerDiagnostic {
2023-08-28 02:32:17 +08:00
path : PathBuf ,
2022-07-02 19:21:27 +08:00
diag : lsp ::Diagnostic ,
2022-05-24 00:10:48 +08:00
offset_encoding : OffsetEncoding ,
2022-07-02 19:21:27 +08:00
}
impl ui ::menu ::Item for PickerDiagnostic {
2022-07-05 21:01:25 +08:00
type Data = ( DiagnosticStyles , DiagnosticsFormat ) ;
2022-07-02 19:21:27 +08:00
2022-12-25 13:54:09 +08:00
fn format ( & self , ( styles , format ) : & Self ::Data ) -> Row {
2022-07-02 19:21:27 +08:00
let mut style = self
. diag
. severity
. map ( | s | match s {
DiagnosticSeverity ::HINT = > styles . hint ,
DiagnosticSeverity ::INFORMATION = > styles . info ,
DiagnosticSeverity ::WARNING = > styles . warning ,
DiagnosticSeverity ::ERROR = > styles . error ,
_ = > Style ::default ( ) ,
} )
. unwrap_or_default ( ) ;
// remove background as it is distracting in the picker list
style . bg = None ;
2023-03-14 01:01:21 +08:00
let code = match self . diag . code . as_ref ( ) {
Some ( NumberOrString ::Number ( n ) ) = > format! ( " ( {n} ) " ) ,
Some ( NumberOrString ::String ( s ) ) = > format! ( " ( {s} ) " ) ,
None = > String ::new ( ) ,
} ;
2022-07-02 19:21:27 +08:00
2022-07-05 21:01:25 +08:00
let path = match format {
DiagnosticsFormat ::HideSourcePath = > String ::new ( ) ,
DiagnosticsFormat ::ShowSourcePath = > {
2023-08-28 02:32:17 +08:00
let path = path ::get_truncated_path ( & self . path ) ;
2022-10-16 01:16:17 +08:00
format! ( " {} : " , path . to_string_lossy ( ) )
2022-07-05 21:01:25 +08:00
}
} ;
2022-07-02 19:21:27 +08:00
Spans ::from ( vec! [
2022-07-05 21:01:25 +08:00
Span ::raw ( path ) ,
2022-07-02 19:21:27 +08:00
Span ::styled ( & self . diag . message , style ) ,
2022-07-05 20:24:57 +08:00
Span ::styled ( code , style ) ,
2022-07-02 19:21:27 +08:00
] )
2022-12-25 13:54:09 +08:00
. into ( )
2022-07-02 19:21:27 +08:00
}
}
2022-02-18 13:01:50 +08:00
fn location_to_file_location ( location : & lsp ::Location ) -> FileLocation {
let path = location . uri . to_file_path ( ) . unwrap ( ) ;
let line = Some ( (
location . range . start . line as usize ,
location . range . end . line as usize ,
) ) ;
2022-11-21 09:58:35 +08:00
( path . into ( ) , line )
2022-02-18 13:01:50 +08:00
}
2022-02-18 13:07:35 +08:00
fn jump_to_location (
editor : & mut Editor ,
location : & lsp ::Location ,
offset_encoding : OffsetEncoding ,
action : Action ,
) {
2022-06-06 23:19:01 +08:00
let ( view , doc ) = current! ( editor ) ;
push_jump ( view , doc ) ;
2022-06-02 12:58:46 +08:00
let path = match location . uri . to_file_path ( ) {
Ok ( path ) = > path ,
Err ( _ ) = > {
2022-06-02 13:06:58 +08:00
let err = format! ( " unable to convert URI to filepath: {} " , location . uri ) ;
editor . set_error ( err ) ;
2022-06-02 12:58:46 +08:00
return ;
}
} ;
2023-08-28 02:32:17 +08:00
jump_to_position ( editor , & path , location . range , offset_encoding , action ) ;
}
2023-08-08 21:17:29 +08:00
2023-08-28 02:32:17 +08:00
fn jump_to_position (
editor : & mut Editor ,
path : & Path ,
range : lsp ::Range ,
offset_encoding : OffsetEncoding ,
action : Action ,
) {
let doc = match editor . open ( path , action ) {
2023-08-08 21:17:29 +08:00
Ok ( id ) = > doc_mut! ( editor , & id ) ,
2022-06-21 04:47:48 +08:00
Err ( err ) = > {
2023-08-28 02:32:17 +08:00
let err = format! ( " failed to open path: {:?} : {:?} " , path , err ) ;
2022-06-21 04:47:48 +08:00
editor . set_error ( err ) ;
return ;
}
2023-08-08 21:17:29 +08:00
} ;
let view = view_mut! ( editor ) ;
2022-02-18 13:07:35 +08:00
// TODO: convert inside server
2023-08-28 02:32:17 +08:00
let new_range = if let Some ( new_range ) = lsp_range_to_range ( doc . text ( ) , range , offset_encoding )
{
new_range
} else {
log ::warn! ( " lsp position out of bounds - {:?} " , range ) ;
return ;
} ;
2023-04-21 11:50:37 +08:00
// we flip the range so that the cursor sits on the start of the symbol
// (for example start of the function).
doc . set_selection ( view . id , Selection ::single ( new_range . head , new_range . anchor ) ) ;
2023-08-08 21:17:29 +08:00
if action . align_view ( view , doc . id ( ) ) {
align_view ( doc , view , Align ::Center ) ;
}
2022-02-18 13:07:35 +08:00
}
2024-02-16 23:55:02 +08:00
type SymbolPicker = Picker < SymbolInformationItem , Option < lsp ::Url > > ;
2022-05-24 00:10:48 +08:00
fn sym_picker ( symbols : Vec < SymbolInformationItem > , current_path : Option < lsp ::Url > ) -> SymbolPicker {
2022-02-18 12:58:18 +08:00
// TODO: drop current_path comparison and instead use workspace: bool flag?
2024-02-16 23:55:02 +08:00
let columns = vec! [ ] ;
Picker ::new (
columns ,
0 ,
symbols ,
current_path ,
move | cx , item , action | {
jump_to_location (
cx . editor ,
& item . symbol . location ,
item . offset_encoding ,
action ,
) ;
} ,
)
2023-06-19 01:23:15 +08:00
. with_preview ( move | _editor , item | Some ( location_to_file_location ( & item . symbol . location ) ) )
2022-03-28 10:03:42 +08:00
. truncate_start ( false )
2022-02-18 12:58:18 +08:00
}
2022-07-05 21:01:25 +08:00
#[ derive(Copy, Clone, PartialEq) ]
enum DiagnosticsFormat {
ShowSourcePath ,
HideSourcePath ,
}
2024-02-16 23:55:02 +08:00
type DiagnosticsPicker = Picker < PickerDiagnostic , ( DiagnosticStyles , DiagnosticsFormat ) > ;
2022-06-30 17:16:18 +08:00
fn diag_picker (
cx : & Context ,
2024-04-08 08:46:32 +08:00
diagnostics : BTreeMap < PathBuf , Vec < ( lsp ::Diagnostic , LanguageServerId ) > > ,
2022-07-05 21:01:25 +08:00
format : DiagnosticsFormat ,
2024-02-16 23:55:02 +08:00
) -> DiagnosticsPicker {
2022-06-30 17:16:18 +08:00
// TODO: drop current_path comparison and instead use workspace: bool flag?
// flatten the map to a vec of (url, diag) pairs
let mut flat_diag = Vec ::new ( ) ;
2023-08-28 02:32:17 +08:00
for ( path , diags ) in diagnostics {
2022-06-30 17:16:18 +08:00
flat_diag . reserve ( diags . len ( ) ) ;
2023-03-17 22:30:49 +08:00
for ( diag , ls ) in diags {
2023-04-06 00:50:05 +08:00
if let Some ( ls ) = cx . editor . language_server_by_id ( ls ) {
2023-03-17 22:30:49 +08:00
flat_diag . push ( PickerDiagnostic {
2023-08-28 02:32:17 +08:00
path : path . clone ( ) ,
2023-03-17 22:30:49 +08:00
diag ,
offset_encoding : ls . offset_encoding ( ) ,
} ) ;
}
2022-06-30 17:16:18 +08:00
}
}
2022-07-02 19:21:27 +08:00
let styles = DiagnosticStyles {
hint : cx . editor . theme . get ( " hint " ) ,
info : cx . editor . theme . get ( " info " ) ,
warning : cx . editor . theme . get ( " warning " ) ,
error : cx . editor . theme . get ( " error " ) ,
} ;
2022-06-30 17:16:18 +08:00
2024-02-16 23:55:02 +08:00
let columns = vec! [ ] ;
2023-06-19 01:27:11 +08:00
Picker ::new (
2024-02-16 23:55:02 +08:00
columns ,
0 ,
2022-06-30 17:16:18 +08:00
flat_diag ,
2022-07-05 21:01:25 +08:00
( styles , format ) ,
2022-05-24 00:10:48 +08:00
move | cx ,
PickerDiagnostic {
2023-08-28 02:32:17 +08:00
path ,
2022-05-24 00:10:48 +08:00
diag ,
offset_encoding ,
} ,
action | {
2023-08-28 02:32:17 +08:00
jump_to_position ( cx . editor , path , diag . range , * offset_encoding , action )
2022-06-30 17:16:18 +08:00
} ,
)
2023-08-28 02:32:17 +08:00
. with_preview ( move | _editor , PickerDiagnostic { path , diag , .. } | {
let line = Some ( ( diag . range . start . line as usize , diag . range . end . line as usize ) ) ;
Some ( ( path . clone ( ) . into ( ) , line ) )
2023-06-19 01:23:15 +08:00
} )
2022-06-30 17:16:18 +08:00
. truncate_start ( false )
}
2022-02-18 12:58:18 +08:00
pub fn symbol_picker ( cx : & mut Context ) {
fn nested_to_flat (
2022-05-24 00:10:48 +08:00
list : & mut Vec < SymbolInformationItem > ,
2022-02-18 12:58:18 +08:00
file : & lsp ::TextDocumentIdentifier ,
symbol : lsp ::DocumentSymbol ,
2022-05-24 00:10:48 +08:00
offset_encoding : OffsetEncoding ,
2022-02-18 12:58:18 +08:00
) {
#[ allow(deprecated) ]
2022-05-24 00:10:48 +08:00
list . push ( SymbolInformationItem {
symbol : lsp ::SymbolInformation {
name : symbol . name ,
kind : symbol . kind ,
tags : symbol . tags ,
deprecated : symbol . deprecated ,
location : lsp ::Location ::new ( file . uri . clone ( ) , symbol . selection_range ) ,
container_name : None ,
} ,
offset_encoding ,
2022-02-18 12:58:18 +08:00
} ) ;
for child in symbol . children . into_iter ( ) . flatten ( ) {
2022-05-24 00:10:48 +08:00
nested_to_flat ( list , file , child , offset_encoding ) ;
2022-02-18 12:58:18 +08:00
}
}
let doc = doc! ( cx . editor ) ;
2023-03-20 08:18:08 +08:00
let mut seen_language_servers = HashSet ::new ( ) ;
2024-04-09 22:28:54 +08:00
let mut futures : FuturesOrdered < _ > = doc
2022-05-24 00:10:48 +08:00
. language_servers_with_feature ( LanguageServerFeature ::DocumentSymbols )
2023-03-20 08:18:08 +08:00
. filter ( | ls | seen_language_servers . insert ( ls . id ( ) ) )
2023-03-21 00:44:04 +08:00
. map ( | language_server | {
let request = language_server . document_symbols ( doc . identifier ( ) ) . unwrap ( ) ;
let offset_encoding = language_server . offset_encoding ( ) ;
let doc_id = doc . identifier ( ) ;
async move {
let json = request . await ? ;
let response : Option < lsp ::DocumentSymbolResponse > = serde_json ::from_value ( json ) ? ;
let symbols = match response {
Some ( symbols ) = > symbols ,
None = > return anyhow ::Ok ( vec! [ ] ) ,
} ;
// lsp has two ways to represent symbols (flat/nested)
// convert the nested variant to flat, so that we have a homogeneous list
let symbols = match symbols {
lsp ::DocumentSymbolResponse ::Flat ( symbols ) = > symbols
. into_iter ( )
. map ( | symbol | SymbolInformationItem {
symbol ,
offset_encoding ,
} )
. collect ( ) ,
lsp ::DocumentSymbolResponse ::Nested ( symbols ) = > {
let mut flat_symbols = Vec ::new ( ) ;
for symbol in symbols {
nested_to_flat ( & mut flat_symbols , & doc_id , symbol , offset_encoding )
}
flat_symbols
2022-05-24 00:10:48 +08:00
}
2023-03-21 00:44:04 +08:00
} ;
Ok ( symbols )
}
2022-05-24 00:10:48 +08:00
} )
. collect ( ) ;
2022-02-18 12:58:18 +08:00
let current_url = doc . url ( ) ;
2022-05-24 00:10:48 +08:00
if futures . is_empty ( ) {
cx . editor
2023-03-20 07:08:24 +08:00
. set_error ( " No configured language server supports document symbols " ) ;
2022-05-24 00:10:48 +08:00
return ;
}
2022-02-18 12:58:18 +08:00
2022-05-24 00:10:48 +08:00
cx . jobs . callback ( async move {
let mut symbols = Vec ::new ( ) ;
// TODO if one symbol request errors, all other requests are discarded (even if they're valid)
while let Some ( mut lsp_items ) = futures . try_next ( ) . await ? {
symbols . append ( & mut lsp_items ) ;
}
2023-03-20 04:22:29 +08:00
let call = move | _editor : & mut Editor , compositor : & mut Compositor | {
2022-05-24 00:10:48 +08:00
let picker = sym_picker ( symbols , current_url ) ;
compositor . push ( Box ::new ( overlaid ( picker ) ) )
} ;
Ok ( Callback ::EditorCompositor ( Box ::new ( call ) ) )
} ) ;
2022-02-18 12:58:18 +08:00
}
pub fn workspace_symbol_picker ( cx : & mut Context ) {
let doc = doc! ( cx . editor ) ;
2023-06-08 08:48:11 +08:00
if doc
. language_servers_with_feature ( LanguageServerFeature ::WorkspaceSymbols )
. count ( )
= = 0
{
cx . editor
. set_error ( " No configured language server supports workspace symbols " ) ;
return ;
}
2022-05-24 00:10:48 +08:00
let get_symbols = move | pattern : String , editor : & mut Editor | {
let doc = doc! ( editor ) ;
2023-03-20 08:18:08 +08:00
let mut seen_language_servers = HashSet ::new ( ) ;
2024-04-09 22:28:54 +08:00
let mut futures : FuturesOrdered < _ > = doc
2022-05-24 00:10:48 +08:00
. language_servers_with_feature ( LanguageServerFeature ::WorkspaceSymbols )
2023-03-20 08:18:08 +08:00
. filter ( | ls | seen_language_servers . insert ( ls . id ( ) ) )
2023-03-21 00:44:04 +08:00
. map ( | language_server | {
let request = language_server . workspace_symbols ( pattern . clone ( ) ) . unwrap ( ) ;
let offset_encoding = language_server . offset_encoding ( ) ;
async move {
let json = request . await ? ;
let response =
serde_json ::from_value ::< Option < Vec < lsp ::SymbolInformation > > > ( json ) ?
. unwrap_or_default ( )
. into_iter ( )
. map ( | symbol | SymbolInformationItem {
symbol ,
offset_encoding ,
} )
. collect ( ) ;
anyhow ::Ok ( response )
}
2022-05-24 00:10:48 +08:00
} )
. collect ( ) ;
if futures . is_empty ( ) {
2023-03-20 07:08:24 +08:00
editor . set_error ( " No configured language server supports workspace symbols " ) ;
2022-11-22 10:52:23 +08:00
}
2022-02-18 12:58:18 +08:00
2022-05-24 00:10:48 +08:00
async move {
let mut symbols = Vec ::new ( ) ;
// TODO if one symbol request errors, all other requests are discarded (even if they're valid)
while let Some ( mut lsp_items ) = futures . try_next ( ) . await ? {
symbols . append ( & mut lsp_items ) ;
}
anyhow ::Ok ( symbols )
}
. boxed ( )
} ;
2022-07-20 00:45:03 +08:00
2022-05-24 00:10:48 +08:00
let current_url = doc . url ( ) ;
let initial_symbols = get_symbols ( " " . to_owned ( ) , cx . editor ) ;
2022-07-20 00:45:03 +08:00
2022-05-24 00:10:48 +08:00
cx . jobs . callback ( async move {
let symbols = initial_symbols . await ? ;
let call = move | _editor : & mut Editor , compositor : & mut Compositor | {
let picker = sym_picker ( symbols , current_url ) ;
2022-07-20 00:45:03 +08:00
let dyn_picker = DynamicPicker ::new ( picker , Box ::new ( get_symbols ) ) ;
2023-04-07 23:10:38 +08:00
compositor . push ( Box ::new ( overlaid ( dyn_picker ) ) )
2022-05-24 00:10:48 +08:00
} ;
Ok ( Callback ::EditorCompositor ( Box ::new ( call ) ) )
} ) ;
2022-02-18 12:58:18 +08:00
}
2022-06-30 17:16:18 +08:00
pub fn diagnostics_picker ( cx : & mut Context ) {
let doc = doc! ( cx . editor ) ;
2023-08-28 02:32:17 +08:00
if let Some ( current_path ) = doc . path ( ) {
2022-06-30 17:16:18 +08:00
let diagnostics = cx
. editor
. diagnostics
2023-08-28 02:32:17 +08:00
. get ( current_path )
2022-06-30 17:16:18 +08:00
. cloned ( )
. unwrap_or_default ( ) ;
let picker = diag_picker (
cx ,
2023-08-28 02:32:17 +08:00
[ ( current_path . clone ( ) , diagnostics ) ] . into ( ) ,
2022-07-05 21:01:25 +08:00
DiagnosticsFormat ::HideSourcePath ,
2022-06-30 17:16:18 +08:00
) ;
2023-04-07 23:10:38 +08:00
cx . push_layer ( Box ::new ( overlaid ( picker ) ) ) ;
2022-06-30 17:16:18 +08:00
}
}
pub fn workspace_diagnostics_picker ( cx : & mut Context ) {
2022-05-24 00:10:48 +08:00
// TODO not yet filtered by LanguageServerFeature, need to do something similar as Document::shown_diagnostics here for all open documents
2022-06-30 17:16:18 +08:00
let diagnostics = cx . editor . diagnostics . clone ( ) ;
2023-08-28 02:32:17 +08:00
let picker = diag_picker ( cx , diagnostics , DiagnosticsFormat ::ShowSourcePath ) ;
2023-04-07 23:10:38 +08:00
cx . push_layer ( Box ::new ( overlaid ( picker ) ) ) ;
2022-06-30 17:16:18 +08:00
}
2022-05-24 00:10:48 +08:00
struct CodeActionOrCommandItem {
lsp_item : lsp ::CodeActionOrCommand ,
2024-04-08 08:46:32 +08:00
language_server_id : LanguageServerId ,
2022-05-24 00:10:48 +08:00
}
impl ui ::menu ::Item for CodeActionOrCommandItem {
2022-07-02 19:21:27 +08:00
type Data = ( ) ;
2022-12-25 13:54:09 +08:00
fn format ( & self , _data : & Self ::Data ) -> Row {
2022-05-24 00:10:48 +08:00
match & self . lsp_item {
2022-07-02 19:21:27 +08:00
lsp ::CodeActionOrCommand ::CodeAction ( action ) = > action . title . as_str ( ) . into ( ) ,
lsp ::CodeActionOrCommand ::Command ( command ) = > command . title . as_str ( ) . into ( ) ,
2022-02-18 12:58:18 +08:00
}
}
}
2022-10-14 17:50:09 +08:00
/// Determines the category of the `CodeAction` using the `CodeAction::kind` field.
/// Returns a number that represent these categories.
/// Categories with a lower number should be displayed first.
///
///
/// While the `kind` field is defined as open ended in the LSP spec (any value may be used)
/// in practice a closed set of common values (mostly suggested in the LSP spec) are used.
2023-04-07 23:10:38 +08:00
/// VSCode displays each of these categories separately (separated by a heading in the codeactions picker)
2022-10-14 17:50:09 +08:00
/// to make them easier to navigate. Helix does not display these headings to the user.
/// However it does sort code actions by their categories to achieve the same order as the VScode picker,
/// just without the headings.
///
/// The order used here is modeled after the [vscode sourcecode](https://github.com/microsoft/vscode/blob/eaec601dd69aeb4abb63b9601a6f44308c8d8c6e/src/vs/editor/contrib/codeAction/browser/codeActionWidget.ts>)
fn action_category ( action : & CodeActionOrCommand ) -> u32 {
if let CodeActionOrCommand ::CodeAction ( CodeAction {
kind : Some ( kind ) , ..
} ) = action
{
let mut components = kind . as_str ( ) . split ( '.' ) ;
match components . next ( ) {
Some ( " quickfix " ) = > 0 ,
Some ( " refactor " ) = > match components . next ( ) {
Some ( " extract " ) = > 1 ,
Some ( " inline " ) = > 2 ,
Some ( " rewrite " ) = > 3 ,
Some ( " move " ) = > 4 ,
Some ( " surround " ) = > 5 ,
_ = > 7 ,
} ,
Some ( " source " ) = > 6 ,
_ = > 7 ,
}
} else {
7
}
}
2023-04-07 23:10:38 +08:00
fn action_preferred ( action : & CodeActionOrCommand ) -> bool {
2022-10-14 17:50:09 +08:00
matches! (
action ,
CodeActionOrCommand ::CodeAction ( CodeAction {
is_preferred : Some ( true ) ,
..
} )
)
}
fn action_fixes_diagnostics ( action : & CodeActionOrCommand ) -> bool {
matches! (
action ,
CodeActionOrCommand ::CodeAction ( CodeAction {
diagnostics : Some ( diagnostics ) ,
..
} ) if ! diagnostics . is_empty ( )
)
}
2022-02-18 12:58:18 +08:00
pub fn code_action ( cx : & mut Context ) {
let ( view , doc ) = current! ( cx . editor ) ;
2022-04-17 11:05:23 +08:00
let selection_range = doc . selection ( view . id ) . primary ( ) ;
2023-03-20 08:18:08 +08:00
let mut seen_language_servers = HashSet ::new ( ) ;
2024-04-09 22:28:54 +08:00
let mut futures : FuturesOrdered < _ > = doc
2022-05-24 00:10:48 +08:00
. language_servers_with_feature ( LanguageServerFeature ::CodeAction )
2023-03-20 08:18:08 +08:00
. filter ( | ls | seen_language_servers . insert ( ls . id ( ) ) )
2022-05-24 00:10:48 +08:00
// TODO this should probably already been filtered in something like "language_servers_with_feature"
. filter_map ( | language_server | {
let offset_encoding = language_server . offset_encoding ( ) ;
let language_server_id = language_server . id ( ) ;
let range = range_to_lsp_range ( doc . text ( ) , selection_range , offset_encoding ) ;
// Filter and convert overlapping diagnostics
let code_action_context = lsp ::CodeActionContext {
diagnostics : doc
. diagnostics ( )
. iter ( )
. filter ( | & diag | {
selection_range
. overlaps ( & helix_core ::Range ::new ( diag . range . start , diag . range . end ) )
} )
. map ( | diag | diagnostic_to_lsp_diagnostic ( doc . text ( ) , diag , offset_encoding ) )
. collect ( ) ,
only : None ,
trigger_kind : Some ( CodeActionTriggerKind ::INVOKED ) ,
} ;
let code_action_request =
language_server . code_actions ( doc . identifier ( ) , range , code_action_context ) ? ;
2023-03-28 03:21:42 +08:00
Some ( ( code_action_request , language_server_id ) )
2022-05-24 00:10:48 +08:00
} )
2023-03-28 03:21:42 +08:00
. map ( | ( request , ls_id ) | async move {
2022-05-24 00:10:48 +08:00
let json = request . await ? ;
let response : Option < lsp ::CodeActionResponse > = serde_json ::from_value ( json ) ? ;
2022-10-07 07:44:53 +08:00
let mut actions = match response {
2022-02-18 12:58:18 +08:00
Some ( a ) = > a ,
2022-05-24 00:10:48 +08:00
None = > return anyhow ::Ok ( Vec ::new ( ) ) ,
2022-02-18 12:58:18 +08:00
} ;
2022-10-07 07:44:53 +08:00
2022-10-14 17:50:09 +08:00
// remove disabled code actions
actions . retain ( | action | {
matches! (
action ,
CodeActionOrCommand ::Command ( _ )
| CodeActionOrCommand ::CodeAction ( CodeAction { disabled : None , .. } )
)
} ) ;
// Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec.
2023-04-07 23:10:38 +08:00
// Many details are modeled after vscode because language servers are usually tested against it.
2022-10-14 17:50:09 +08:00
// VScode sorts the codeaction two times:
//
// First the codeactions that fix some diagnostics are moved to the front.
// If both codeactions fix some diagnostics (or both fix none) the codeaction
2023-04-07 23:10:38 +08:00
// that is marked with `is_preferred` is shown first. The codeactions are then shown in separate
2022-10-14 17:50:09 +08:00
// submenus that only contain a certain category (see `action_category`) of actions.
//
// Below this done in in a single sorting step
actions . sort_by ( | action1 , action2 | {
// sort actions by category
let order = action_category ( action1 ) . cmp ( & action_category ( action2 ) ) ;
if order ! = Ordering ::Equal {
return order ;
2022-10-08 20:18:53 +08:00
}
2022-10-14 17:50:09 +08:00
// within the categories sort by relevancy.
// Modeled after the `codeActionsComparator` function in vscode:
// https://github.com/microsoft/vscode/blob/eaec601dd69aeb4abb63b9601a6f44308c8d8c6e/src/vs/editor/contrib/codeAction/browser/codeAction.ts
// if one code action fixes a diagnostic but the other one doesn't show it first
let order = action_fixes_diagnostics ( action1 )
. cmp ( & action_fixes_diagnostics ( action2 ) )
. reverse ( ) ;
if order ! = Ordering ::Equal {
return order ;
}
2023-04-07 23:10:38 +08:00
// if one of the codeactions is marked as preferred show it first
2022-10-14 17:50:09 +08:00
// otherwise keep the original LSP sorting
2023-04-07 23:10:38 +08:00
action_preferred ( action1 )
. cmp ( & action_preferred ( action2 ) )
2022-10-14 17:50:09 +08:00
. reverse ( )
2022-10-08 20:18:53 +08:00
} ) ;
2022-10-07 07:44:53 +08:00
2022-05-24 00:10:48 +08:00
Ok ( actions
. into_iter ( )
. map ( | lsp_item | CodeActionOrCommandItem {
lsp_item ,
language_server_id : ls_id ,
} )
. collect ( ) )
} )
. collect ( ) ;
if futures . is_empty ( ) {
cx . editor
2023-03-20 07:08:24 +08:00
. set_error ( " No configured language server supports code actions " ) ;
2022-05-24 00:10:48 +08:00
return ;
}
cx . jobs . callback ( async move {
let mut actions = Vec ::new ( ) ;
// TODO if one code action request errors, all other requests are ignored (even if they're valid)
while let Some ( mut lsp_items ) = futures . try_next ( ) . await ? {
actions . append ( & mut lsp_items ) ;
}
let call = move | editor : & mut Editor , compositor : & mut Compositor | {
if actions . is_empty ( ) {
editor . set_error ( " No code actions available " ) ;
return ;
}
let mut picker = ui ::Menu ::new ( actions , ( ) , move | editor , action , event | {
2022-10-14 02:10:10 +08:00
if event ! = PromptEvent ::Validate {
return ;
}
2022-02-18 12:58:18 +08:00
2022-10-14 02:10:10 +08:00
// always present here
2022-05-24 00:10:48 +08:00
let action = action . unwrap ( ) ;
2023-07-27 10:57:19 +08:00
let Some ( language_server ) = editor . language_server_by_id ( action . language_server_id )
else {
2023-03-28 03:21:42 +08:00
editor . set_error ( " Language Server disappeared " ) ;
return ;
} ;
let offset_encoding = language_server . offset_encoding ( ) ;
2022-10-07 07:44:53 +08:00
2022-05-24 00:10:48 +08:00
match & action . lsp_item {
2022-10-14 02:10:10 +08:00
lsp ::CodeActionOrCommand ::Command ( command ) = > {
log ::debug! ( " code action command: {:?} " , command ) ;
2022-05-24 00:10:48 +08:00
execute_lsp_command ( editor , action . language_server_id , command . clone ( ) ) ;
2022-10-14 02:10:10 +08:00
}
lsp ::CodeActionOrCommand ::CodeAction ( code_action ) = > {
log ::debug! ( " code action: {:?} " , code_action ) ;
2023-07-22 03:50:08 +08:00
// we support lsp "codeAction/resolve" for `edit` and `command` fields
let mut resolved_code_action = None ;
if code_action . edit . is_none ( ) | | code_action . command . is_none ( ) {
2023-07-27 10:57:19 +08:00
if let Some ( future ) =
language_server . resolve_code_action ( code_action . clone ( ) )
{
2023-07-22 03:50:08 +08:00
if let Ok ( response ) = helix_lsp ::block_on ( future ) {
2023-07-27 10:57:19 +08:00
if let Ok ( code_action ) =
serde_json ::from_value ::< CodeAction > ( response )
{
2023-07-22 03:50:08 +08:00
resolved_code_action = Some ( code_action ) ;
}
}
}
}
2023-07-27 10:57:19 +08:00
let resolved_code_action =
resolved_code_action . as_ref ( ) . unwrap_or ( code_action ) ;
2023-07-22 03:50:08 +08:00
if let Some ( ref workspace_edit ) = resolved_code_action . edit {
2024-01-29 00:34:45 +08:00
let _ = editor . apply_workspace_edit ( offset_encoding , workspace_edit ) ;
2022-02-18 12:58:18 +08:00
}
2022-10-14 02:10:10 +08:00
// 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 {
2022-05-24 00:10:48 +08:00
execute_lsp_command ( editor , action . language_server_id , command . clone ( ) ) ;
2022-10-07 07:44:53 +08:00
}
2022-02-18 12:58:18 +08:00
}
2022-10-14 02:10:10 +08:00
}
} ) ;
2022-02-18 12:58:18 +08:00
picker . move_down ( ) ; // pre-select the first item
2024-04-23 22:15:42 +08:00
let popup = Popup ::new ( " code-action " , picker ) . with_scrollbar ( false ) ;
2023-12-19 09:17:12 +08:00
2022-03-03 09:14:50 +08:00
compositor . replace_or_push ( " code-action " , popup ) ;
2022-05-24 00:10:48 +08:00
} ;
Ok ( Callback ::EditorCompositor ( Box ::new ( call ) ) )
} ) ;
2022-02-18 12:58:18 +08:00
}
2022-11-09 17:17:09 +08:00
impl ui ::menu ::Item for lsp ::Command {
type Data = ( ) ;
2022-12-25 13:54:09 +08:00
fn format ( & self , _data : & Self ::Data ) -> Row {
2022-11-09 17:17:09 +08:00
self . title . as_str ( ) . into ( )
}
}
2024-04-08 08:46:32 +08:00
pub fn execute_lsp_command (
editor : & mut Editor ,
language_server_id : LanguageServerId ,
cmd : lsp ::Command ,
) {
2022-02-18 12:58:18 +08:00
// the command is executed on the server and communicated back
// to the client asynchronously using workspace edits
2022-05-24 00:10:48 +08:00
let future = match editor
2023-04-06 00:50:05 +08:00
. language_server_by_id ( language_server_id )
2022-05-24 00:10:48 +08:00
. and_then ( | language_server | language_server . command ( cmd ) )
{
2022-11-22 10:52:23 +08:00
Some ( future ) = > future ,
None = > {
editor . set_error ( " Language server does not support executing commands " ) ;
return ;
}
} ;
2022-02-18 12:58:18 +08:00
tokio ::spawn ( async move {
2022-11-22 10:52:23 +08:00
let res = future . await ;
2022-02-18 12:58:18 +08:00
if let Err ( e ) = res {
log ::error! ( " execute LSP command: {} " , e ) ;
}
} ) ;
}
2023-03-12 08:01:15 +08:00
#[ derive(Debug) ]
pub struct ApplyEditError {
pub kind : ApplyEditErrorKind ,
pub failed_change_idx : usize ,
}
#[ derive(Debug) ]
pub enum ApplyEditErrorKind {
DocumentChanged ,
FileNotFound ,
UnknownURISchema ,
IoError ( std ::io ::Error ) ,
// TODO: check edits before applying and propagate failure
// InvalidEdit,
}
impl ToString for ApplyEditErrorKind {
fn to_string ( & self ) -> String {
match self {
ApplyEditErrorKind ::DocumentChanged = > " document has changed " . to_string ( ) ,
ApplyEditErrorKind ::FileNotFound = > " file not found " . to_string ( ) ,
ApplyEditErrorKind ::UnknownURISchema = > " URI schema not supported " . to_string ( ) ,
ApplyEditErrorKind ::IoError ( err ) = > err . to_string ( ) ,
}
}
}
2024-01-25 13:11:12 +08:00
/// Precondition: `locations` should be non-empty.
2022-02-18 12:58:18 +08:00
fn goto_impl (
editor : & mut Editor ,
compositor : & mut Compositor ,
locations : Vec < lsp ::Location > ,
offset_encoding : OffsetEncoding ,
) {
2024-01-17 02:59:48 +08:00
let cwdir = helix_stdx ::env ::current_working_dir ( ) ;
2022-02-18 12:58:18 +08:00
match locations . as_slice ( ) {
[ location ] = > {
2022-02-18 13:07:35 +08:00
jump_to_location ( editor , location , offset_encoding , Action ::Replace ) ;
2022-02-18 12:58:18 +08:00
}
2024-01-25 13:11:12 +08:00
[ ] = > unreachable! ( " `locations` should be non-empty for `goto_impl` " ) ,
2022-02-18 12:58:18 +08:00
_locations = > {
2024-02-16 23:55:02 +08:00
let columns = vec! [ ] ;
let picker = Picker ::new ( columns , 0 , locations , cwdir , move | cx , location , action | {
2023-06-19 01:23:15 +08:00
jump_to_location ( cx . editor , location , offset_encoding , action )
} )
. with_preview ( move | _editor , location | Some ( location_to_file_location ( location ) ) ) ;
2023-04-07 23:10:38 +08:00
compositor . push ( Box ::new ( overlaid ( picker ) ) ) ;
2022-02-18 12:58:18 +08:00
}
}
}
2022-02-18 13:11:50 +08:00
fn to_locations ( definitions : Option < lsp ::GotoDefinitionResponse > ) -> Vec < lsp ::Location > {
match definitions {
Some ( lsp ::GotoDefinitionResponse ::Scalar ( location ) ) = > vec! [ location ] ,
Some ( lsp ::GotoDefinitionResponse ::Array ( locations ) ) = > locations ,
Some ( lsp ::GotoDefinitionResponse ::Link ( locations ) ) = > locations
. into_iter ( )
. map ( | location_link | lsp ::Location {
uri : location_link . target_uri ,
range : location_link . target_range ,
} )
. collect ( ) ,
None = > Vec ::new ( ) ,
}
}
2023-03-19 06:13:58 +08:00
fn goto_single_impl < P , F > ( cx : & mut Context , feature : LanguageServerFeature , request_provider : P )
where
P : Fn ( & Client , lsp ::Position , lsp ::TextDocumentIdentifier ) -> Option < F > ,
F : Future < Output = helix_lsp ::Result < serde_json ::Value > > + 'static + Send ,
{
2023-01-31 18:38:53 +08:00
let ( view , doc ) = current! ( cx . editor ) ;
2023-03-21 00:44:04 +08:00
let language_server = language_server_with_feature! ( cx . editor , doc , feature ) ;
let offset_encoding = language_server . offset_encoding ( ) ;
let pos = doc . position ( view . id , offset_encoding ) ;
let future = request_provider ( language_server , pos , doc . identifier ( ) ) . unwrap ( ) ;
cx . callback (
future ,
move | editor , compositor , response : Option < lsp ::GotoDefinitionResponse > | {
let items = to_locations ( response ) ;
2024-01-25 13:11:12 +08:00
if items . is_empty ( ) {
editor . set_error ( " No definition found. " ) ;
} else {
goto_impl ( editor , compositor , items , offset_encoding ) ;
}
2023-03-21 00:44:04 +08:00
} ,
) ;
2023-03-19 06:13:58 +08:00
}
2023-01-31 18:38:53 +08:00
2023-03-19 06:13:58 +08:00
pub fn goto_declaration ( cx : & mut Context ) {
goto_single_impl (
cx ,
LanguageServerFeature ::GotoDeclaration ,
| ls , pos , doc_id | ls . goto_declaration ( doc_id , pos , None ) ,
2023-01-31 18:38:53 +08:00
) ;
}
2022-02-18 12:58:18 +08:00
pub fn goto_definition ( cx : & mut Context ) {
2023-03-19 06:13:58 +08:00
goto_single_impl (
cx ,
LanguageServerFeature ::GotoDefinition ,
| ls , pos , doc_id | ls . goto_definition ( doc_id , pos , None ) ,
2022-02-18 12:58:18 +08:00
) ;
}
pub fn goto_type_definition ( cx : & mut Context ) {
2023-03-19 06:13:58 +08:00
goto_single_impl (
cx ,
LanguageServerFeature ::GotoTypeDefinition ,
| ls , pos , doc_id | ls . goto_type_definition ( doc_id , pos , None ) ,
2022-02-18 12:58:18 +08:00
) ;
}
pub fn goto_implementation ( cx : & mut Context ) {
2023-03-19 06:13:58 +08:00
goto_single_impl (
cx ,
LanguageServerFeature ::GotoImplementation ,
| ls , pos , doc_id | ls . goto_implementation ( doc_id , pos , None ) ,
2022-02-18 12:58:18 +08:00
) ;
}
pub fn goto_reference ( cx : & mut Context ) {
2023-04-27 22:30:15 +08:00
let config = cx . editor . config ( ) ;
2022-02-18 12:58:18 +08:00
let ( view , doc ) = current! ( cx . editor ) ;
2023-03-21 00:44:04 +08:00
// TODO could probably support multiple language servers,
// not sure if there's a real practical use case for this though
let language_server =
language_server_with_feature! ( cx . editor , doc , LanguageServerFeature ::GotoReference ) ;
let offset_encoding = language_server . offset_encoding ( ) ;
let pos = doc . position ( view . id , offset_encoding ) ;
let future = language_server
. goto_reference (
doc . identifier ( ) ,
pos ,
config . lsp . goto_reference_include_declaration ,
None ,
)
. unwrap ( ) ;
cx . callback (
future ,
move | editor , compositor , response : Option < Vec < lsp ::Location > > | {
let items = response . unwrap_or_default ( ) ;
2024-01-25 13:11:12 +08:00
if items . is_empty ( ) {
editor . set_error ( " No references found. " ) ;
} else {
goto_impl ( editor , compositor , items , offset_encoding ) ;
}
2022-02-18 12:58:18 +08:00
} ,
2023-03-21 00:44:04 +08:00
) ;
2022-02-18 12:58:18 +08:00
}
pub fn signature_help ( cx : & mut Context ) {
2023-12-01 07:03:27 +08:00
cx . editor
. handlers
. trigger_signature_help ( SignatureHelpInvoked ::Manual , cx . editor )
2022-02-18 12:58:18 +08:00
}
2022-06-27 19:19:56 +08:00
2022-02-18 12:58:18 +08:00
pub fn hover ( cx : & mut Context ) {
let ( view , doc ) = current! ( cx . editor ) ;
2023-03-21 00:44:04 +08:00
// TODO support multiple language servers (merge UI somehow)
let language_server =
language_server_with_feature! ( cx . editor , doc , LanguageServerFeature ::Hover ) ;
2022-02-18 12:58:18 +08:00
// TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
2023-03-21 00:44:04 +08:00
let pos = doc . position ( view . id , language_server . offset_encoding ( ) ) ;
let future = language_server
. text_document_hover ( doc . identifier ( ) , pos , None )
. unwrap ( ) ;
2022-02-18 12:58:18 +08:00
cx . callback (
future ,
2022-03-22 22:25:40 +08:00
move | editor , compositor , response : Option < lsp ::Hover > | {
2022-02-18 12:58:18 +08:00
if let Some ( hover ) = response {
// hover.contents / .range <- used for visualizing
fn marked_string_to_markdown ( contents : lsp ::MarkedString ) -> String {
match contents {
lsp ::MarkedString ::String ( contents ) = > contents ,
lsp ::MarkedString ::LanguageString ( string ) = > {
if string . language = = " markdown " {
string . value
} else {
format! ( " ``` {} \n {} \n ``` " , string . language , string . value )
}
}
}
}
let contents = match hover . contents {
lsp ::HoverContents ::Scalar ( contents ) = > marked_string_to_markdown ( contents ) ,
lsp ::HoverContents ::Array ( contents ) = > contents
. into_iter ( )
. map ( marked_string_to_markdown )
. collect ::< Vec < _ > > ( )
. join ( " \n \n " ) ,
lsp ::HoverContents ::Markup ( contents ) = > contents . value ,
} ;
// skip if contents empty
2022-02-21 15:45:48 +08:00
let contents = ui ::Markdown ::new ( contents , editor . syn_loader . clone ( ) ) ;
2022-02-23 11:46:12 +08:00
let popup = Popup ::new ( " hover " , contents ) . auto_close ( true ) ;
2022-03-03 09:14:50 +08:00
compositor . replace_or_push ( " hover " , popup ) ;
2022-02-18 12:58:18 +08:00
}
} ,
) ;
}
2022-06-27 19:19:56 +08:00
2022-02-18 12:58:18 +08:00
pub fn rename_symbol ( cx : & mut Context ) {
2023-03-08 10:11:43 +08:00
fn get_prefill_from_word_boundary ( editor : & Editor ) -> String {
let ( view , doc ) = current_ref! ( editor ) ;
let text = doc . text ( ) . slice ( .. ) ;
let primary_selection = doc . selection ( view . id ) . primary ( ) ;
if primary_selection . len ( ) > 1 {
primary_selection
} else {
use helix_core ::textobject ::{ textobject_word , TextObject } ;
textobject_word ( text , primary_selection , TextObject ::Inside , 1 , false )
}
. fragment ( text )
. into ( )
2022-07-18 09:17:13 +08:00
}
2023-03-08 10:11:43 +08:00
fn get_prefill_from_lsp_response (
editor : & Editor ,
offset_encoding : OffsetEncoding ,
response : Option < lsp ::PrepareRenameResponse > ,
) -> Result < String , & 'static str > {
match response {
Some ( lsp ::PrepareRenameResponse ::Range ( range ) ) = > {
let text = doc! ( editor ) . text ( ) ;
Ok ( lsp_range_to_range ( text , range , offset_encoding )
. ok_or ( " lsp sent invalid selection range for rename " ) ?
. fragment ( text . slice ( .. ) )
. into ( ) )
}
Some ( lsp ::PrepareRenameResponse ::RangeWithPlaceholder { placeholder , .. } ) = > {
Ok ( placeholder )
2022-02-18 12:58:18 +08:00
}
2023-03-08 10:11:43 +08:00
Some ( lsp ::PrepareRenameResponse ::DefaultBehavior { .. } ) = > {
Ok ( get_prefill_from_word_boundary ( editor ) )
}
None = > Err ( " lsp did not respond to prepare rename request " ) ,
}
}
2022-02-18 12:58:18 +08:00
2022-05-24 00:10:48 +08:00
fn create_rename_prompt (
editor : & Editor ,
prefill : String ,
2024-06-12 22:44:47 +08:00
history_register : Option < char > ,
2024-04-08 08:46:32 +08:00
language_server_id : Option < LanguageServerId > ,
2022-05-24 00:10:48 +08:00
) -> Box < ui ::Prompt > {
2023-03-08 10:11:43 +08:00
let prompt = ui ::Prompt ::new (
" rename-to: " . into ( ) ,
2024-06-12 22:44:47 +08:00
history_register ,
2023-03-08 10:11:43 +08:00
ui ::completers ::none ,
move | cx : & mut compositor ::Context , input : & str , event : PromptEvent | {
if event ! = PromptEvent ::Validate {
return ;
}
let ( view , doc ) = current! ( cx . editor ) ;
2023-03-21 00:44:04 +08:00
let Some ( language_server ) = doc
2022-05-24 00:10:48 +08:00
. language_servers_with_feature ( LanguageServerFeature ::RenameSymbol )
2023-04-06 00:56:19 +08:00
. find ( | ls | language_server_id . map_or ( true , | id | id = = ls . id ( ) ) )
2023-03-21 00:44:04 +08:00
else {
2023-07-27 10:57:19 +08:00
cx . editor
. set_error ( " No configured language server supports symbol renaming " ) ;
2023-03-21 00:44:04 +08:00
return ;
} ;
let offset_encoding = language_server . offset_encoding ( ) ;
let pos = doc . position ( view . id , offset_encoding ) ;
let future = language_server
. rename_symbol ( doc . identifier ( ) , pos , input . to_string ( ) )
. unwrap ( ) ;
match block_on ( future ) {
Ok ( edits ) = > {
2024-01-29 00:34:45 +08:00
let _ = cx . editor . apply_workspace_edit ( offset_encoding , & edits ) ;
2023-03-12 08:01:15 +08:00
}
2023-03-21 00:44:04 +08:00
Err ( err ) = > cx . editor . set_error ( err . to_string ( ) ) ,
2023-03-08 10:11:43 +08:00
}
} ,
)
. with_line ( prefill , editor ) ;
2022-02-18 12:58:18 +08:00
2023-03-08 10:11:43 +08:00
Box ::new ( prompt )
}
2023-03-21 00:44:04 +08:00
let ( view , doc ) = current_ref! ( cx . editor ) ;
2024-06-12 22:44:47 +08:00
let history_register = cx . register ;
2023-03-08 10:11:43 +08:00
2024-01-09 08:55:11 +08:00
if doc
. language_servers_with_feature ( LanguageServerFeature ::RenameSymbol )
. next ( )
. is_none ( )
{
cx . editor
. set_error ( " No configured language server supports symbol renaming " ) ;
return ;
}
2023-03-21 00:44:04 +08:00
let language_server_with_prepare_rename_support = doc
2022-05-24 00:10:48 +08:00
. language_servers_with_feature ( LanguageServerFeature ::RenameSymbol )
2023-03-21 00:44:04 +08:00
. find ( | ls | {
matches! (
ls . capabilities ( ) . rename_provider ,
Some ( lsp ::OneOf ::Right ( lsp ::RenameOptions {
prepare_provider : Some ( true ) ,
..
} ) )
)
2022-05-24 00:10:48 +08:00
} ) ;
2023-03-08 10:11:43 +08:00
2023-03-21 00:44:04 +08:00
if let Some ( language_server ) = language_server_with_prepare_rename_support {
let ls_id = language_server . id ( ) ;
let offset_encoding = language_server . offset_encoding ( ) ;
let pos = doc . position ( view . id , offset_encoding ) ;
let future = language_server
. prepare_rename ( doc . identifier ( ) , pos )
. unwrap ( ) ;
cx . callback (
2023-03-08 10:11:43 +08:00
future ,
move | editor , compositor , response : Option < lsp ::PrepareRenameResponse > | {
let prefill = match get_prefill_from_lsp_response ( editor , offset_encoding , response )
{
Ok ( p ) = > p ,
Err ( e ) = > {
editor . set_error ( e ) ;
2022-11-22 10:52:23 +08:00
return ;
}
} ;
2023-03-08 10:11:43 +08:00
2024-06-12 22:44:47 +08:00
let prompt = create_rename_prompt ( editor , prefill , history_register , Some ( ls_id ) ) ;
2023-03-08 10:11:43 +08:00
compositor . push ( prompt ) ;
} ,
2023-03-21 00:44:04 +08:00
) ;
} else {
let prefill = get_prefill_from_word_boundary ( cx . editor ) ;
2024-06-12 22:44:47 +08:00
let prompt = create_rename_prompt ( cx . editor , prefill , history_register , None ) ;
2023-03-21 00:44:04 +08:00
cx . push_layer ( prompt ) ;
}
2022-02-18 12:58:18 +08:00
}
2022-06-27 19:19:56 +08:00
pub fn select_references_to_symbol_under_cursor ( cx : & mut Context ) {
let ( view , doc ) = current! ( cx . editor ) ;
2023-03-21 00:44:04 +08:00
let language_server =
language_server_with_feature! ( cx . editor , doc , LanguageServerFeature ::DocumentHighlight ) ;
let offset_encoding = language_server . offset_encoding ( ) ;
let pos = doc . position ( view . id , offset_encoding ) ;
let future = language_server
. text_document_document_highlight ( doc . identifier ( ) , pos , None )
. unwrap ( ) ;
2022-06-27 19:19:56 +08:00
cx . callback (
future ,
move | editor , _compositor , response : Option < Vec < lsp ::DocumentHighlight > > | {
let document_highlights = match response {
Some ( highlights ) if ! highlights . is_empty ( ) = > highlights ,
_ = > return ,
} ;
let ( view , doc ) = current! ( editor ) ;
let text = doc . text ( ) ;
2023-05-25 20:01:56 +08:00
let pos = doc . selection ( view . id ) . primary ( ) . cursor ( text . slice ( .. ) ) ;
2022-06-27 19:19:56 +08:00
// We must find the range that contains our primary cursor to prevent our primary cursor to move
let mut primary_index = 0 ;
let ranges = document_highlights
. iter ( )
. filter_map ( | highlight | lsp_range_to_range ( text , highlight . range , offset_encoding ) )
. enumerate ( )
. map ( | ( i , range ) | {
if range . contains ( pos ) {
primary_index = i ;
}
range
} )
. collect ( ) ;
let selection = Selection ::new ( ranges , primary_index ) ;
doc . set_selection ( view . id , selection ) ;
} ,
) ;
}
2023-03-11 10:32:14 +08:00
pub fn compute_inlay_hints_for_all_views ( editor : & mut Editor , jobs : & mut crate ::job ::Jobs ) {
if ! editor . config ( ) . lsp . display_inlay_hints {
return ;
}
for ( view , _ ) in editor . tree . views ( ) {
let doc = match editor . documents . get ( & view . doc ) {
Some ( doc ) = > doc ,
None = > continue ,
} ;
if let Some ( callback ) = compute_inlay_hints_for_view ( view , doc ) {
jobs . callback ( callback ) ;
}
}
}
fn compute_inlay_hints_for_view (
view : & View ,
doc : & Document ,
) -> Option < std ::pin ::Pin < Box < impl Future < Output = Result < crate ::job ::Callback , anyhow ::Error > > > > > {
let view_id = view . id ;
let doc_id = view . doc ;
2023-03-21 00:44:04 +08:00
let language_server = doc
. language_servers_with_feature ( LanguageServerFeature ::InlayHints )
. next ( ) ? ;
2022-05-24 00:10:48 +08:00
let doc_text = doc . text ( ) ;
let len_lines = doc_text . len_lines ( ) ;
// Compute ~3 times the current view height of inlay hints, that way some scrolling
// will not show half the view with hints and half without while still being faster
// than computing all the hints for the full file (which could be dozens of time
// longer than the view is).
let view_height = view . inner_height ( ) ;
let first_visible_line = doc_text . char_to_line ( view . offset . anchor . min ( doc_text . len_chars ( ) ) ) ;
let first_line = first_visible_line . saturating_sub ( view_height ) ;
let last_line = first_visible_line
. saturating_add ( view_height . saturating_mul ( 2 ) )
. min ( len_lines ) ;
let new_doc_inlay_hints_id = DocumentInlayHintsId {
first_line ,
last_line ,
} ;
// Don't recompute the annotations in case nothing has changed about the view
if ! doc . inlay_hints_oudated
& & doc
. inlay_hints ( view_id )
. map_or ( false , | dih | dih . id = = new_doc_inlay_hints_id )
{
return None ;
}
2023-03-11 10:32:14 +08:00
2022-05-24 00:10:48 +08:00
let doc_slice = doc_text . slice ( .. ) ;
let first_char_in_range = doc_slice . line_to_char ( first_line ) ;
let last_char_in_range = doc_slice . line_to_char ( last_line ) ;
2023-03-11 10:32:14 +08:00
2022-05-24 00:10:48 +08:00
let range = helix_lsp ::util ::range_to_lsp_range (
doc_text ,
helix_core ::Range ::new ( first_char_in_range , last_char_in_range ) ,
language_server . offset_encoding ( ) ,
) ;
2023-03-11 10:32:14 +08:00
2022-05-24 00:10:48 +08:00
let offset_encoding = language_server . offset_encoding ( ) ;
2023-03-11 10:32:14 +08:00
let callback = super ::make_job_callback (
2022-05-24 00:10:48 +08:00
language_server . text_document_range_inlay_hints ( doc . identifier ( ) , range , None ) ? ,
2023-03-11 10:32:14 +08:00
move | editor , _compositor , response : Option < Vec < lsp ::InlayHint > > | {
// The config was modified or the window was closed while the request was in flight
if ! editor . config ( ) . lsp . display_inlay_hints | | editor . tree . try_get ( view_id ) . is_none ( ) {
return ;
}
// Add annotations to relevant document, not the current one (it may have changed in between)
let doc = match editor . documents . get_mut ( & doc_id ) {
Some ( doc ) = > doc ,
None = > return ,
} ;
// If we have neither hints nor an LSP, empty the inlay hints since they're now oudated
2022-05-24 00:10:48 +08:00
let mut hints = match response {
Some ( hints ) if ! hints . is_empty ( ) = > hints ,
2023-03-11 10:32:14 +08:00
_ = > {
doc . set_inlay_hints (
view_id ,
DocumentInlayHints ::empty_with_id ( new_doc_inlay_hints_id ) ,
) ;
doc . inlay_hints_oudated = false ;
return ;
}
} ;
// Most language servers will already send them sorted but ensure this is the case to
// avoid errors on our end.
hints . sort_unstable_by_key ( | inlay_hint | inlay_hint . position ) ;
let mut padding_before_inlay_hints = Vec ::new ( ) ;
let mut type_inlay_hints = Vec ::new ( ) ;
let mut parameter_inlay_hints = Vec ::new ( ) ;
let mut other_inlay_hints = Vec ::new ( ) ;
let mut padding_after_inlay_hints = Vec ::new ( ) ;
let doc_text = doc . text ( ) ;
for hint in hints {
let char_idx =
match helix_lsp ::util ::lsp_pos_to_pos ( doc_text , hint . position , offset_encoding )
{
Some ( pos ) = > pos ,
// Skip inlay hints that have no "real" position
None = > continue ,
} ;
let label = match hint . label {
lsp ::InlayHintLabel ::String ( s ) = > s ,
lsp ::InlayHintLabel ::LabelParts ( parts ) = > parts
. into_iter ( )
. map ( | p | p . value )
. collect ::< Vec < _ > > ( )
. join ( " " ) ,
} ;
let inlay_hints_vec = match hint . kind {
Some ( lsp ::InlayHintKind ::TYPE ) = > & mut type_inlay_hints ,
Some ( lsp ::InlayHintKind ::PARAMETER ) = > & mut parameter_inlay_hints ,
// We can't warn on unknown kind here since LSPs are free to set it or not, for
// example Rust Analyzer does not: every kind will be `None`.
_ = > & mut other_inlay_hints ,
} ;
if let Some ( true ) = hint . padding_left {
padding_before_inlay_hints . push ( InlineAnnotation ::new ( char_idx , " " ) ) ;
}
inlay_hints_vec . push ( InlineAnnotation ::new ( char_idx , label ) ) ;
if let Some ( true ) = hint . padding_right {
padding_after_inlay_hints . push ( InlineAnnotation ::new ( char_idx , " " ) ) ;
}
}
doc . set_inlay_hints (
view_id ,
DocumentInlayHints {
id : new_doc_inlay_hints_id ,
2023-11-20 05:34:03 +08:00
type_inlay_hints ,
parameter_inlay_hints ,
other_inlay_hints ,
padding_before_inlay_hints ,
padding_after_inlay_hints ,
2023-03-11 10:32:14 +08:00
} ,
) ;
doc . inlay_hints_oudated = false ;
} ,
) ;
Some ( callback )
}