@ -1,13 +1,17 @@
use crate ::{
find_ root , jsonrpc ,
find_ lsp_workspace , jsonrpc ,
transport ::{ Payload , Transport } ,
Call , Error , OffsetEncoding , Result ,
} ;
use helix_core ::{ ChangeSet, Rope } ;
use helix_core ::{ find_workspace, ChangeSet, Rope } ;
use helix_loader ::{ self , VERSION_AND_GIT_HASH } ;
use lsp ::PositionEncodingKind ;
use lsp ::{
notification ::DidChangeWorkspaceFolders , DidChangeWorkspaceFoldersParams , OneOf ,
PositionEncodingKind , WorkspaceFolder , WorkspaceFoldersChangeEvent ,
} ;
use lsp_types as lsp ;
use parking_lot ::Mutex ;
use serde ::Deserialize ;
use serde_json ::Value ;
use std ::future ::Future ;
@ -26,6 +30,17 @@ use tokio::{
} ,
} ;
fn workspace_for_uri ( uri : lsp ::Url ) -> WorkspaceFolder {
lsp ::WorkspaceFolder {
name : uri
. path_segments ( )
. and_then ( | segments | segments . last ( ) )
. map ( | basename | basename . to_string ( ) )
. unwrap_or_default ( ) ,
uri ,
}
}
#[ derive(Debug) ]
pub struct Client {
id : usize ,
@ -36,11 +51,120 @@ pub struct Client {
config : Option < Value > ,
root_path : std ::path ::PathBuf ,
root_uri : Option < lsp ::Url > ,
workspace_folders : Vec < lsp ::WorkspaceFolder > ,
workspace_folders : Mutex < Vec < lsp ::WorkspaceFolder > > ,
initalize_notify : Arc < Notify > ,
/// workspace folders added while the server is still initalizing
req_timeout : u64 ,
}
impl Client {
pub fn try_add_doc (
self : & Arc < Self > ,
root_markers : & [ String ] ,
manual_roots : & [ PathBuf ] ,
doc_path : Option < & std ::path ::PathBuf > ,
may_support_workspace : bool ,
) -> bool {
let ( workspace , workspace_is_cwd ) = find_workspace ( ) ;
let root = find_lsp_workspace (
doc_path
. and_then ( | x | x . parent ( ) . and_then ( | x | x . to_str ( ) ) )
. unwrap_or ( "." ) ,
root_markers ,
manual_roots ,
& workspace ,
workspace_is_cwd ,
) ;
let root_uri = root
. as_ref ( )
. and_then ( | root | lsp ::Url ::from_file_path ( root ) . ok ( ) ) ;
if self . root_path = = root . unwrap_or ( workspace )
| | root_uri . as_ref ( ) . map_or ( false , | root_uri | {
self . workspace_folders
. lock ( )
. iter ( )
. any ( | workspace | & workspace . uri = = root_uri )
} )
{
// workspace URI is already registered so we can use this client
return true ;
}
// this server definitly doesn't support multiple workspace, no need to check capabilities
if ! may_support_workspace {
return false ;
}
let Some ( capabilities ) = self . capabilities . get ( ) else {
let client = Arc ::clone ( self ) ;
// initalization hasn't finished yet, deal with this new root later
// TODO: In the edgecase that a **new root** is added
// for an LSP that **doesn't support workspace_folders** before initaliation is finished
// the new roots are ignored.
// That particular edgecase would require retroactively spawning new LSP
// clients and therefore also require us to retroactively update the corresponding
// documents LSP client handle. It's doable but a pretty weird edgecase so let's
// wait and see if anyone ever runs into it.
tokio ::spawn ( async move {
client . initalize_notify . notified ( ) . await ;
if let Some ( workspace_folders_caps ) = client
. capabilities ( )
. workspace
. as_ref ( )
. and_then ( | cap | cap . workspace_folders . as_ref ( ) )
. filter ( | cap | cap . supported . unwrap_or ( false ) )
{
client . add_workspace_folder (
root_uri ,
& workspace_folders_caps . change_notifications ,
) ;
}
} ) ;
return true ;
} ;
if let Some ( workspace_folders_caps ) = capabilities
. workspace
. as_ref ( )
. and_then ( | cap | cap . workspace_folders . as_ref ( ) )
. filter ( | cap | cap . supported . unwrap_or ( false ) )
{
self . add_workspace_folder ( root_uri , & workspace_folders_caps . change_notifications ) ;
true
} else {
// the server doesn't support multi workspaces, we need a new client
false
}
}
fn add_workspace_folder (
& self ,
root_uri : Option < lsp ::Url > ,
change_notifications : & Option < OneOf < bool , String > > ,
) {
// root_uri is None just means that there isn't really any LSP workspace
// associated with this file. For servers that support multiple workspaces
// there is just one server so we can always just use that shared instance.
// No need to add a new workspace root here as there is no logical root for this file
// let the server deal with this
let Some ( root_uri ) = root_uri else {
return ;
} ;
// server supports workspace folders, let's add the new root to the list
self . workspace_folders
. lock ( )
. push ( workspace_for_uri ( root_uri . clone ( ) ) ) ;
if & Some ( OneOf ::Left ( false ) ) = = change_notifications {
// server specifically opted out of DidWorkspaceChange notifications
// let's assume the server will request the workspace folders itself
// and that we can therefore reuse the client (but are done now)
return ;
}
tokio ::spawn ( self . did_change_workspace ( vec! [ workspace_for_uri ( root_uri ) ] , Vec ::new ( ) ) ) ;
}
#[ allow(clippy::type_complexity) ]
#[ allow(clippy::too_many_arguments) ]
pub fn start (
@ -76,30 +200,25 @@ impl Client {
let ( server_rx , server_tx , initialize_notify ) =
Transport ::start ( reader , writer , stderr , id ) ;
let root _path = find_root (
let ( workspace , workspace_is_cwd ) = find_workspace ( ) ;
let root = find_lsp_workspace (
doc_path
. and_then ( | x | x . parent ( ) . and_then ( | x | x . to_str ( ) ) )
. unwrap_or ( "." ) ,
root_markers ,
manual_roots ,
& workspace ,
workspace_is_cwd ,
) ;
let root_uri = lsp ::Url ::from_file_path ( root_path . clone ( ) ) . ok ( ) ;
// `root_uri` and `workspace_folder` can be empty in case there is no workspace
// `root_url` can not, use `workspace` as a fallback
let root_path = root . clone ( ) . unwrap_or_else ( | | workspace . clone ( ) ) ;
let root_uri = root . and_then ( | root | lsp ::Url ::from_file_path ( root ) . ok ( ) ) ;
// TODO: support multiple workspace folders
let workspace_folders = root_uri
. clone ( )
. map ( | root | {
vec! [ lsp ::WorkspaceFolder {
name : root
. path_segments ( )
. and_then ( | segments | segments . last ( ) )
. map ( | basename | basename . to_string ( ) )
. unwrap_or_default ( ) ,
uri : root ,
} ]
} )
. map ( | root | vec! [ workspace_for_uri ( root ) ] )
. unwrap_or_default ( ) ;
let client = Self {
@ -110,10 +229,10 @@ impl Client {
capabilities : OnceCell ::new ( ) ,
config ,
req_timeout ,
root_path ,
root_uri ,
workspace_folders ,
workspace_folders : Mutex ::new ( workspace_folders ) ,
initalize_notify : initialize_notify . clone ( ) ,
} ;
Ok ( ( client , server_rx , initialize_notify ) )
@ -169,8 +288,10 @@ impl Client {
self . config . as_ref ( )
}
pub fn workspace_folders ( & self ) -> & [ lsp ::WorkspaceFolder ] {
& self . workspace_folders
pub async fn workspace_folders (
& self ,
) -> parking_lot ::MutexGuard < ' _ , Vec < lsp ::WorkspaceFolder > > {
self . workspace_folders . lock ( )
}
/// Execute a RPC request on the language server.
@ -298,7 +419,7 @@ impl Client {
#[ allow(deprecated) ]
let params = lsp ::InitializeParams {
process_id : Some ( std ::process ::id ( ) ) ,
workspace_folders : Some ( self . workspace_folders . clone( ) ) ,
workspace_folders : Some ( self . workspace_folders . lock( ) . clone( ) ) ,
// root_path is obsolete, but some clients like pyright still use it so we specify both.
// clients will prefer _uri if possible
root_path : self . root_path . to_str ( ) . map ( | path | path . to_owned ( ) ) ,
@ -469,6 +590,16 @@ impl Client {
)
}
pub fn did_change_workspace (
& self ,
added : Vec < WorkspaceFolder > ,
removed : Vec < WorkspaceFolder > ,
) -> impl Future < Output = Result < ( ) > > {
self . notify ::< DidChangeWorkspaceFolders > ( DidChangeWorkspaceFoldersParams {
event : WorkspaceFoldersChangeEvent { added , removed } ,
} )
}
// -------------------------------------------------------------------------------------------
// Text document
// -------------------------------------------------------------------------------------------