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.rs | 588 |
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); } } |