Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-lsp/src/lib.rs')
-rw-r--r--helix-lsp/src/lib.rs588
1 files changed, 237 insertions, 351 deletions
diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs
index 259980dd..53b2712d 100644
--- a/helix-lsp/src/lib.rs
+++ b/helix-lsp/src/lib.rs
@@ -1,22 +1,20 @@
mod client;
pub mod file_event;
-mod file_operations;
pub mod jsonrpc;
+pub mod snippet;
mod transport;
-use arc_swap::ArcSwap;
pub use client::Client;
pub use futures_executor::block_on;
-pub use helix_lsp_types as lsp;
pub use jsonrpc::Call;
pub use lsp::{Position, Url};
+pub use lsp_types as lsp;
use futures_util::stream::select_all::SelectAll;
-use helix_core::syntax::config::{
+use helix_core::syntax::{
LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures,
};
use helix_stdx::path;
-use slotmap::SlotMap;
use tokio::sync::mpsc::UnboundedReceiver;
use std::{
@@ -28,16 +26,15 @@ use std::{
use thiserror::Error;
use tokio_stream::wrappers::UnboundedReceiverStream;
-pub type Result<T, E = Error> = core::result::Result<T, E>;
+pub type Result<T> = core::result::Result<T, Error>;
pub type LanguageServerName = String;
-pub use helix_core::diagnostic::LanguageServerId;
#[derive(Error, Debug)]
pub enum Error {
#[error("protocol error: {0}")]
Rpc(#[from] jsonrpc::Error),
#[error("failed to parse: {0}")]
- Parse(Box<dyn std::error::Error + Send + Sync>),
+ Parse(#[from] serde_json::Error),
#[error("IO Error: {0}")]
IO(#[from] std::io::Error),
#[error("request {0} timed out")]
@@ -52,18 +49,6 @@ pub enum Error {
Other(#[from] anyhow::Error),
}
-impl From<serde_json::Error> for Error {
- fn from(value: serde_json::Error) -> Self {
- Self::Parse(Box::new(value))
- }
-}
-
-impl From<sonic_rs::Error> for Error {
- fn from(value: sonic_rs::Error) -> Self {
- Self::Parse(Box::new(value))
- }
-}
-
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OffsetEncoding {
/// UTF-8 code units aka bytes
@@ -78,8 +63,7 @@ pub enum OffsetEncoding {
pub mod util {
use super::*;
use helix_core::line_ending::{line_end_byte_index, line_end_char_index};
- use helix_core::snippets::{RenderedSnippet, Snippet, SnippetRenderCtx};
- use helix_core::{chars, RopeSlice};
+ use helix_core::{chars, RopeSlice, SmallVec};
use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
/// Converts a diagnostic in the document to [`lsp::Diagnostic`].
@@ -296,6 +280,7 @@ pub mod util {
if replace_mode {
end += text
.chars_at(cursor)
+ .skip(1)
.take_while(|ch| chars::char_is_word(*ch))
.count();
}
@@ -367,17 +352,25 @@ pub mod util {
transaction.with_selection(selection)
}
- /// Creates a [Transaction] from the [Snippet] in a completion response.
+ /// Creates a [Transaction] from the [snippet::Snippet] in a completion response.
/// The transaction applies the edit to all cursors.
+ #[allow(clippy::too_many_arguments)]
pub fn generate_transaction_from_snippet(
doc: &Rope,
selection: &Selection,
edit_offset: Option<(i128, i128)>,
replace_mode: bool,
- snippet: Snippet,
- cx: &mut SnippetRenderCtx,
- ) -> (Transaction, RenderedSnippet) {
+ snippet: snippet::Snippet,
+ line_ending: &str,
+ include_placeholder: bool,
+ tab_width: usize,
+ indent_width: usize,
+ ) -> Transaction {
let text = doc.slice(..);
+
+ let mut off = 0i128;
+ let mut mapped_doc = doc.clone();
+ let mut selection_tabstops: SmallVec<[_; 1]> = SmallVec::new();
let (removed_start, removed_end) = completion_range(
text,
edit_offset,
@@ -386,7 +379,8 @@ pub mod util {
)
.expect("transaction must be valid for primary selection");
let removed_text = text.slice(removed_start..removed_end);
- let (transaction, mapped_selection, snippet) = snippet.render(
+
+ let (transaction, mut selection) = Transaction::change_by_selection_ignore_overlapping(
doc,
selection,
|range| {
@@ -395,15 +389,108 @@ pub mod util {
.filter(|(start, end)| text.slice(start..end) == removed_text)
.unwrap_or_else(|| find_completion_range(text, replace_mode, cursor))
},
- cx,
+ |replacement_start, replacement_end| {
+ let mapped_replacement_start = (replacement_start as i128 + off) as usize;
+ let mapped_replacement_end = (replacement_end as i128 + off) as usize;
+
+ let line_idx = mapped_doc.char_to_line(mapped_replacement_start);
+ let indent_level = helix_core::indent::indent_level_for_line(
+ mapped_doc.line(line_idx),
+ tab_width,
+ indent_width,
+ ) * indent_width;
+
+ let newline_with_offset = format!(
+ "{line_ending}{blank:indent_level$}",
+ line_ending = line_ending,
+ blank = ""
+ );
+
+ let (replacement, tabstops) =
+ snippet::render(&snippet, &newline_with_offset, include_placeholder);
+ selection_tabstops.push((mapped_replacement_start, tabstops));
+ mapped_doc.remove(mapped_replacement_start..mapped_replacement_end);
+ mapped_doc.insert(mapped_replacement_start, &replacement);
+ off +=
+ replacement_start as i128 - replacement_end as i128 + replacement.len() as i128;
+
+ Some(replacement)
+ },
);
- let transaction = transaction.with_selection(snippet.first_selection(
- // we keep the direction of the old primary selection in case it changed during mapping
- // but use the primary idx from the mapped selection in case ranges had to be merged
- selection.primary().direction(),
- mapped_selection.primary_index(),
- ));
- (transaction, snippet)
+
+ let changes = transaction.changes();
+ if changes.is_empty() {
+ return transaction;
+ }
+
+ // Don't normalize to avoid merging/reording selections which would
+ // break the association between tabstops and selections. Most ranges
+ // will be replaced by tabstops anyways and the final selection will be
+ // normalized anyways
+ selection = selection.map_no_normalize(changes);
+ let mut mapped_selection = SmallVec::with_capacity(selection.len());
+ let mut mapped_primary_idx = 0;
+ let primary_range = selection.primary();
+ for (range, (tabstop_anchor, tabstops)) in selection.into_iter().zip(selection_tabstops) {
+ if range == primary_range {
+ mapped_primary_idx = mapped_selection.len()
+ }
+
+ let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty());
+ let Some(tabstops) = tabstops else {
+ // no tabstop normal mapping
+ mapped_selection.push(range);
+ continue;
+ };
+
+ // expand the selection to cover the tabstop to retain the helix selection semantic
+ // the tabstop closest to the range simply replaces `head` while anchor remains in place
+ // the remaining tabstops receive their own single-width cursor
+ if range.head < range.anchor {
+ let last_idx = tabstops.len() - 1;
+ let last_tabstop = tabstop_anchor + tabstops[last_idx].0;
+
+ // if selection is forward but was moved to the right it is
+ // contained entirely in the replacement text, just do a point
+ // selection (fallback below)
+ if range.anchor > last_tabstop {
+ let range = Range::new(range.anchor, last_tabstop);
+ mapped_selection.push(range);
+ let rem_tabstops = tabstops[..last_idx]
+ .iter()
+ .map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
+ mapped_selection.extend(rem_tabstops);
+ continue;
+ }
+ } else {
+ let first_tabstop = tabstop_anchor + tabstops[0].0;
+
+ // if selection is forward but was moved to the right it is
+ // contained entirely in the replacement text, just do a point
+ // selection (fallback below)
+ if range.anchor < first_tabstop {
+ // we can't properly compute the the next grapheme
+ // here because the transaction hasn't been applied yet
+ // that is not a problem because the range gets grapheme aligned anyway
+ // tough so just adding one will always cause head to be grapheme
+ // aligned correctly when applied to the document
+ let range = Range::new(range.anchor, first_tabstop + 1);
+ mapped_selection.push(range);
+ let rem_tabstops = tabstops[1..]
+ .iter()
+ .map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
+ mapped_selection.extend(rem_tabstops);
+ continue;
+ }
+ };
+
+ let tabstops = tabstops
+ .iter()
+ .map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
+ mapped_selection.extend(tabstops);
+ }
+
+ transaction.with_selection(Selection::new(mapped_selection, mapped_primary_idx))
}
pub fn generate_transaction_from_edits(
@@ -413,7 +500,7 @@ pub mod util {
) -> Transaction {
// Sort edits by start range, since some LSPs (Omnisharp) send them
// in reverse order.
- edits.sort_by_key(|edit| edit.range.start);
+ edits.sort_unstable_by_key(|edit| edit.range.start);
// Generate a diff if the edit is a full document replacement.
#[allow(clippy::collapsible_if)]
@@ -450,16 +537,6 @@ pub mod util {
} else {
return (0, 0, None);
};
-
- if start > end {
- log::error!(
- "Invalid LSP text edit start {:?} > end {:?}, discarding",
- start,
- end
- );
- return (0, 0, None);
- }
-
(start, end, replacement)
}),
)
@@ -475,7 +552,6 @@ pub enum MethodCall {
RegisterCapability(lsp::RegistrationParams),
UnregisterCapability(lsp::UnregistrationParams),
ShowDocument(lsp::ShowDocumentParams),
- WorkspaceDiagnosticRefresh,
}
impl MethodCall {
@@ -507,7 +583,6 @@ impl MethodCall {
let params: lsp::ShowDocumentParams = params.parse()?;
Self::ShowDocument(params)
}
- lsp::request::WorkspaceDiagnosticRefresh::METHOD => Self::WorkspaceDiagnosticRefresh,
_ => {
return Err(Error::Unhandled);
}
@@ -563,42 +638,38 @@ impl Notification {
#[derive(Debug)]
pub struct Registry {
- inner: SlotMap<LanguageServerId, Arc<Client>>,
- inner_by_name: HashMap<LanguageServerName, Vec<Arc<Client>>>,
- syn_loader: Arc<ArcSwap<helix_core::syntax::Loader>>,
- pub incoming: SelectAll<UnboundedReceiverStream<(LanguageServerId, Call)>>,
+ inner: HashMap<LanguageServerName, Vec<Arc<Client>>>,
+ syn_loader: Arc<helix_core::syntax::Loader>,
+ counter: usize,
+ pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
pub file_event_handler: file_event::Handler,
}
impl Registry {
- pub fn new(syn_loader: Arc<ArcSwap<helix_core::syntax::Loader>>) -> Self {
+ pub fn new(syn_loader: Arc<helix_core::syntax::Loader>) -> Self {
Self {
- inner: SlotMap::with_key(),
- inner_by_name: HashMap::new(),
+ inner: HashMap::new(),
syn_loader,
+ counter: 0,
incoming: SelectAll::new(),
file_event_handler: file_event::Handler::new(),
}
}
- pub fn get_by_id(&self, id: LanguageServerId) -> Option<&Arc<Client>> {
- self.inner.get(id)
+ pub fn get_by_id(&self, id: usize) -> Option<&Client> {
+ self.inner
+ .values()
+ .flatten()
+ .find(|client| client.id() == id)
+ .map(|client| &**client)
}
- pub fn remove_by_id(&mut self, id: LanguageServerId) {
- let Some(client) = self.inner.remove(id) else {
- log::debug!("client was already removed");
- return;
- };
+ pub fn remove_by_id(&mut self, id: usize) {
self.file_event_handler.remove_client(id);
- let instances = self
- .inner_by_name
- .get_mut(client.name())
- .expect("inner and inner_by_name must be synced");
- instances.retain(|ls| id != ls.id());
- if instances.is_empty() {
- self.inner_by_name.remove(client.name());
- }
+ self.inner.retain(|_, language_servers| {
+ language_servers.retain(|ls| id != ls.id());
+ !language_servers.is_empty()
+ });
}
fn start_client(
@@ -608,81 +679,76 @@ impl Registry {
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
- ) -> Result<Arc<Client>, StartupError> {
- let syn_loader = self.syn_loader.load();
- let config = syn_loader
+ ) -> Result<Arc<Client>> {
+ let config = self
+ .syn_loader
.language_server_configs()
.get(&name)
.ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?;
- let id = self.inner.try_insert_with_key(|id| {
- start_client(
- id,
- name,
- ls_config,
- config,
- doc_path,
- root_dirs,
- enable_snippets,
- )
- .map(|client| {
- self.incoming.push(UnboundedReceiverStream::new(client.1));
- client.0
- })
- })?;
- Ok(self.inner[id].clone())
+ let id = self.counter;
+ self.counter += 1;
+ let NewClient(client, incoming) = start_client(
+ id,
+ name,
+ ls_config,
+ config,
+ doc_path,
+ root_dirs,
+ enable_snippets,
+ )?;
+ self.incoming.push(UnboundedReceiverStream::new(incoming));
+ Ok(client)
}
- /// If this method is called, all documents that have a reference to the language server have to refresh their language servers,
+ /// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers,
+ /// as it could be that language servers of these documents were stopped by this method.
/// See helix_view::editor::Editor::refresh_language_servers
- pub fn restart_server(
+ pub fn restart(
&mut self,
- name: &str,
language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
- ) -> Option<Result<Arc<Client>>> {
- if let Some(old_clients) = self.inner_by_name.remove(name) {
- if old_clients.is_empty() {
- log::info!("restarting client for '{name}' which was manually stopped");
- } else {
- log::info!("stopping existing clients for '{name}'");
- }
- for old_client in old_clients {
- self.file_event_handler.remove_client(old_client.id());
- self.inner.remove(old_client.id());
- tokio::spawn(async move {
- let _ = old_client.force_shutdown().await;
- });
- }
- }
- let client = match self.start_client(
- name.to_string(),
- language_config,
- doc_path,
- root_dirs,
- enable_snippets,
- ) {
- Ok(client) => client,
- Err(StartupError::NoRequiredRootFound) => return None,
- Err(StartupError::Error(err)) => return Some(Err(err)),
- };
- self.inner_by_name
- .insert(name.to_owned(), vec![client.clone()]);
+ ) -> Result<Vec<Arc<Client>>> {
+ language_config
+ .language_servers
+ .iter()
+ .filter_map(|LanguageServerFeatures { name, .. }| {
+ if self.inner.contains_key(name) {
+ let client = match self.start_client(
+ name.clone(),
+ language_config,
+ doc_path,
+ root_dirs,
+ enable_snippets,
+ ) {
+ Ok(client) => client,
+ error => return Some(error),
+ };
+ let old_clients = self
+ .inner
+ .insert(name.clone(), vec![client.clone()])
+ .unwrap();
+
+ for old_client in old_clients {
+ self.file_event_handler.remove_client(old_client.id());
+ tokio::spawn(async move {
+ let _ = old_client.force_shutdown().await;
+ });
+ }
- Some(Ok(client))
+ Some(Ok(client))
+ } else {
+ None
+ }
+ })
+ .collect()
}
pub fn stop(&mut self, name: &str) {
- if let Some(clients) = self.inner_by_name.get_mut(name) {
- // Drain the clients vec so that the entry in `inner_by_name` remains
- // empty. We use the empty vec as a "tombstone" to mean that a server
- // has been manually stopped with :lsp-stop and shouldn't be automatically
- // restarted by `get`. :lsp-restart can be used to restart the server
- // manually.
- for client in clients.drain(..) {
+ if let Some(clients) = self.inner.remove(name) {
+ for client in clients {
self.file_event_handler.remove_client(client.id());
- self.inner.remove(client.id());
tokio::spawn(async move {
let _ = client.force_shutdown().await;
});
@@ -697,25 +763,13 @@ impl Registry {
root_dirs: &'a [PathBuf],
enable_snippets: bool,
) -> impl Iterator<Item = (LanguageServerName, Result<Arc<Client>>)> + 'a {
- language_config.language_servers.iter().filter_map(
+ language_config.language_servers.iter().map(
move |LanguageServerFeatures { name, .. }| {
- if let Some(clients) = self.inner_by_name.get(name) {
- // If the clients vec is empty, do not automatically start a client
- // for this server. The empty vec is a tombstone left to mean that a
- // server has been manually stopped and shouldn't be started automatically.
- // See `stop`.
- if clients.is_empty() {
- return None;
- }
-
+ if let Some(clients) = self.inner.get(name) {
if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
- let manual_roots = language_config
- .workspace_lsp_roots
- .as_deref()
- .unwrap_or(root_dirs);
- client.try_add_doc(&language_config.roots, manual_roots, doc_path, *i == 0)
+ client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
}) {
- return Some((name.to_owned(), Ok(client.clone())));
+ return (name.to_owned(), Ok(client.clone()));
}
}
match self.start_client(
@@ -726,38 +780,34 @@ impl Registry {
enable_snippets,
) {
Ok(client) => {
- self.inner_by_name
+ self.inner
.entry(name.to_owned())
.or_default()
.push(client.clone());
- Some((name.clone(), Ok(client)))
+ (name.clone(), Ok(client))
}
- Err(StartupError::NoRequiredRootFound) => None,
- Err(StartupError::Error(err)) => Some((name.to_owned(), Err(err))),
+ Err(err) => (name.to_owned(), Err(err)),
}
},
)
}
pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
- self.inner.values()
+ self.inner.values().flatten()
}
}
#[derive(Debug)]
pub enum ProgressStatus {
Created,
- Started {
- title: String,
- progress: lsp::WorkDoneProgress,
- },
+ Started(lsp::WorkDoneProgress),
}
impl ProgressStatus {
pub fn progress(&self) -> Option<&lsp::WorkDoneProgress> {
match &self {
ProgressStatus::Created => None,
- ProgressStatus::Started { title: _, progress } => Some(progress),
+ ProgressStatus::Started(progress) => Some(progress),
}
}
}
@@ -766,7 +816,7 @@ impl ProgressStatus {
/// Acts as a container for progress reported by language servers. Each server
/// has a unique id assigned at creation through [`Registry`]. This id is then used
/// to store the progress in this map.
-pub struct LspProgressMap(HashMap<LanguageServerId, HashMap<lsp::ProgressToken, ProgressStatus>>);
+pub struct LspProgressMap(HashMap<usize, HashMap<lsp::ProgressToken, ProgressStatus>>);
impl LspProgressMap {
pub fn new() -> Self {
@@ -774,42 +824,28 @@ impl LspProgressMap {
}
/// Returns a map of all tokens corresponding to the language server with `id`.
- pub fn progress_map(
- &self,
- id: LanguageServerId,
- ) -> Option<&HashMap<lsp::ProgressToken, ProgressStatus>> {
+ pub fn progress_map(&self, id: usize) -> Option<&HashMap<lsp::ProgressToken, ProgressStatus>> {
self.0.get(&id)
}
- pub fn is_progressing(&self, id: LanguageServerId) -> bool {
+ pub fn is_progressing(&self, id: usize) -> bool {
self.0.get(&id).map(|it| !it.is_empty()).unwrap_or_default()
}
/// Returns last progress status for a given server with `id` and `token`.
- pub fn progress(
- &self,
- id: LanguageServerId,
- token: &lsp::ProgressToken,
- ) -> Option<&ProgressStatus> {
+ pub fn progress(&self, id: usize, token: &lsp::ProgressToken) -> Option<&ProgressStatus> {
self.0.get(&id).and_then(|values| values.get(token))
}
- pub fn title(&self, id: LanguageServerId, token: &lsp::ProgressToken) -> Option<&String> {
- self.progress(id, token).and_then(|p| match p {
- ProgressStatus::Created => None,
- ProgressStatus::Started { title, .. } => Some(title),
- })
- }
-
/// Checks if progress `token` for server with `id` is created.
- pub fn is_created(&mut self, id: LanguageServerId, token: &lsp::ProgressToken) -> bool {
+ pub fn is_created(&mut self, id: usize, token: &lsp::ProgressToken) -> bool {
self.0
.get(&id)
.map(|values| values.get(token).is_some())
.unwrap_or_default()
}
- pub fn create(&mut self, id: LanguageServerId, token: lsp::ProgressToken) {
+ pub fn create(&mut self, id: usize, token: lsp::ProgressToken) {
self.0
.entry(id)
.or_default()
@@ -819,110 +855,50 @@ impl LspProgressMap {
/// Ends the progress by removing the `token` from server with `id`, if removed returns the value.
pub fn end_progress(
&mut self,
- id: LanguageServerId,
+ id: usize,
token: &lsp::ProgressToken,
) -> Option<ProgressStatus> {
self.0.get_mut(&id).and_then(|vals| vals.remove(token))
}
- /// Updates the progress of `token` for server with `id` to begin state `status`
- pub fn begin(
- &mut self,
- id: LanguageServerId,
- token: lsp::ProgressToken,
- status: lsp::WorkDoneProgressBegin,
- ) {
- self.0.entry(id).or_default().insert(
- token,
- ProgressStatus::Started {
- title: status.title.clone(),
- progress: lsp::WorkDoneProgress::Begin(status),
- },
- );
- }
-
- /// Updates the progress of `token` for server with `id` to report state `status`.
+ /// Updates the progress of `token` for server with `id` to `status`, returns the value replaced or `None`.
pub fn update(
&mut self,
- id: LanguageServerId,
+ id: usize,
token: lsp::ProgressToken,
- status: lsp::WorkDoneProgressReport,
- ) {
+ status: lsp::WorkDoneProgress,
+ ) -> Option<ProgressStatus> {
self.0
.entry(id)
.or_default()
- .entry(token)
- .and_modify(|e| match e {
- ProgressStatus::Created => (),
- ProgressStatus::Started { progress, .. } => {
- *progress = lsp::WorkDoneProgress::Report(status)
- }
- });
+ .insert(token, ProgressStatus::Started(status))
}
}
-struct NewClient(Arc<Client>, UnboundedReceiver<(LanguageServerId, Call)>);
-
-enum StartupError {
- NoRequiredRootFound,
- Error(Error),
-}
-
-impl<T: Into<Error>> From<T> for StartupError {
- fn from(value: T) -> Self {
- StartupError::Error(value.into())
- }
-}
+struct NewClient(Arc<Client>, UnboundedReceiver<(usize, Call)>);
/// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that
/// it is only called when it makes sense.
fn start_client(
- id: LanguageServerId,
+ id: usize,
name: String,
config: &LanguageConfiguration,
ls_config: &LanguageServerConfiguration,
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
-) -> Result<NewClient, StartupError> {
- let (workspace, workspace_is_cwd) = helix_loader::find_workspace();
- let workspace = path::normalize(workspace);
- let root = find_lsp_workspace(
- doc_path
- .and_then(|x| x.parent().and_then(|x| x.to_str()))
- .unwrap_or("."),
- &config.roots,
- config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
- &workspace,
- workspace_is_cwd,
- );
-
- // `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());
-
- if let Some(globset) = &ls_config.required_root_patterns {
- if !root_path
- .read_dir()?
- .flatten()
- .map(|entry| entry.file_name())
- .any(|entry| globset.is_match(entry))
- {
- return Err(StartupError::NoRequiredRootFound);
- }
- }
-
+) -> Result<NewClient> {
let (client, incoming, initialize_notify) = Client::start(
&ls_config.command,
&ls_config.args,
ls_config.config.clone(),
- &ls_config.environment,
- root_path,
- root_uri,
+ ls_config.environment.clone(),
+ &config.roots,
+ config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
id,
name,
ls_config.timeout,
+ doc_path,
)?;
let client = Arc::new(client);
@@ -946,7 +922,17 @@ fn start_client(
}
// next up, notify<initialized>
- _client.notify::<lsp::notification::Initialized>(lsp::InitializedParams {});
+ let notification_result = _client
+ .notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
+ .await;
+
+ if let Err(e) = notification_result {
+ log::error!(
+ "failed to notify language server of its initialization: {}",
+ e
+ );
+ return;
+ }
initialize_notify.notify_one();
});
@@ -1045,7 +1031,7 @@ mod tests {
#[test]
fn emoji_format_gh_4791() {
- use lsp::{Position, Range, TextEdit};
+ use lsp_types::{Position, Range, TextEdit};
let edits = vec![
TextEdit {
@@ -1078,107 +1064,7 @@ mod tests {
let mut source = Rope::from_str("[\n\"πŸ‡ΊπŸ‡Έ\",\n\"πŸŽ„\",\n]");
- let transaction = generate_transaction_from_edits(&source, edits, OffsetEncoding::Utf16);
- assert!(transaction.apply(&mut source));
- assert_eq!(source, "[\n \"πŸ‡ΊπŸ‡Έ\",\n \"πŸŽ„\",\n]");
- }
- #[test]
- fn rahh() {
- use helix_lsp_types::*;
- let th = [
- TextEdit {
- range: Range {
- start: Position {
- line: 1,
- character: 0,
- },
- end: Position {
- line: 1,
- character: 4,
- },
- },
- new_text: "".into(),
- },
- TextEdit {
- range: Range {
- start: Position {
- line: 2,
- character: 0,
- },
- end: Position {
- line: 3,
- character: 1,
- },
- },
- new_text: "".into(),
- },
- TextEdit {
- range: Range {
- start: Position {
- line: 3,
- character: 9,
- },
- end: Position {
- line: 3,
- character: 9,
- },
- },
- new_text: "let new =\n".into(),
- },
- TextEdit {
- range: Range {
- start: Position {
- line: 3,
- character: 20,
- },
- end: Position {
- line: 3,
- character: 29,
- },
- },
- new_text: "".into(),
- },
- TextEdit {
- range: Range {
- start: Position {
- line: 3,
- character: 56,
- },
- end: Position {
- line: 4,
- character: 24,
- },
- },
- new_text: "".into(),
- },
- TextEdit {
- range: Range {
- start: Position {
- line: 6,
- character: 1,
- },
- end: Position {
- line: 6,
- character: 1,
- },
- },
- new_text: "\n".into(),
- },
- ];
- let mut source = Rope::from_str(
- "impl Editor { // 0
- pub fn open(f: &Path) { // 1
-// 2
- let new = std::fs::read_to_string(f) // 3
- .map_err(anyhow::Error::from)?; // 4
- }
-}",
- );
- println!("{}", source);
-
- let transaction =
- generate_transaction_from_edits(&source, th.to_vec(), OffsetEncoding::Utf8);
+ let transaction = generate_transaction_from_edits(&source, edits, OffsetEncoding::Utf8);
assert!(transaction.apply(&mut source));
- println!("{}", source);
}
}