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 ;
2023-09-04 03:47:17 +08:00
use tui ::{ text ::Span , 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-04-06 00:20:35 +08:00
use helix_core ::{
2025-02-21 06:08:47 +08:00
diagnostic ::DiagnosticProvider , syntax ::config ::LanguageServerFeature ,
2025-03-21 21:44:40 +08:00
text_annotations ::InlineAnnotation , Selection , Uri ,
2024-04-06 00:20:35 +08:00
} ;
2024-01-17 02:59:48 +08:00
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 ,
2024-02-17 00:19:00 +08:00
ui ::{ self , overlay ::overlaid , FileLocation , Picker , Popup , PromptEvent } ,
2022-02-18 12:58:18 +08:00
} ;
2025-03-21 21:44:40 +08:00
use std ::{ cmp ::Ordering , collections ::HashSet , fmt ::Display , future ::Future , path ::Path } ;
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 ;
}
}
} } ;
}
2025-01-30 06:25:12 +08:00
/// A wrapper around `lsp::Location` that swaps out the LSP URI for `helix_core::Uri` and adds
/// the server's offset encoding.
2024-08-08 23:03:29 +08:00
#[ derive(Debug, Clone, PartialEq, Eq) ]
struct Location {
uri : Uri ,
range : lsp ::Range ,
2025-01-30 06:25:12 +08:00
offset_encoding : OffsetEncoding ,
2024-08-08 23:03:29 +08:00
}
2025-01-30 06:25:12 +08:00
fn lsp_location_to_location (
location : lsp ::Location ,
offset_encoding : OffsetEncoding ,
) -> Option < Location > {
2024-08-08 23:03:29 +08:00
let uri = match location . uri . try_into ( ) {
Ok ( uri ) = > uri ,
Err ( err ) = > {
log ::warn! ( " discarding invalid or unsupported URI: {err} " ) ;
return None ;
}
} ;
Some ( Location {
uri ,
range : location . range ,
2025-01-30 06:25:12 +08:00
offset_encoding ,
2024-08-08 23:03:29 +08:00
} )
}
2022-05-24 00:10:48 +08:00
struct SymbolInformationItem {
2024-08-08 23:03:29 +08:00
location : Location ,
2022-05-24 00:10:48 +08:00
symbol : lsp ::SymbolInformation ,
}
2022-07-02 19:21:27 +08:00
struct DiagnosticStyles {
hint : Style ,
info : Style ,
warning : Style ,
error : Style ,
}
struct PickerDiagnostic {
2024-08-08 23:03:29 +08:00
location : Location ,
2022-07-02 19:21:27 +08:00
diag : lsp ::Diagnostic ,
}
2024-08-08 23:03:29 +08:00
fn location_to_file_location ( location : & Location ) -> Option < FileLocation > {
let path = location . uri . as_path ( ) ? ;
let line = Some ( (
location . range . start . line as usize ,
location . range . end . line as usize ,
) ) ;
2024-04-06 02:49:02 +08:00
Some ( ( path . into ( ) , line ) )
2022-02-18 13:01:50 +08:00
}
2025-01-30 06:25:12 +08:00
fn jump_to_location ( editor : & mut Editor , location : & Location , action : Action ) {
2022-06-06 23:19:01 +08:00
let ( view , doc ) = current! ( editor ) ;
push_jump ( view , doc ) ;
2024-08-08 23:03:29 +08:00
let Some ( path ) = location . uri . as_path ( ) else {
let err = format! ( " unable to convert URI to filepath: {:?} " , location . uri ) ;
editor . set_error ( err ) ;
return ;
2022-06-02 12:58:46 +08:00
} ;
2025-01-30 06:25:12 +08:00
jump_to_position (
editor ,
path ,
location . range ,
location . offset_encoding ,
action ,
) ;
2023-08-28 02:32:17 +08:00
}
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
}
2023-09-04 03:47:17 +08:00
fn display_symbol_kind ( kind : lsp ::SymbolKind ) -> & 'static str {
match kind {
lsp ::SymbolKind ::FILE = > " file " ,
lsp ::SymbolKind ::MODULE = > " module " ,
lsp ::SymbolKind ::NAMESPACE = > " namespace " ,
lsp ::SymbolKind ::PACKAGE = > " package " ,
lsp ::SymbolKind ::CLASS = > " class " ,
lsp ::SymbolKind ::METHOD = > " method " ,
lsp ::SymbolKind ::PROPERTY = > " property " ,
lsp ::SymbolKind ::FIELD = > " field " ,
lsp ::SymbolKind ::CONSTRUCTOR = > " construct " ,
lsp ::SymbolKind ::ENUM = > " enum " ,
lsp ::SymbolKind ::INTERFACE = > " interface " ,
lsp ::SymbolKind ::FUNCTION = > " function " ,
lsp ::SymbolKind ::VARIABLE = > " variable " ,
lsp ::SymbolKind ::CONSTANT = > " constant " ,
lsp ::SymbolKind ::STRING = > " string " ,
lsp ::SymbolKind ::NUMBER = > " number " ,
lsp ::SymbolKind ::BOOLEAN = > " boolean " ,
lsp ::SymbolKind ::ARRAY = > " array " ,
lsp ::SymbolKind ::OBJECT = > " object " ,
lsp ::SymbolKind ::KEY = > " key " ,
lsp ::SymbolKind ::NULL = > " null " ,
lsp ::SymbolKind ::ENUM_MEMBER = > " enummem " ,
lsp ::SymbolKind ::STRUCT = > " struct " ,
lsp ::SymbolKind ::EVENT = > " event " ,
lsp ::SymbolKind ::OPERATOR = > " operator " ,
lsp ::SymbolKind ::TYPE_PARAMETER = > " typeparam " ,
_ = > {
log ::warn! ( " Unknown symbol kind: {:?} " , kind ) ;
" "
}
}
}
2022-07-05 21:01:25 +08:00
#[ derive(Copy, Clone, PartialEq) ]
enum DiagnosticsFormat {
ShowSourcePath ,
HideSourcePath ,
}
2023-09-04 03:47:17 +08:00
type DiagnosticsPicker = Picker < PickerDiagnostic , DiagnosticStyles > ;
2024-02-16 23:55:02 +08:00
2022-06-30 17:16:18 +08:00
fn diag_picker (
cx : & Context ,
2025-03-21 21:44:40 +08:00
diagnostics : impl IntoIterator < Item = ( Uri , Vec < ( lsp ::Diagnostic , DiagnosticProvider ) > ) > ,
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 ( ) ;
2024-04-06 00:20:35 +08:00
for ( uri , diags ) in diagnostics {
2022-06-30 17:16:18 +08:00
flat_diag . reserve ( diags . len ( ) ) ;
2023-03-17 22:30:49 +08:00
2025-03-21 21:44:40 +08:00
for ( diag , provider ) in diags {
if let Some ( ls ) = provider
. language_server_id ( )
. and_then ( | id | cx . editor . language_server_by_id ( id ) )
{
2023-03-17 22:30:49 +08:00
flat_diag . push ( PickerDiagnostic {
2024-08-08 23:03:29 +08:00
location : Location {
uri : uri . clone ( ) ,
range : diag . range ,
2025-01-30 06:25:12 +08:00
offset_encoding : ls . offset_encoding ( ) ,
2024-08-08 23:03:29 +08:00
} ,
2023-03-17 22:30:49 +08:00
diag ,
} ) ;
}
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
2023-09-04 03:47:17 +08:00
let mut columns = vec! [
ui ::PickerColumn ::new (
" severity " ,
| item : & PickerDiagnostic , styles : & DiagnosticStyles | {
match item . diag . severity {
Some ( DiagnosticSeverity ::HINT ) = > Span ::styled ( " HINT " , styles . hint ) ,
Some ( DiagnosticSeverity ::INFORMATION ) = > Span ::styled ( " INFO " , styles . info ) ,
Some ( DiagnosticSeverity ::WARNING ) = > Span ::styled ( " WARN " , styles . warning ) ,
Some ( DiagnosticSeverity ::ERROR ) = > Span ::styled ( " ERROR " , styles . error ) ,
_ = > Span ::raw ( " " ) ,
}
. into ( )
} ,
) ,
ui ::PickerColumn ::new ( " code " , | item : & PickerDiagnostic , _ | {
match item . diag . code . as_ref ( ) {
Some ( NumberOrString ::Number ( n ) ) = > n . to_string ( ) . into ( ) ,
Some ( NumberOrString ::String ( s ) ) = > s . as_str ( ) . into ( ) ,
None = > " " . into ( ) ,
}
} ) ,
ui ::PickerColumn ::new ( " message " , | item : & PickerDiagnostic , _ | {
item . diag . message . as_str ( ) . into ( )
} ) ,
] ;
let mut primary_column = 2 ; // message
if format = = DiagnosticsFormat ::ShowSourcePath {
columns . insert (
// between message code and message
2 ,
ui ::PickerColumn ::new ( " path " , | item : & PickerDiagnostic , _ | {
2024-08-08 23:03:29 +08:00
if let Some ( path ) = item . location . uri . as_path ( ) {
2024-04-06 00:20:35 +08:00
path ::get_truncated_path ( path )
. to_string_lossy ( )
. to_string ( )
. into ( )
} else {
Default ::default ( )
}
2023-09-04 03:47:17 +08:00
} ) ,
) ;
primary_column + = 1 ;
}
2023-06-19 01:27:11 +08:00
Picker ::new (
2024-02-16 23:55:02 +08:00
columns ,
2023-09-04 03:47:17 +08:00
primary_column ,
2022-06-30 17:16:18 +08:00
flat_diag ,
2023-09-04 03:47:17 +08:00
styles ,
2024-08-08 23:03:29 +08:00
move | cx , diag , action | {
2025-01-30 06:25:12 +08:00
jump_to_location ( cx . editor , & diag . location , action ) ;
2024-04-05 09:17:06 +08:00
let ( view , doc ) = current! ( cx . editor ) ;
view . diagnostics_handler
. immediately_show_diagnostic ( doc , view . id ) ;
2022-06-30 17:16:18 +08:00
} ,
)
2024-08-08 23:03:29 +08:00
. with_preview ( move | _editor , diag | location_to_file_location ( & diag . location ) )
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 ,
2024-04-06 02:49:02 +08:00
uri : & Uri ,
2022-02-18 12:58:18 +08:00
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 ,
} ,
2024-08-08 23:03:29 +08:00
location : Location {
uri : uri . clone ( ) ,
range : symbol . selection_range ,
2025-01-30 06:25:12 +08:00
offset_encoding ,
2024-08-08 23:03:29 +08:00
} ,
2022-02-18 12:58:18 +08:00
} ) ;
for child in symbol . children . into_iter ( ) . flatten ( ) {
2024-04-06 02:49:02 +08:00
nested_to_flat ( list , file , uri , 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 ( ) ;
2024-04-06 02:49:02 +08:00
let doc_uri = doc
. uri ( )
. expect ( " docs with active language servers must be backed by paths " ) ;
2023-03-21 00:44:04 +08:00
async move {
2025-03-23 02:18:17 +08:00
let symbols = match request . await ? {
2023-03-21 00:44:04 +08:00
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 {
2024-08-08 23:03:29 +08:00
location : Location {
uri : doc_uri . clone ( ) ,
range : symbol . location . range ,
2025-01-30 06:25:12 +08:00
offset_encoding ,
2024-08-08 23:03:29 +08:00
} ,
2023-03-21 00:44:04 +08:00
symbol ,
} )
. collect ( ) ,
lsp ::DocumentSymbolResponse ::Nested ( symbols ) = > {
let mut flat_symbols = Vec ::new ( ) ;
for symbol in symbols {
2024-04-06 02:49:02 +08:00
nested_to_flat (
& mut flat_symbols ,
& doc_id ,
& doc_uri ,
symbol ,
offset_encoding ,
)
2023-03-21 00:44:04 +08:00
}
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
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 ( ) ;
2025-03-21 22:26:18 +08:00
while let Some ( response ) = futures . next ( ) . await {
match response {
Ok ( mut items ) = > symbols . append ( & mut items ) ,
Err ( err ) = > log ::error! ( " Error requesting document symbols: {err} " ) ,
}
2022-05-24 00:10:48 +08:00
}
2023-03-20 04:22:29 +08:00
let call = move | _editor : & mut Editor , compositor : & mut Compositor | {
2024-04-24 21:22:22 +08:00
let columns = [
2024-04-01 23:45:49 +08:00
ui ::PickerColumn ::new ( " kind " , | item : & SymbolInformationItem , _ | {
display_symbol_kind ( item . symbol . kind ) . into ( )
} ) ,
// Some symbols in the document symbol picker may have a URI that isn't
// the current file. It should be rare though, so we concatenate that
// URI in with the symbol name in this picker.
ui ::PickerColumn ::new ( " name " , | item : & SymbolInformationItem , _ | {
item . symbol . name . as_str ( ) . into ( )
} ) ,
2025-02-27 07:28:34 +08:00
ui ::PickerColumn ::new ( " container " , | item : & SymbolInformationItem , _ | {
item . symbol
. container_name
. as_deref ( )
. unwrap_or_default ( )
. into ( )
} ) ,
2024-04-01 23:45:49 +08:00
] ;
let picker = Picker ::new (
columns ,
1 , // name column
symbols ,
( ) ,
move | cx , item , action | {
2025-01-30 06:25:12 +08:00
jump_to_location ( cx . editor , & item . location , action ) ;
2024-04-01 23:45:49 +08:00
} ,
)
2024-08-08 23:03:29 +08:00
. with_preview ( move | _editor , item | location_to_file_location ( & item . location ) )
2024-04-01 23:45:49 +08:00
. truncate_start ( false ) ;
2022-05-24 00:10:48 +08:00
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 ) {
2024-02-17 00:19:00 +08:00
use crate ::ui ::picker ::Injector ;
2022-02-18 12:58:18 +08:00
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
2024-02-17 00:19:00 +08:00
let get_symbols = | pattern : & str , editor : & mut Editor , _data , injector : & Injector < _ , _ > | {
2022-05-24 00:10:48 +08:00
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 | {
2024-02-17 00:19:00 +08:00
let request = language_server
. workspace_symbols ( pattern . to_string ( ) )
. unwrap ( ) ;
2023-03-21 00:44:04 +08:00
let offset_encoding = language_server . offset_encoding ( ) ;
async move {
2025-03-23 02:18:17 +08:00
let symbols = request
. await ?
. and_then ( | resp | match resp {
lsp ::WorkspaceSymbolResponse ::Flat ( symbols ) = > Some ( symbols ) ,
lsp ::WorkspaceSymbolResponse ::Nested ( _ ) = > None ,
} )
. unwrap_or_default ( ) ;
let response : Vec < _ > = symbols
. into_iter ( )
. filter_map ( | symbol | {
let uri = match Uri ::try_from ( & symbol . location . uri ) {
Ok ( uri ) = > uri ,
Err ( err ) = > {
log ::warn! ( " discarding symbol with invalid URI: {err} " ) ;
return None ;
}
} ;
Some ( SymbolInformationItem {
location : Location {
uri ,
range : symbol . location . range ,
offset_encoding ,
} ,
symbol ,
2023-03-21 00:44:04 +08:00
} )
2025-03-23 02:18:17 +08:00
} )
. collect ( ) ;
2023-03-21 00:44:04 +08:00
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
2024-02-17 00:19:00 +08:00
let injector = injector . clone ( ) ;
2022-05-24 00:10:48 +08:00
async move {
2025-03-21 22:26:18 +08:00
while let Some ( response ) = futures . next ( ) . await {
match response {
Ok ( items ) = > {
for item in items {
injector . push ( item ) ? ;
}
}
Err ( err ) = > log ::error! ( " Error requesting workspace symbols: {err} " ) ,
2024-02-17 00:19:00 +08:00
}
2022-05-24 00:10:48 +08:00
}
2024-02-17 00:19:00 +08:00
Ok ( ( ) )
2022-05-24 00:10:48 +08:00
}
. boxed ( )
} ;
2024-04-24 21:22:22 +08:00
let columns = [
2024-02-17 00:19:00 +08:00
ui ::PickerColumn ::new ( " kind " , | item : & SymbolInformationItem , _ | {
display_symbol_kind ( item . symbol . kind ) . into ( )
} ) ,
ui ::PickerColumn ::new ( " name " , | item : & SymbolInformationItem , _ | {
item . symbol . name . as_str ( ) . into ( )
} )
. without_filtering ( ) ,
2025-02-27 07:28:34 +08:00
ui ::PickerColumn ::new ( " container " , | item : & SymbolInformationItem , _ | {
item . symbol
. container_name
. as_deref ( )
. unwrap_or_default ( )
. into ( )
} ) ,
2024-02-17 00:19:00 +08:00
ui ::PickerColumn ::new ( " path " , | item : & SymbolInformationItem , _ | {
2024-08-08 23:03:29 +08:00
if let Some ( path ) = item . location . uri . as_path ( ) {
2024-04-06 02:49:02 +08:00
path ::get_relative_path ( path )
. to_string_lossy ( )
. to_string ( )
. into ( )
2024-04-06 00:20:35 +08:00
} else {
item . symbol . location . uri . to_string ( ) . into ( )
2024-02-17 00:19:00 +08:00
}
} ) ,
] ;
2022-07-20 00:45:03 +08:00
2024-02-17 00:19:00 +08:00
let picker = Picker ::new (
columns ,
1 , // name column
2024-04-24 02:34:43 +08:00
[ ] ,
2024-02-17 00:19:00 +08:00
( ) ,
move | cx , item , action | {
2025-01-30 06:25:12 +08:00
jump_to_location ( cx . editor , & item . location , action ) ;
2024-02-17 00:19:00 +08:00
} ,
)
2024-08-08 23:03:29 +08:00
. with_preview ( | _editor , item | location_to_file_location ( & item . location ) )
2024-02-17 00:19:00 +08:00
. with_dynamic_query ( get_symbols , None )
. truncate_start ( false ) ;
2022-05-24 00:10:48 +08:00
2024-02-17 00:19:00 +08:00
cx . push_layer ( Box ::new ( overlaid ( picker ) ) ) ;
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 ) ;
2024-04-06 00:20:35 +08:00
if let Some ( uri ) = doc . uri ( ) {
let diagnostics = cx . editor . diagnostics . get ( & uri ) . cloned ( ) . unwrap_or_default ( ) ;
2025-03-21 21:44:40 +08:00
let picker = diag_picker ( cx , [ ( uri , diagnostics ) ] , DiagnosticsFormat ::HideSourcePath ) ;
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 {
2025-03-23 02:18:17 +08:00
let Some ( mut actions ) = request . await ? else {
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 ( ) ;
2025-03-21 22:10:24 +08:00
while let Some ( output ) = futures . next ( ) . await {
match output {
Ok ( mut lsp_items ) = > actions . append ( & mut lsp_items ) ,
Err ( err ) = > log ::error! ( " while gathering code actions: {err} " ) ,
}
2022-05-24 00:10:48 +08:00
}
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 ) ;
2025-03-17 01:50:56 +08:00
editor . execute_lsp_command ( command . clone ( ) , action . language_server_id ) ;
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 ( ) {
2025-03-23 02:18:17 +08:00
if let Some ( future ) = language_server . resolve_code_action ( code_action ) {
if let Ok ( code_action ) = helix_lsp ::block_on ( future ) {
resolved_code_action = Some ( code_action ) ;
2023-07-22 03:50:08 +08:00
}
}
}
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 {
2025-03-17 01:50:56 +08:00
editor . execute_lsp_command ( command . clone ( ) , action . language_server_id ) ;
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
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,
}
2024-08-01 05:39:46 +08:00
impl Display for ApplyEditErrorKind {
fn fmt ( & self , f : & mut std ::fmt ::Formatter < '_ > ) -> std ::fmt ::Result {
2023-03-12 08:01:15 +08:00
match self {
2024-08-01 05:39:46 +08:00
ApplyEditErrorKind ::DocumentChanged = > f . write_str ( " document has changed " ) ,
ApplyEditErrorKind ::FileNotFound = > f . write_str ( " file not found " ) ,
ApplyEditErrorKind ::UnknownURISchema = > f . write_str ( " URI schema not supported " ) ,
ApplyEditErrorKind ::IoError ( err ) = > f . write_str ( & format! ( " {err} " ) ) ,
2023-03-12 08:01:15 +08:00
}
}
}
2024-01-25 13:11:12 +08:00
/// Precondition: `locations` should be non-empty.
2025-01-30 06:25:12 +08:00
fn goto_impl ( editor : & mut Editor , compositor : & mut Compositor , locations : Vec < Location > ) {
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 ] = > {
2025-01-30 06:25:12 +08:00
jump_to_location ( editor , location , 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-04-24 21:22:22 +08:00
let columns = [ ui ::PickerColumn ::new (
2023-09-04 03:47:17 +08:00
" location " ,
2024-08-08 23:03:29 +08:00
| item : & Location , cwdir : & std ::path ::PathBuf | {
let path = if let Some ( path ) = item . uri . as_path ( ) {
path . strip_prefix ( cwdir ) . unwrap_or ( path ) . to_string_lossy ( )
2023-09-04 03:47:17 +08:00
} else {
2024-08-08 23:03:29 +08:00
item . uri . to_string ( ) . into ( )
} ;
2023-09-04 03:47:17 +08:00
2024-08-08 23:03:29 +08:00
format! ( " {path} : {} " , item . range . start . line + 1 ) . into ( )
2023-09-04 03:47:17 +08:00
} ,
) ] ;
2025-01-30 06:25:12 +08:00
let picker = Picker ::new ( columns , 0 , locations , cwdir , | cx , location , action | {
jump_to_location ( cx . editor , location , action )
2023-06-19 01:23:15 +08:00
} )
2025-01-30 06:25:12 +08:00
. with_preview ( | _editor , location | 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
}
}
}
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 > ,
2025-03-23 02:18:17 +08:00
F : Future < Output = helix_lsp ::Result < Option < lsp ::GotoDefinitionResponse > > > + 'static + Send ,
2023-03-19 06:13:58 +08:00
{
2025-01-30 22:54:25 +08:00
let ( view , doc ) = current_ref! ( cx . editor ) ;
let mut futures : FuturesOrdered < _ > = doc
. language_servers_with_feature ( feature )
. map ( | language_server | {
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 ( ) ;
2025-03-23 02:18:17 +08:00
async move { anyhow ::Ok ( ( future . await ? , offset_encoding ) ) }
2025-01-30 22:54:25 +08:00
} )
. collect ( ) ;
2023-03-21 00:44:04 +08:00
2025-01-30 22:54:25 +08:00
cx . jobs . callback ( async move {
let mut locations = Vec ::new ( ) ;
2025-03-21 22:26:18 +08:00
while let Some ( response ) = futures . next ( ) . await {
2025-01-30 22:54:25 +08:00
match response {
2025-03-21 22:26:18 +08:00
Ok ( ( response , offset_encoding ) ) = > match response {
Some ( lsp ::GotoDefinitionResponse ::Scalar ( lsp_location ) ) = > {
locations . extend ( lsp_location_to_location ( lsp_location , offset_encoding ) ) ;
}
Some ( lsp ::GotoDefinitionResponse ::Array ( lsp_locations ) ) = > {
locations . extend ( lsp_locations . into_iter ( ) . flat_map ( | location | {
2025-01-30 22:54:25 +08:00
lsp_location_to_location ( location , offset_encoding )
2025-03-21 22:26:18 +08:00
} ) ) ;
}
Some ( lsp ::GotoDefinitionResponse ::Link ( lsp_locations ) ) = > {
locations . extend (
lsp_locations
. into_iter ( )
. map ( | location_link | {
lsp ::Location ::new (
location_link . target_uri ,
location_link . target_range ,
)
} )
. flat_map ( | location | {
lsp_location_to_location ( location , offset_encoding )
} ) ,
) ;
}
None = > ( ) ,
} ,
Err ( err ) = > log ::error! ( " Error requesting locations: {err} " ) ,
2025-01-30 22:54:25 +08:00
}
}
let call = move | editor : & mut Editor , compositor : & mut Compositor | {
if locations . is_empty ( ) {
2024-01-25 13:11:12 +08:00
editor . set_error ( " No definition found. " ) ;
} else {
2025-01-30 22:54:25 +08:00
goto_impl ( editor , compositor , locations ) ;
2024-01-25 13:11:12 +08:00
}
2025-01-30 22:54:25 +08:00
} ;
Ok ( Callback ::EditorCompositor ( Box ::new ( call ) ) )
} ) ;
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 ( ) ;
2025-01-30 23:04:01 +08:00
let ( view , doc ) = current_ref! ( cx . editor ) ;
2023-03-21 00:44:04 +08:00
2025-01-30 23:04:01 +08:00
let mut futures : FuturesOrdered < _ > = doc
. language_servers_with_feature ( LanguageServerFeature ::GotoReference )
. map ( | language_server | {
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 ( ) ;
2025-03-23 02:18:17 +08:00
async move { anyhow ::Ok ( ( future . await ? , offset_encoding ) ) }
2025-01-30 23:04:01 +08:00
} )
. collect ( ) ;
cx . jobs . callback ( async move {
let mut locations = Vec ::new ( ) ;
2025-03-21 22:26:18 +08:00
while let Some ( response ) = futures . next ( ) . await {
match response {
Ok ( ( lsp_locations , offset_encoding ) ) = > locations . extend (
lsp_locations
. into_iter ( )
. flatten ( )
. flat_map ( | location | lsp_location_to_location ( location , offset_encoding ) ) ,
) ,
Err ( err ) = > log ::error! ( " Error requesting references: {err} " ) ,
}
2025-01-30 23:04:01 +08:00
}
let call = move | editor : & mut Editor , compositor : & mut Compositor | {
if locations . is_empty ( ) {
2024-01-25 13:11:12 +08:00
editor . set_error ( " No references found. " ) ;
} else {
2025-01-30 23:04:01 +08:00
goto_impl ( editor , compositor , locations ) ;
2024-01-25 13:11:12 +08:00
}
2025-01-30 23:04:01 +08:00
} ;
Ok ( Callback ::EditorCompositor ( Box ::new ( call ) ) )
} ) ;
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 ) {
2025-01-27 01:24:50 +08:00
use ui ::lsp ::hover ::Hover ;
2022-02-18 12:58:18 +08:00
let ( view , doc ) = current! ( cx . editor ) ;
2025-01-27 01:24:50 +08:00
if doc
. language_servers_with_feature ( LanguageServerFeature ::Hover )
. count ( )
= = 0
{
cx . editor
. set_error ( " No configured language server supports hover " ) ;
return ;
}
2022-02-18 12:58:18 +08:00
2025-01-27 01:24:50 +08:00
let mut seen_language_servers = HashSet ::new ( ) ;
let mut futures : FuturesOrdered < _ > = doc
. language_servers_with_feature ( LanguageServerFeature ::Hover )
. filter ( | ls | seen_language_servers . insert ( ls . id ( ) ) )
. map ( | language_server | {
let server_name = language_server . name ( ) . to_string ( ) ;
// TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
let pos = doc . position ( view . id , language_server . offset_encoding ( ) ) ;
let request = language_server
. text_document_hover ( doc . identifier ( ) , pos , None )
. unwrap ( ) ;
2022-02-18 12:58:18 +08:00
2025-03-05 01:01:06 +08:00
async move { anyhow ::Ok ( ( server_name , request . await ? ) ) }
2025-01-27 01:24:50 +08:00
} )
. collect ( ) ;
2022-02-18 12:58:18 +08:00
2025-01-27 01:24:50 +08:00
cx . jobs . callback ( async move {
let mut hovers : Vec < ( String , lsp ::Hover ) > = Vec ::new ( ) ;
2022-02-18 12:58:18 +08:00
2025-03-21 22:26:18 +08:00
while let Some ( response ) = futures . next ( ) . await {
match response {
Ok ( ( server_name , Some ( hover ) ) ) = > hovers . push ( ( server_name , hover ) ) ,
Ok ( _ ) = > ( ) ,
Err ( err ) = > log ::error! ( " Error requesting hover: {err} " ) ,
2025-01-27 01:24:50 +08:00
}
}
2022-02-18 12:58:18 +08:00
2025-01-27 01:24:50 +08:00
let call = move | editor : & mut Editor , compositor : & mut Compositor | {
if hovers . is_empty ( ) {
editor . set_status ( " No hover results available. " ) ;
return ;
2022-02-18 12:58:18 +08:00
}
2025-01-27 01:24:50 +08:00
// create new popup
let contents = Hover ::new ( hovers , editor . syn_loader . clone ( ) ) ;
let popup = Popup ::new ( Hover ::ID , contents ) . auto_close ( true ) ;
compositor . replace_or_push ( Hover ::ID , popup ) ;
} ;
Ok ( Callback ::EditorCompositor ( Box ::new ( call ) ) )
} ) ;
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 ) = > {
2025-03-23 02:18:17 +08:00
let _ = cx
. editor
. apply_workspace_edit ( offset_encoding , & edits . unwrap_or_default ( ) ) ;
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 ( ) ;
2024-07-24 01:54:00 +08:00
let first_visible_line =
doc_text . char_to_line ( doc . view_offset ( view_id ) . anchor . min ( doc_text . len_chars ( ) ) ) ;
2022-05-24 00:10:48 +08:00
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 )
2025-01-10 01:02:21 +08:00
. is_some_and ( | dih | dih . id = = new_doc_inlay_hints_id )
2022-05-24 00:10:48 +08:00
{
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.
2024-07-29 05:22:28 +08:00
hints . sort_by_key ( | inlay_hint | inlay_hint . position ) ;
2023-03-11 10:32:14 +08:00
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 ( ) ;
2025-06-14 00:09:21 +08:00
let inlay_hints_length_limit = doc . config . load ( ) . lsp . inlay_hints_length_limit ;
2023-03-11 10:32:14 +08:00
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 ,
} ;
2025-06-14 00:09:21 +08:00
let mut label = match hint . label {
2023-03-11 10:32:14 +08:00
lsp ::InlayHintLabel ::String ( s ) = > s ,
lsp ::InlayHintLabel ::LabelParts ( parts ) = > parts
. into_iter ( )
. map ( | p | p . value )
. collect ::< Vec < _ > > ( )
. join ( " " ) ,
} ;
2025-06-14 00:09:21 +08:00
// Truncate the hint if too long
if let Some ( limit ) = inlay_hints_length_limit {
// Limit on displayed width
use helix_core ::unicode ::{
segmentation ::UnicodeSegmentation , width ::UnicodeWidthStr ,
} ;
let width = label . width ( ) ;
let limit = limit . get ( ) . into ( ) ;
if width > limit {
let mut floor_boundary = 0 ;
let mut acc = 0 ;
for ( i , grapheme_cluster ) in label . grapheme_indices ( true ) {
acc + = grapheme_cluster . width ( ) ;
if acc > limit {
floor_boundary = i ;
break ;
}
}
label . truncate ( floor_boundary ) ;
label . push ( '…' ) ;
}
}
2023-03-11 10:32:14 +08:00
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 )
}