diff --git a/Cargo.lock b/Cargo.lock index d33f430f92c2..fac910215dc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1464,6 +1464,7 @@ dependencies = [ "nucleo", "once_cell", "open", + "pin-project-lite", "pulldown-cmark", "same-file", "serde", diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index ec7b2dadd844..a416bdf3c455 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -44,6 +44,7 @@ tokio-stream = "0.1" futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } arc-swap = { version = "1.7.1" } termini = "1" +pin-project-lite = "0.2" # Logging fern = "0.6" diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 3eec6406abf0..f71a34c4f716 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -33,7 +33,12 @@ use crate::{ use log::{debug, error, info, warn}; #[cfg(not(feature = "integration"))] use std::io::stdout; -use std::{collections::btree_map::Entry, io::stdin, path::Path, sync::Arc}; +use std::{ + collections::btree_map::Entry, + io::stdin, + path::{Path, PathBuf}, + sync::Arc, +}; #[cfg(not(windows))] use anyhow::Context; @@ -335,7 +340,7 @@ impl Application { self.handle_terminal_events(event).await; } Some(callback) = self.jobs.callbacks.recv() => { - self.jobs.handle_callback(&mut self.editor, &mut self.compositor, Ok(Some(callback))).await; + self.jobs.handle_callback(&mut self.editor, &mut self.compositor, Ok(Some(callback))); self.render().await; } Some(msg) = self.jobs.status_messages.recv() => { @@ -350,9 +355,39 @@ impl Application { helix_event::request_redraw(); } Some(callback) = self.jobs.wait_futures.next() => { - self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback).await; + self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render().await; } + // Some(Ok(on_save_job)) = self.jobs.on_save.next() => { + // let doc = doc!(self.editor, &on_save_job.doc_id); + // self.editor.on_save(doc).await; + + // // if self.editor.config().auto_format { + // // if let Some(fmt) = doc.auto_format() { + // // let scrolloff = self.editor.config().scrolloff; + // // let doc = doc_mut!(self.editor, &on_save_job.doc_id); + // // let view = view_mut!(self.editor, on_save_job.view_id); + + // // if let Ok(format) = fmt.await { + // // if doc.version() == on_save_job.doc_version { + // // doc.apply(&format, view.id); + // // doc.append_changes_to_history(view); + // // doc.detect_indent_and_line_ending(); + // // view.ensure_cursor_in_view(doc, scrolloff); + // // } else { + // // log::info!("discarded formatting changes because the document changed"); + // // } + // // } + // // } + // // log::debug!("CODEACTION FORMATTED"); + // // } + + // if let Err(err) = self.editor.save::(on_save_job.doc_id, on_save_job.path, on_save_job.force) { + // self.editor.set_error(format!("Error saving: {}", err)); + // } + // log::debug!("CODEACTION SAVED"); + // self.render().await; + // } event = self.editor.wait_event() => { let _idle_handled = self.handle_editor_event(event).await; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 4d3f49633210..a12fbdf92602 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3397,6 +3397,45 @@ async fn make_format_callback( Ok(call) } +async fn make_format_callback_old( + doc_id: DocumentId, + doc_version: i32, + view_id: ViewId, + format: impl Future> + Send + 'static, + write: Option<(Option, bool)>, +) -> anyhow::Result { + let format = format.await; + + let call: job::Callback = Callback::Editor(Box::new(move |editor| { + if !editor.documents.contains_key(&doc_id) || !editor.tree.contains(view_id) { + return; + } + + let scrolloff = editor.config().scrolloff; + let doc = doc_mut!(editor, &doc_id); + let view = view_mut!(editor, view_id); + + if let Ok(format) = format { + if doc.version() == doc_version { + doc.apply(&format, view.id); + doc.append_changes_to_history(view); + doc.detect_indent_and_line_ending(); + view.ensure_cursor_in_view(doc, scrolloff); + } else { + log::info!("discarded formatting changes because the document changed"); + } + } + + if let Some((path, force)) = write { + let id = doc.id(); + if let Err(err) = editor.save(id, path, force) { + editor.set_error(format!("Error saving: {}", err)); + } + } + })); + + Ok(call) +} pub fn format_callback( doc_id: DocumentId, @@ -3446,11 +3485,8 @@ pub fn on_save_callback( code_action_on_save_cfg ); let doc = doc!(editor, &doc_id); - // let code_actions = - // helix_lsp::block_on(code_actions_on_save(doc, code_action_on_save_cfg.clone())); - let code_actions = tokio::task::spawn_blocking(|| { - code_actions_on_save(doc, code_action_on_save_cfg.clone()) - }); + let code_actions = + helix_lsp::block_on(code_actions_on_save(doc, code_action_on_save_cfg.clone())); if code_actions.is_empty() { log::debug!( @@ -3503,6 +3539,48 @@ pub async fn make_on_save_callback( Ok(call) } +pub fn code_action_callback( + editor: &mut Editor, + code_action_on_save_cfg: String, + code_actions: Vec, +) { + if code_actions.is_empty() { + log::debug!( + "Code action on save not found {:?}", + code_action_on_save_cfg + ); + editor.set_error(format!( + "Code Action not found: {:?}", + code_action_on_save_cfg + )); + } + + for code_action in code_actions { + log::debug!( + "Applying code action on save {:?} for language server {:?}", + code_action.lsp_item, + code_action.language_server_id + ); + apply_code_action(editor, &code_action); + } +} + +pub async fn make_code_action_callback( + doc: &Document, + code_action_on_save_cfg: String, +) -> anyhow::Result { + log::debug!( + "Attempting code action on save {:?}", + code_action_on_save_cfg + ); + let code_actions = code_actions_on_save(doc, code_action_on_save_cfg.clone()).await; + + let call = Callback::Editor(Box::new(move |editor| { + code_action_callback(editor, code_action_on_save_cfg, code_actions); + })); + Ok(call) +} + #[derive(PartialEq, Eq)] pub enum Open { Below, diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index dc1699792e32..fd621c0dc9c5 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -6,7 +6,7 @@ use helix_lsp::{ block_on, lsp::{ self, CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionTriggerKind, - DiagnosticSeverity, NumberOrString, + DiagnosticSeverity, NumberOrString, WorkspaceEdit, }, util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range}, Client, LanguageServerId, OffsetEncoding, @@ -850,6 +850,29 @@ pub fn apply_code_action(editor: &mut Editor, action: &CodeActionOrCommandItem) } } +pub fn apply_code_action_on_save( + language_server: &Client, + action: &CodeActionOrCommandItem, +) -> Option { + match &action.lsp_item { + lsp::CodeActionOrCommand::CodeAction(code_action) => { + log::debug!("code action: {:?}", code_action); + // we support lsp "codeAction/resolve" for `edit` and `command` fields + if code_action.edit.is_none() || code_action.command.is_none() { + if let Some(future) = language_server.resolve_code_action(code_action.clone()) { + if let Ok(response) = helix_lsp::block_on(future) { + if let Ok(code_action) = serde_json::from_value::(response) { + return code_action.edit; + } + } + } + } + None + } + _ => None, // Commands are deprecated and not supported + } +} + impl ui::menu::Item for lsp::Command { type Data = (); fn format(&self, _data: &Self::Data) -> Row { diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 7c89e167f5ae..68b8679a6d7c 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -336,18 +336,97 @@ fn write_impl( ) -> anyhow::Result<()> { let config = cx.editor.config(); let jobs = &mut cx.jobs; - let (view, doc) = current!(cx.editor); let path = path.map(AsRef::as_ref); - if config.insert_final_newline { - insert_final_newline(doc, view.id); + let (doc_id, view_id, code_actions_on_save_cfg) = { + let (view, doc) = current!(cx.editor); + + if config.insert_final_newline { + insert_final_newline(doc, view.id); + } + + // Save an undo checkpoint for any outstanding changes. + doc.append_changes_to_history(view); + + ( + doc.id(), + view.id, + doc.language_config() + .and_then(|c| c.code_actions_on_save.clone()), + ) + }; + + if let Some(code_actions_on_save_cfg) = code_actions_on_save_cfg { + for code_action_on_save_cfg in code_actions_on_save_cfg + .into_iter() + .filter_map(|action| action.enabled.then_some(action.code_action)) + { + log::debug!( + "Attempting code action on save {:?}", + code_action_on_save_cfg + ); + let code_actions = { + let doc = doc!(cx.editor, &doc_id); + helix_lsp::block_on(code_actions_on_save(doc, code_action_on_save_cfg.clone())) + }; + + if code_actions.is_empty() { + log::debug!( + "Code action on save not found {:?}", + code_action_on_save_cfg + ); + cx.editor.set_error(format!( + "Code Action not found: {:?}", + code_action_on_save_cfg + )); + } + + for code_action in code_actions { + log::debug!( + "Applying code action on save {:?} for language server {:?}", + code_action.lsp_item, + code_action.language_server_id + ); + let Some(language_server) = cx + .editor + .language_server_by_id(code_action.language_server_id) + else { + cx.editor.set_error("Language Server disappeared"); + continue; + }; + if let Some(ref workspace_edit) = + apply_code_action_on_save(language_server, &code_action) + { + let offset_encoding = language_server.offset_encoding(); + let _ = cx + .editor + .apply_workspace_edit(offset_encoding, workspace_edit); + } + } + } } - // Save an undo checkpoint for any outstanding changes. - doc.append_changes_to_history(view); + let fmt = if config.auto_format { + let doc = doc!(cx.editor, &doc_id); + doc.auto_format().map(|fmt| { + let callback = make_format_callback_old( + doc.id(), + doc.version(), + view_id, + fmt, + Some((path.map(Into::into), force)), + ); - let callback = make_on_save_callback(doc.id(), view.id, path.map(Into::into), force); - jobs.add(Job::with_callback(callback).wait_before_exiting()); + jobs.add(Job::with_callback(callback).wait_before_exiting()); + }) + } else { + None + }; + + if fmt.is_none() { + let id = doc_id; + cx.editor.save(id, path, force)?; + } Ok(()) } @@ -355,12 +434,15 @@ fn write_impl( fn insert_final_newline(doc: &mut Document, view_id: ViewId) { let text = doc.text(); if line_ending::get_line_ending(&text.slice(..)).is_none() { + log::debug!("INSERTING NEW LNIE"); let eof = Selection::point(text.len_chars()); let insert = Transaction::insert(text, &eof, doc.line_ending.as_str().into()); doc.apply(&insert, view_id); } } +fn on_save_code_actions() {} + fn write( cx: &mut compositor::Context, args: &[Cow], @@ -687,18 +769,94 @@ pub fn write_all_impl( .collect(); for (doc_id, target_view) in saves { - let doc = doc_mut!(cx.editor, &doc_id); - let view = view_mut!(cx.editor, target_view); + let (doc_id, view_id, code_actions_on_save_cfg) = { + let doc = doc_mut!(cx.editor, &doc_id); + let view = view_mut!(cx.editor, target_view); - if config.insert_final_newline { - insert_final_newline(doc, target_view); - } + if config.insert_final_newline { + insert_final_newline(doc, view.id); + } - // Save an undo checkpoint for any outstanding changes. - doc.append_changes_to_history(view); + // Save an undo checkpoint for any outstanding changes. + doc.append_changes_to_history(view); + + ( + doc.id(), + view.id, + doc.language_config() + .and_then(|c| c.code_actions_on_save.clone()), + ) + }; + + if let Some(code_actions_on_save_cfg) = code_actions_on_save_cfg { + for code_action_on_save_cfg in code_actions_on_save_cfg + .into_iter() + .filter_map(|action| action.enabled.then_some(action.code_action)) + { + log::debug!( + "Attempting code action on save {:?}", + code_action_on_save_cfg + ); + let code_actions = { + let doc = doc!(cx.editor, &doc_id); + helix_lsp::block_on(code_actions_on_save(doc, code_action_on_save_cfg.clone())) + }; + + if code_actions.is_empty() { + log::debug!( + "Code action on save not found {:?}", + code_action_on_save_cfg + ); + cx.editor.set_error(format!( + "Code Action not found: {:?}", + code_action_on_save_cfg + )); + } + + for code_action in code_actions { + log::debug!( + "Applying code action on save {:?} for language server {:?}", + code_action.lsp_item, + code_action.language_server_id + ); + let Some(language_server) = cx + .editor + .language_server_by_id(code_action.language_server_id) + else { + cx.editor.set_error("Language Server disappeared"); + continue; + }; + if let Some(ref workspace_edit) = + apply_code_action_on_save(language_server, &code_action) + { + let offset_encoding = language_server.offset_encoding(); + let _ = cx + .editor + .apply_workspace_edit(offset_encoding, workspace_edit); + } + } + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } + let fmt = if config.auto_format { + let doc = doc!(cx.editor, &doc_id); + doc.auto_format().map(|fmt| { + let callback = make_format_callback_old( + doc_id, + doc.version(), + target_view, + fmt, + Some((None, force)), + ); + jobs.add(Job::with_callback(callback).wait_before_exiting()); + }) + } else { + None + }; - let callback = make_on_save_callback(doc.id(), target_view, None, force); - jobs.add(Job::with_callback(callback).wait_before_exiting()); + if fmt.is_none() { + cx.editor.save::(doc_id, None, force)?; + } } if !errors.is_empty() && !force { diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs index 40912dae0791..9045979e426b 100644 --- a/helix-term/src/job.rs +++ b/helix-term/src/job.rs @@ -1,12 +1,18 @@ +use std::collections::VecDeque; +use std::path::PathBuf; + +use anyhow::Result; use helix_event::status::StatusMessage; use helix_event::{runtime_local, send_blocking}; -use helix_view::Editor; +use helix_lsp::lsp::CodeActionKind; +use helix_view::{DocumentId, Editor, ViewId}; use once_cell::sync::OnceCell; use crate::compositor::Compositor; +use crate::stream::FuturesUnorderedSeries; use futures_util::future::{BoxFuture, Future, FutureExt}; -use futures_util::stream::{FuturesUnordered, StreamExt}; +use futures_util::stream::{FuturesOrdered, FuturesUnordered, StreamExt}; use tokio::sync::mpsc::{channel, Receiver, Sender}; pub type EditorCompositorCallback = Box; @@ -47,7 +53,7 @@ pub struct Job { pub struct Jobs { /// jobs that need to complete before we exit. - pub wait_futures: FuturesUnordered, + pub wait_futures: FuturesUnorderedSeries, pub callbacks: Receiver, pub status_messages: Receiver, } @@ -82,7 +88,7 @@ impl Jobs { let _ = JOB_QUEUE.set(tx); let status_messages = helix_event::status::setup(); Self { - wait_futures: FuturesUnordered::new(), + wait_futures: FuturesUnorderedSeries::new(), callbacks: rx, status_messages, } @@ -99,7 +105,7 @@ impl Jobs { self.add(Job::with_callback(f)); } - pub async fn handle_callback( + pub fn handle_callback( &self, editor: &mut Editor, compositor: &mut Compositor, @@ -117,7 +123,7 @@ impl Jobs { } } - pub fn add(&self, j: Job) { + pub fn add(&mut self, j: Job) { if j.wait { self.wait_futures.push(j.future); } else { diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index cf4fbd9fa7ae..25c18032b675 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -10,6 +10,7 @@ pub mod events; pub mod health; pub mod job; pub mod keymap; +pub mod stream; pub mod ui; use std::path::Path; diff --git a/helix-term/src/stream.rs b/helix-term/src/stream.rs new file mode 100644 index 000000000000..f672f6b82934 --- /dev/null +++ b/helix-term/src/stream.rs @@ -0,0 +1,254 @@ +use core::cmp::Ordering; +use core::fmt::{self, Debug}; +use core::iter::FromIterator; +use core::pin::Pin; +use futures_util::stream::{FusedStream, FuturesUnordered, Stream}; +use futures_util::task::{Context, Poll}; +use futures_util::{ready, Future, StreamExt}; +use std::collections::binary_heap::PeekMut; +use std::collections::{BinaryHeap, HashMap}; + +use pin_project_lite::pin_project; + +pin_project! { + #[must_use = "futures do nothing unless you `.await` or poll them"] + #[derive(Debug)] + struct SeriesWrapper { + #[pin] + data: T, // A future or a future's output + // Use i64 for index since isize may overflow in 32-bit targets. + index: i64, + } +} + +impl PartialEq for SeriesWrapper { + fn eq(&self, other: &Self) -> bool { + self.index == other.index + } +} + +impl Eq for SeriesWrapper {} + +impl PartialOrd for SeriesWrapper { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for SeriesWrapper { + fn cmp(&self, other: &Self) -> Ordering { + // BinaryHeap is a max heap, so compare backwards here. + other.index.cmp(&self.index) + } +} + +impl Future for SeriesWrapper +where + T: Future, +{ + type Output = SeriesWrapper; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let index = self.index; + self.project().data.poll(cx).map(|output| SeriesWrapper { + data: output, + index, + }) + } +} + +/// An unbounded queue of futures. +/// +/// This "combinator" is similar to [`FuturesUnordered`], but it imposes a FIFO +/// order on top of the set of futures. While futures in the set will race to +/// completion in parallel, results will only be returned in the order their +/// originating futures were added to the queue. +/// +/// Futures are pushed into this queue and their realized values are yielded in +/// order. This structure is optimized to manage a large number of futures. +/// Futures managed by [`FuturesUnorderedSeries`] will only be polled when they generate +/// notifications. This reduces the required amount of work needed to coordinate +/// large numbers of futures. +/// +/// When a [`FuturesUnorderedSeries`] is first created, it does not contain any futures. +/// Calling [`poll_next`](FuturesUnorderedSeries::poll_next) in this state will result +/// in [`Poll::Ready(None)`](Poll::Ready) to be returned. Futures are submitted +/// to the queue using [`push_back`](FuturesUnorderedSeries::push_back) (or +/// [`push_front`](FuturesUnorderedSeries::push_front)); however, the future will +/// **not** be polled at this point. [`FuturesUnorderedSeries`] will only poll managed +/// futures when [`FuturesUnorderedSeries::poll_next`] is called. As such, it +/// is important to call [`poll_next`](FuturesUnorderedSeries::poll_next) after pushing +/// new futures. +/// +/// If [`FuturesUnorderedSeries::poll_next`] returns [`Poll::Ready(None)`](Poll::Ready) +/// this means that the queue is currently not managing any futures. A future +/// may be submitted to the queue at a later time. At that point, a call to +/// [`FuturesUnorderedSeries::poll_next`] will either return the future's resolved value +/// **or** [`Poll::Pending`] if the future has not yet completed. When +/// multiple futures are submitted to the queue, [`FuturesUnorderedSeries::poll_next`] +/// will return [`Poll::Pending`] until the first future completes, even if +/// some of the later futures have already completed. +/// +/// Note that you can create a ready-made [`FuturesUnorderedSeries`] via the +/// [`collect`](Iterator::collect) method, or you can start with an empty queue +/// with the [`FuturesUnorderedSeries::new`] constructor. +/// +/// This type is only available when the `std` or `alloc` feature of this +/// library is activated, and it is activated by default. +#[must_use = "streams do nothing unless polled"] +pub struct FuturesUnorderedSeries { + in_progress_queue: FuturesUnordered>, + series_queue: HashMap>, + next_index: i64, +} + +impl Unpin for FuturesUnorderedSeries {} + +impl FuturesUnorderedSeries { + /// Constructs a new, empty `FuturesUnorderedSeries` + /// + /// The returned [`FuturesUnorderedSeries`] does not contain any futures and, in + /// this state, [`FuturesUnorderedSeries::poll_next`] will return + /// [`Poll::Ready(None)`](Poll::Ready). + pub fn new() -> Self { + Self { + in_progress_queue: FuturesUnordered::new(), + series_queue: HashMap::new(), + next_index: 0, + } + } + + /// Returns the number of futures contained in the queue. + /// + /// This represents the total number of in-flight futures, both + /// those currently processing and those that are left in a series. + pub fn len(&self) -> usize { + self.in_progress_queue.len() + self.series_queue.values().fold(0, |acc, s| acc + s.len()) + } + + /// Returns `true` if the queue contains no futures + pub fn is_empty(&self) -> bool { + self.in_progress_queue.is_empty() && self.series_queue.is_empty() + } + + /// Push a future into the queue. + /// + /// This function submits the given future to the internal set for managing. + /// This function will not call [`poll`](Future::poll) on the submitted + /// future. The caller must ensure that [`FuturesUnorderedSeries::poll_next`] is + /// called in order to receive task notifications. + pub fn push(&mut self, future: Fut) { + let wrapped = SeriesWrapper { + data: future, + index: self.next_index, + }; + self.next_index += 1; + self.in_progress_queue.push(wrapped); + } + + /// Push a vector of futures to run sequentially into the queue. + /// + /// This function submits the given future to the internal set for managing. + /// This function will not call [`poll`](Future::poll) on the submitted + /// future. The caller must ensure that [`FuturesUnorderedSeries::poll_next`] is + /// called in order to receive task notifications. + pub fn push_series(&mut self, mut futures: Vec) { + futures.reverse(); + if let Some(first) = futures.pop() { + let wrapped = SeriesWrapper { + data: first, + index: self.next_index, + }; + if !futures.is_empty() { + self.series_queue.insert(self.next_index, futures); + } + self.next_index += 1; + self.in_progress_queue.push(wrapped); + } + } +} + +impl Default for FuturesUnorderedSeries { + fn default() -> Self { + Self::new() + } +} + +impl Stream for FuturesUnorderedSeries { + type Item = Fut::Output; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = &mut *self; + + // // Check to see if we've already received the next value + // if let Some(next_output) = this.queued_outputs.peek_mut() { + // if next_output.index == this.next_outgoing_index { + // this.next_outgoing_index += 1; + // return Poll::Ready(Some(PeekMut::pop(next_output).data)); + // } + // } + + loop { + match ready!(this.in_progress_queue.poll_next_unpin(cx)) { + Some(output) => { + if let Some(futures) = this.series_queue.get_mut(&output.index) { + if let Some(next) = futures.pop() { + let wrapped = SeriesWrapper { + data: next, + index: output.index, + }; + this.in_progress_queue.push(wrapped); + } + if futures.is_empty() { + this.series_queue.remove(&output.index); + } + } + + return Poll::Ready(Some(output.data)); + } + None => return Poll::Ready(None), + } + } + } + + fn size_hint(&self) -> (usize, Option) { + let len = self.len(); + (len, Some(len)) + } +} + +impl Debug for FuturesUnorderedSeries { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "FuturesUnorderedSeries {{ ... }}") + } +} + +impl FromIterator for FuturesUnorderedSeries { + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + let acc = Self::new(); + iter.into_iter().fold(acc, |mut acc, item| { + acc.push(item); + acc + }) + } +} + +impl FusedStream for FuturesUnorderedSeries { + fn is_terminated(&self) -> bool { + self.in_progress_queue.is_terminated() && self.series_queue.is_empty() + } +} + +impl Extend for FuturesUnorderedSeries { + fn extend(&mut self, iter: I) + where + I: IntoIterator, + { + for item in iter { + self.push(item); + } + } +} diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index f2d7f192daf0..77ec13b09fc8 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -9,9 +9,11 @@ use helix_core::doc_formatter::TextFormat; use helix_core::encoding::Encoding; use helix_core::syntax::{Highlight, LanguageServerFeature}; use helix_core::text_annotations::{InlineAnnotation, Overlay}; -use helix_lsp::util::lsp_pos_to_pos; +use helix_lsp::lsp::{CodeActionKind, CodeActionTriggerKind}; +use helix_lsp::util::{diagnostic_to_lsp_diagnostic, lsp_pos_to_pos, range_to_lsp_range}; use helix_stdx::faccess::{copy_metadata, readonly}; use helix_vcs::{DiffHandle, DiffProviderRegistry}; +use serde_json::Value; use thiserror; use ::parking_lot::Mutex; @@ -19,7 +21,7 @@ use serde::de::{self, Deserialize, Deserializer}; use serde::Serialize; use std::borrow::Cow; use std::cell::Cell; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::future::Future; use std::io; @@ -1438,7 +1440,8 @@ impl Document { ); if let Some(notify) = notify { - let _ = helix_lsp::block_on(notify); + // let _ = helix_lsp::block_on(notify); + tokio::spawn(notify); } } } @@ -2129,6 +2132,49 @@ impl Document { pub fn reset_all_inlay_hints(&mut self) { self.inlay_hints = Default::default(); } + + pub fn code_actions_for_range( + &self, + range: helix_core::Range, + only: Option>, + ) -> Vec<( + impl Future>, + LanguageServerId, + )> { + let mut seen_language_servers = HashSet::new(); + + self.language_servers_with_feature(LanguageServerFeature::CodeAction) + .filter(|ls| seen_language_servers.insert(ls.id())) + // 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 lsp_range = range_to_lsp_range(self.text(), range, offset_encoding); + // Filter and convert overlapping diagnostics + let code_action_context = lsp::CodeActionContext { + diagnostics: self + .diagnostics() + .iter() + .filter(|&diag| { + range + .overlaps(&helix_core::Range::new(diag.range.start, diag.range.end)) + }) + .map(|diag| { + diagnostic_to_lsp_diagnostic(self.text(), diag, offset_encoding) + }) + .collect(), + only: only.clone(), + trigger_kind: Some(CodeActionTriggerKind::INVOKED), + }; + let code_action_request = language_server.code_actions( + self.identifier(), + lsp_range, + code_action_context, + )?; + Some((code_action_request, language_server_id)) + }) + .collect::>() + } } #[derive(Debug, Default)] diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 1708b3b4e053..43236588278e 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -12,12 +12,16 @@ use crate::{ tree::{self, Tree}, Document, DocumentId, View, ViewId, }; + use dap::StackFrame; use helix_vcs::DiffProviderRegistry; use futures_util::stream::select_all::SelectAll; use futures_util::{future, StreamExt}; -use helix_lsp::{Call, LanguageServerId}; +use helix_lsp::{ + lsp::{CodeAction, CodeActionKind, CodeActionOrCommand}, + Call, LanguageServerId, +}; use tokio_stream::wrappers::UnboundedReceiverStream; use std::{ @@ -25,6 +29,7 @@ use std::{ cell::Cell, collections::{BTreeMap, HashMap, HashSet}, fs, + future::Future, io::{self, stdin}, num::NonZeroUsize, path::{Path, PathBuf}, @@ -1142,6 +1147,11 @@ pub enum CloseError { SaveError(anyhow::Error), } +pub struct CodeActionOrCommandItem { + pub lsp_item: lsp::CodeActionOrCommand, + pub language_server_id: LanguageServerId, +} + impl Editor { pub fn new( mut area: Rect, @@ -1335,6 +1345,15 @@ impl Editor { .map(|client| &**client) } + pub fn language_server_by_id_mut( + &mut self, + language_server_id: LanguageServerId, + ) -> Option<&helix_lsp::Client> { + self.language_servers + .get_by_id(language_server_id) + .map(|client| &**client) + } + /// Refreshes the language server for a given document pub fn refresh_language_servers(&mut self, doc_id: DocumentId) { self.launch_language_servers(doc_id) @@ -1809,6 +1828,146 @@ impl Editor { Ok(()) } + pub async fn apply_code_action(&mut self, action: &CodeActionOrCommandItem) { + let Some(language_server) = self.language_server_by_id(action.language_server_id) else { + self.set_error("Language Server disappeared"); + return; + }; + let offset_encoding = language_server.offset_encoding(); + + match &action.lsp_item { + lsp::CodeActionOrCommand::Command(command) => { + log::debug!("code action command: {:?}", command); + // execute_lsp_command(action.language_server_id, command.clone()); + } + lsp::CodeActionOrCommand::CodeAction(code_action) => { + log::debug!("code action: {:?}", code_action); + // 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() { + if let Some(future) = language_server.resolve_code_action(code_action.clone()) { + if let Ok(response) = future.await { + if let Ok(code_action) = serde_json::from_value::(response) + { + resolved_code_action = Some(code_action); + } + } + } + } + let resolved_code_action = resolved_code_action.as_ref().unwrap_or(code_action); + + if let Some(ref workspace_edit) = resolved_code_action.edit { + let _ = self.apply_workspace_edit(offset_encoding, workspace_edit); + } + + // if code action provides both edit and command first the edit + // should be applied and then the command + // if let Some(command) = &code_action.command { + // execute_lsp_command(self, action.language_server_id, command.clone()); + // } + } + } + } + + pub async fn on_save(&mut self, doc: &Document) { + if let Some(code_actions_on_save_cfg) = doc + .language_config() + .and_then(|c| c.code_actions_on_save.clone()) + { + for code_action_on_save_cfg in code_actions_on_save_cfg + .into_iter() + .filter_map(|action| action.enabled.then_some(action.code_action)) + { + log::debug!( + "Attempting code action on save {:?}", + code_action_on_save_cfg + ); + let code_actions = + Editor::code_actions_on_save(doc, code_action_on_save_cfg.clone()).await; + + if code_actions.is_empty() { + log::debug!( + "Code action on save not found {:?}", + code_action_on_save_cfg + ); + self.set_error(format!( + "Code Action not found: {:?}", + code_action_on_save_cfg + )); + } + + for code_action in code_actions { + log::debug!( + "Applying code action on save {:?} for language server {:?}", + code_action.lsp_item, + code_action.language_server_id + ); + self.apply_code_action(&code_action); + } + } + } + + log::debug!("CODEACTION APPLIED"); + + // if self.config().auto_format { + // // let doc = doc!(editor, &doc_id); + // if let Some(fmt) = doc.auto_format() { + // format_callback(doc.id(), doc.version(), view_id, fmt.await, editor); + // } + // log::debug!("CODEACTION FORMATTED"); + // } + + // if let Err(err) = editor.save::(doc_id, path, force) { + // editor.set_error(format!("Error saving: {}", err)); + // } + // log::debug!("CODEACTION SAVED"); + } + + pub fn code_actions_on_save( + doc: &Document, + code_action_on_save: String, + ) -> impl Future> { + let full_range = Range::new(0, doc.text().len_chars()); + let code_action_kind = CodeActionKind::from(code_action_on_save); + let futures = doc.code_actions_for_range(full_range, Some(vec![code_action_kind])); + + async { + let mut items: Vec = vec![]; + + for (request, language_server_id) in futures { + if let Ok(json) = request.await { + if let Ok(Some(mut actions)) = + serde_json::from_value::>(json) + { + // Retain only enabled code actions that do not have commands. + // + // Commands are not supported because they apply + // workspace edits asynchronously and there is currently no mechanism + // to handle waiting for the workspace edits to be applied before moving + // on to the next code action (or auto-format). + actions.retain(|action| { + matches!( + action, + CodeActionOrCommand::CodeAction(CodeAction { + disabled: None, + command: None, + .. + }) + ) + }); + + if let Some(lsp_item) = actions.first() { + items.push(CodeActionOrCommandItem { + lsp_item: lsp_item.clone(), + language_server_id, + }); + } + } + } + } + items + } + } pub fn save>( &mut self,