├── .gitignore ├── .rustfmt.toml ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── LICENSE-MIT ├── README.md ├── benches └── linecache.rs ├── examples └── it_works.rs └── src ├── cache.rs ├── client.rs ├── core.rs ├── errors.rs ├── frontend.rs ├── lib.rs ├── protocol ├── client.rs ├── codec.rs ├── endpoint.rs ├── errors.rs ├── message.rs ├── mod.rs ├── server.rs └── transport.rs └── structs ├── alert.rs ├── config.rs ├── findreplace.rs ├── language.rs ├── line.rs ├── mod.rs ├── modifyselection.rs ├── operation.rs ├── plugins.rs ├── position.rs ├── scroll_to.rs ├── style.rs ├── theme.rs ├── update.rs └── view.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | .idea/** 5 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | attributes_on_same_line_as_field = false 2 | attributes_on_same_line_as_variant = false 3 | error_on_line_overflow = false 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | matrix: 7 | allow_failures: 8 | - rust: nightly 9 | cache: cargo 10 | before_script: 11 | - rustup component add clippy 12 | - rustup component add rustfmt 13 | script: 14 | # Required for tests. Install only if not present already 15 | - bash -c 'which xi-core || cargo install --git https://github.com/xi-editor/xi-editor xi-core' 16 | - cargo clippy 17 | - cargo fmt -- --check 18 | - cargo test 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Corentin Henry "] 3 | description = "Xi Rpc Lib - Tokio based implementation of the RPC used in the Xi editor" 4 | homepage = "https://github.com/xi-frontend/xrl" 5 | keywords = [ 6 | "xi", 7 | "rpc", 8 | "json-rpc", 9 | ] 10 | license-file = "LICENSE-MIT" 11 | name = "xrl" 12 | readme = "README.md" 13 | repository = "https://github.com/xi-frontend/xrl" 14 | version = "0.0.9" 15 | edition = "2018" 16 | 17 | [dependencies] 18 | bytes = "0.4.12" 19 | futures = "0.1.27" 20 | log = "0.4.6" 21 | serde = "1.0.92" 22 | serde_derive = "1.0.92" 23 | serde_json = "1.0.39" 24 | tokio = "0.1.21" 25 | tokio-codec = "0.1.1" 26 | tokio-process = "0.2.3" 27 | syntect = { version = "3.2.0", default-features = false } 28 | 29 | [dependencies.clippy] 30 | optional = true 31 | version = "0.0.302" 32 | 33 | [dev-dependencies] 34 | criterion = "0.2" 35 | 36 | [[bench]] 37 | name = "linecache" 38 | harness = false 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Corentin Henry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | XRL (Xi Rpc Lib) is an implementation of a client for Xi RPC. 2 | It is used to build [xi-tui](https://github.com/xi-frontend/xi-term/). 3 | 4 | It is still work in progress, but the major features are already in place. 5 | -------------------------------------------------------------------------------- /examples/it_works.rs: -------------------------------------------------------------------------------- 1 | extern crate futures; 2 | extern crate tokio; 3 | extern crate xrl; 4 | 5 | use futures::{future, Future, Stream}; 6 | use xrl::*; 7 | 8 | // Type that represent a `xi-core` peer. It implements `Frontend`, 9 | // which means it can handle notifications and requests from 10 | // `xi-core`. 11 | #[allow(dead_code)] 12 | struct MyFrontend { 13 | // This is not actually used in this example, but if we wanted to 14 | // our frontend could use a `Client` so that it could send 15 | // requests and notifications to `xi-core`, instead of just 16 | // handling incoming messages. 17 | client: Client, 18 | } 19 | 20 | // Implement how our client handles notifications & requests from the core. 21 | impl Frontend for MyFrontend { 22 | type NotificationResult = Result<(), ()>; 23 | fn handle_notification(&mut self, notification: XiNotification) -> Self::NotificationResult { 24 | use XiNotification::*; 25 | match notification { 26 | Update(update) => println!("received `update` from Xi core:\n{:?}", update), 27 | ScrollTo(scroll) => println!("received `scroll_to` from Xi core:\n{:?}", scroll), 28 | DefStyle(style) => println!("received `def_style` from Xi core:\n{:?}", style), 29 | AvailablePlugins(plugins) => { 30 | println!("received `available_plugins` from Xi core:\n{:?}", plugins) 31 | } 32 | UpdateCmds(cmds) => println!("received `update_cmds` from Xi core:\n{:?}", cmds), 33 | PluginStarted(plugin) => { 34 | println!("received `plugin_started` from Xi core:\n{:?}", plugin) 35 | } 36 | PluginStoped(plugin) => { 37 | println!("received `plugin_stoped` from Xi core:\n{:?}", plugin) 38 | } 39 | ConfigChanged(config) => { 40 | println!("received `config_changed` from Xi core:\n{:?}", config) 41 | } 42 | ThemeChanged(theme) => println!("received `theme_changed` from Xi core:\n{:?}", theme), 43 | Alert(alert) => println!("received `alert` from Xi core:\n{:?}", alert), 44 | AvailableThemes(themes) => { 45 | println!("received `available_themes` from Xi core:\n{:?}", themes) 46 | } 47 | FindStatus(status) => println!("received `find_status` from Xi core:\n{:?}", status), 48 | ReplaceStatus(status) => { 49 | println!("received `replace_status` from Xi core:\n{:?}", status) 50 | } 51 | AvailableLanguages(langs) => { 52 | println!("received `available_languages` from Xi core:\n{:?}", langs) 53 | } 54 | LanguageChanged(lang) => { 55 | println!("received `language_changed` from Xi core:\n{:?}", lang) 56 | } 57 | } 58 | Ok(()) 59 | } 60 | 61 | type MeasureWidthResult = Result>, ()>; 62 | // we don't actually use the `request` argument in this example, 63 | // hence the attribute. 64 | #[allow(unused_variables)] 65 | fn handle_measure_width(&mut self, request: MeasureWidth) -> Self::MeasureWidthResult { 66 | Ok(Vec::new()) 67 | } 68 | } 69 | 70 | struct MyFrontendBuilder; 71 | 72 | impl FrontendBuilder for MyFrontendBuilder { 73 | type Frontend = MyFrontend; 74 | fn build(self, client: Client) -> Self::Frontend { 75 | MyFrontend { client } 76 | } 77 | } 78 | 79 | fn main() { 80 | tokio::run(future::lazy(move || { 81 | // spawn Xi core 82 | let (client, core_stderr) = spawn("xi-core", MyFrontendBuilder {}).unwrap(); 83 | 84 | // start logging Xi core's stderr 85 | tokio::spawn( 86 | core_stderr 87 | .for_each(|msg| { 88 | println!("xi-core stderr: {}", msg); 89 | Ok(()) 90 | }) 91 | .map_err(|_| ()), 92 | ); 93 | 94 | let client_clone = client.clone(); 95 | client 96 | // Xi core expects the first notification to be 97 | // "client_started" 98 | .client_started(None, None) 99 | .map_err(|e| eprintln!("failed to send \"client_started\": {:?}", e)) 100 | .and_then(move |_| { 101 | let client = client_clone.clone(); 102 | client 103 | .new_view(None) 104 | .map(|view_name| println!("opened new view: {}", view_name)) 105 | .map_err(|e| eprintln!("failed to open a new view: {:?}", e)) 106 | .and_then(move |_| { 107 | // Forces to shut down the Xi-RPC 108 | // endoint. Otherwise, this example would keep 109 | // running until the xi-core process 110 | // terminates. 111 | println!("shutting down"); 112 | client_clone.shutdown(); 113 | Ok(()) 114 | }) 115 | }) 116 | })); 117 | } 118 | -------------------------------------------------------------------------------- /src/cache.rs: -------------------------------------------------------------------------------- 1 | use crate::{Line, Operation, OperationType, Update}; 2 | 3 | /// Line cache struct to work with xi update protocol. 4 | #[derive(Clone, Debug, Default)] 5 | pub struct LineCache { 6 | invalid_before: u64, 7 | lines: Vec, 8 | invalid_after: u64, 9 | } 10 | 11 | impl LineCache { 12 | /// Retrieve the number of invalid lines before 13 | /// the start of the line cache. 14 | pub fn before(&self) -> u64 { 15 | self.invalid_before 16 | } 17 | 18 | /// Retrieve the number of invalid lines after 19 | /// the line cache. 20 | pub fn after(&self) -> u64 { 21 | self.invalid_after 22 | } 23 | 24 | /// Retrieve all lines in the cache. 25 | pub fn lines(&self) -> &Vec { 26 | &self.lines 27 | } 28 | 29 | /// Retrieve the total height of the linecache 30 | pub fn height(&self) -> u64 { 31 | self.before() + self.lines.len() as u64 + self.after() 32 | } 33 | 34 | /// Handle an xi-core update. 35 | pub fn update(&mut self, update: Update) { 36 | debug!("line cache before update: {:?}", self); 37 | debug!( 38 | "operations to be applied to the line cache: {:?}", 39 | &update.operations 40 | ); 41 | 42 | let mut helper = UpdateHelper { 43 | old_cache: self, 44 | new_cache: LineCache::default(), 45 | }; 46 | 47 | helper.update(update.operations); 48 | } 49 | 50 | pub fn is_empty(&self) -> bool { 51 | self.lines.is_empty() 52 | } 53 | } 54 | 55 | #[derive(Debug)] 56 | // This struct manages the modification of the given LineCache by the 57 | // updates received from xi-core. 58 | // 59 | // Its main workflow is to borrow a LineCache as "old_cache" and then 60 | // call the primary method, UpdateHelper::update(), which accepts an 61 | // Update object and then calculates the new cache as a function 62 | // f(old_cache, update)->new_cache as specified by the xi frontend 63 | // protocol. 64 | // 65 | // UpdateHelper is using an internal variable "new_cache" local to 66 | // UpdateHelper for holding the current state of the cache. In the end 67 | // of update(), the content of new_cache is written into old_cache. 68 | struct UpdateHelper<'a> { 69 | old_cache: &'a mut LineCache, 70 | new_cache: LineCache, 71 | } 72 | 73 | impl<'a> UpdateHelper<'a> { 74 | fn apply_copy(&mut self, nb_lines: u64, first_line_num: Option) { 75 | debug!("copying {} lines", nb_lines); 76 | let UpdateHelper { 77 | old_cache: 78 | LineCache { 79 | invalid_before: ref mut old_invalid_before, 80 | lines: ref mut old_lines, 81 | invalid_after: ref mut old_invalid_after, 82 | }, 83 | new_cache: 84 | LineCache { 85 | invalid_before: ref mut new_invalid_before, 86 | lines: ref mut new_lines, 87 | invalid_after: ref mut new_invalid_after, 88 | }, 89 | } = self; 90 | 91 | // The number of lines left to copy 92 | let mut nb_lines = nb_lines; 93 | 94 | // STEP 1: Handle the invalid lines that precede the valid ones 95 | // ------------------------------------------------------------ 96 | 97 | if *old_invalid_before >= nb_lines { 98 | // case 1: there are more (or equal) invalid lines than lines to copy 99 | 100 | // decrement old_invalid_lines by nb_lines 101 | *old_invalid_before -= nb_lines; 102 | 103 | // and increment new_invalid_lines by the same amount 104 | *new_invalid_before += nb_lines; 105 | 106 | // there is no more line to copy so we're done 107 | return; 108 | } else { 109 | // case 2: there are more lines to copy than invalid lines 110 | 111 | // decrement the nb of lines to copy by the number of invalid lines 112 | nb_lines -= *old_invalid_before; 113 | 114 | // increment new_invalid_lines by the same amount 115 | *new_invalid_before += *old_invalid_before; 116 | 117 | // we don't have any invalid lines left 118 | *old_invalid_before = 0; 119 | } 120 | 121 | // STEP 2: Handle the valid lines 122 | // ------------------------------------------------------------ 123 | 124 | let nb_valid_lines = old_lines.len(); 125 | let range; 126 | 127 | if nb_lines <= (nb_valid_lines as u64) { 128 | // case 1: the are more (or equal) valid lines than lines to copy 129 | 130 | // the range of lines to copy: from the start to nb_lines - 1; 131 | range = 0..nb_lines as usize; 132 | 133 | // after the copy, we won't have any line remaining to copy 134 | nb_lines = 0; 135 | } else { 136 | // case 2: there are more lines to copy than valid lines 137 | 138 | // we copy all the valid lines 139 | range = 0..nb_valid_lines; 140 | 141 | // after the operation we'll have (nb_lines - nb_valid_lines) left to copy 142 | nb_lines -= nb_valid_lines as u64; 143 | } 144 | 145 | // we'll only apply the copy if there actually are valid lines to copy 146 | if nb_valid_lines > 0 { 147 | let diff = if let Some(new_first_line_num) = first_line_num { 148 | // find the first "real" line (ie non-wrapped), and 149 | // compute the difference between its line number and 150 | // its *new* line number, given by the "copy" 151 | // operation. This will be used to update the line 152 | // number for all the lines we copy. 153 | old_lines 154 | .iter() 155 | .find_map(|line| { 156 | line.line_num 157 | .map(|num| new_first_line_num as i64 - num as i64) 158 | }) 159 | .unwrap_or(0) 160 | } else { 161 | // if the "copy" operation does not specify a new line 162 | // number, just set the diff to 0 163 | 0 164 | }; 165 | 166 | let copied_lines = old_lines.drain(range).map(|mut line| { 167 | line.line_num = line 168 | .line_num 169 | .map(|line_num| (line_num as i64 + diff) as u64); 170 | line 171 | }); 172 | new_lines.extend(copied_lines); 173 | } 174 | 175 | // if there are no more lines to copy we're done 176 | if nb_lines == 0 { 177 | return; 178 | } 179 | 180 | // STEP 3: Handle the remaining invalid lines 181 | // ------------------------------------------------------------ 182 | 183 | // We should have at least enough invalid lines to copy, otherwise it indicates there's a 184 | // problem, and we panic. 185 | if *old_invalid_after >= nb_lines { 186 | *old_invalid_after -= nb_lines; 187 | *new_invalid_after += nb_lines; 188 | } else { 189 | error!( 190 | "{} lines left to copy, but only {} lines in the old cache", 191 | nb_lines, *old_invalid_after 192 | ); 193 | panic!("cache update failed"); 194 | } 195 | } 196 | 197 | fn apply_skip(&mut self, nb_lines: u64) { 198 | debug!("skipping {} lines", nb_lines); 199 | 200 | let LineCache { 201 | invalid_before: ref mut old_invalid_before, 202 | lines: ref mut old_lines, 203 | invalid_after: ref mut old_invalid_after, 204 | } = self.old_cache; 205 | 206 | let mut nb_lines = nb_lines; 207 | 208 | // Skip invalid lines that come before the valid ones. 209 | if *old_invalid_before > nb_lines { 210 | *old_invalid_before -= nb_lines; 211 | return; 212 | } else if *old_invalid_before > 0 { 213 | nb_lines -= *old_invalid_before; 214 | *old_invalid_before = 0; 215 | } 216 | 217 | // Skip the valid lines 218 | let nb_valid_lines = old_lines.len(); 219 | if nb_lines < nb_valid_lines as u64 { 220 | old_lines.drain(0..nb_lines as usize).last(); 221 | return; 222 | } else { 223 | old_lines.drain(..).last(); 224 | nb_lines -= nb_valid_lines as u64; 225 | } 226 | 227 | // Skip the remaining invalid lines 228 | if *old_invalid_after >= nb_lines { 229 | *old_invalid_after -= nb_lines; 230 | return; 231 | } 232 | 233 | error!( 234 | "{} lines left to skip, but only {} lines in the old cache", 235 | nb_lines, *old_invalid_after 236 | ); 237 | panic!("cache update failed"); 238 | } 239 | 240 | fn apply_invalidate(&mut self, nb_lines: u64) { 241 | debug!("invalidating {} lines", nb_lines); 242 | if self.new_cache.lines.is_empty() { 243 | self.new_cache.invalid_before += nb_lines; 244 | } else { 245 | self.new_cache.invalid_after += nb_lines; 246 | } 247 | } 248 | 249 | fn apply_insert(&mut self, mut lines: Vec) { 250 | debug!("inserting {} lines", lines.len()); 251 | self.new_cache.lines.extend(lines.drain(..).map(|mut line| { 252 | trim_new_line(&mut line.text); 253 | line 254 | })); 255 | } 256 | 257 | fn apply_update(&mut self, nb_lines: u64, lines: Vec, first_line_num: Option) { 258 | debug!("updating {} lines", nb_lines); 259 | let old_lines = &mut self.old_cache.lines; 260 | let new_lines = &mut self.new_cache.lines; 261 | 262 | if nb_lines > old_lines.len() as u64 { 263 | error!( 264 | "{} lines to update, but only {} lines in cache", 265 | nb_lines, 266 | old_lines.len() 267 | ); 268 | panic!("failed to update the cache"); 269 | } 270 | 271 | let diff = if let Some(new_first_line_num) = first_line_num { 272 | old_lines 273 | .iter() 274 | .find_map(|line| { 275 | line.line_num 276 | .map(|num| new_first_line_num as i64 - num as i64) 277 | }) 278 | .unwrap_or(0) 279 | } else { 280 | 0 281 | }; 282 | 283 | new_lines.extend( 284 | old_lines 285 | .drain(0..nb_lines as usize) 286 | .zip(lines.into_iter()) 287 | .map(|(mut old_line, update)| { 288 | old_line.cursor = update.cursor; 289 | old_line.styles = update.styles; 290 | old_line.line_num = old_line 291 | .line_num 292 | .map(|line_num| (line_num as i64 + diff) as u64); 293 | old_line 294 | }), 295 | ) 296 | } 297 | 298 | fn update(&mut self, operations: Vec) { 299 | self.new_cache = LineCache::default(); 300 | 301 | trace!("updating the line cache"); 302 | trace!("cache state before: {:?}", self); 303 | trace!("operations to be applied: {:?}", &operations); 304 | 305 | for op in operations { 306 | debug!("operation: {:?}", &op); 307 | debug!("cache helper before operation {:?}", self); 308 | 309 | match op.operation_type { 310 | OperationType::Copy => self.apply_copy(op.nb_lines, op.line_num), 311 | OperationType::Skip => self.apply_skip(op.nb_lines), 312 | OperationType::Invalidate => self.apply_invalidate(op.nb_lines), 313 | OperationType::Insert => self.apply_insert(op.lines), 314 | OperationType::Update => self.apply_update(op.nb_lines, op.lines, op.line_num), 315 | } 316 | 317 | debug!("cache helper after operation {:?}", self); 318 | } 319 | 320 | std::mem::swap(self.old_cache, &mut self.new_cache); 321 | } 322 | } 323 | 324 | fn trim_new_line(text: &mut String) { 325 | if let Some('\n') = text.chars().last() { 326 | text.pop(); 327 | } 328 | } 329 | 330 | #[test] 331 | // This test simulates a simple edit operation on a LineCache. 332 | fn test_cache_edit() { 333 | let mut cache = LineCache { 334 | invalid_before: 0, 335 | lines: serde_json::from_str::>( 336 | r#" 337 | [ 338 | {"text":"line1", "ln":1}, 339 | {"text":"line2", "ln":2}, 340 | {"text":"line3", "ln":3}, 341 | {"text":"line4", "ln":4}, 342 | {"text":"line5", "ln":5} 343 | ] 344 | "#, 345 | ) 346 | .unwrap(), 347 | invalid_after: 0, 348 | }; 349 | 350 | let upd = Update { 351 | operations: serde_json::from_str::>( 352 | r#" 353 | [ 354 | {"op":"copy", "n":1}, 355 | {"op":"ins", "n":2, "lines": [ 356 | {"text":"new_line2", "ln":2}, 357 | {"text":"new_line3", "ln":3} 358 | ]}, 359 | {"op":"skip", "n":2}, 360 | {"op":"copy", "n":2} 361 | ] 362 | "#, 363 | ) 364 | .unwrap(), 365 | pristine: true, 366 | rev: None, 367 | view_id: std::str::FromStr::from_str("view-id-1").unwrap(), 368 | }; 369 | 370 | cache.update(upd); 371 | 372 | assert_eq!( 373 | cache.lines, 374 | serde_json::from_str::>( 375 | r#"[{"text":"line1", "ln":1}, 376 | {"text":"new_line2", "ln":2}, 377 | {"text":"new_line3", "ln":3}, 378 | {"text":"line4", "ln":4}, 379 | {"text":"line5", "ln":5}]"# 380 | ) 381 | .unwrap() 382 | ); 383 | } 384 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::ClientError; 2 | use crate::protocol; 3 | use crate::structs::{ModifySelection, ViewId}; 4 | use futures::{future, future::Either, Future}; 5 | use serde::Serialize; 6 | use serde_json::Value; 7 | use serde_json::{from_value, to_value, Map}; 8 | 9 | /// A client to send notifications and request to xi-core. 10 | #[derive(Clone)] 11 | pub struct Client(pub protocol::Client); 12 | 13 | fn get_edit_params( 14 | view_id: ViewId, 15 | method: &str, 16 | params: Option, 17 | ) -> Result { 18 | let params_value = if let Some(params) = params { 19 | to_value(params)? 20 | } else { 21 | json!([]) 22 | }; 23 | 24 | Ok(json!({ 25 | "method": method, 26 | "view_id": view_id, 27 | "params": params_value, 28 | })) 29 | } 30 | 31 | impl Client { 32 | /// Send a notification to the core. Most (if not all) notifications 33 | /// supported by the core are already implemented, so this method 34 | /// should not be necessary in most cases. 35 | pub fn notify( 36 | &self, 37 | method: &str, 38 | params: Value, 39 | ) -> impl Future { 40 | info!(">>> notification: method={}, params={}", method, ¶ms); 41 | self.0 42 | .notify(method, params) 43 | .map_err(|_| ClientError::NotifyFailed) 44 | } 45 | 46 | /// Send a request to the core. Most (if not all) notifications 47 | /// supported by the core are already implemented, so this method 48 | /// should not be necessary in most cases. 49 | pub fn request( 50 | &self, 51 | method: &str, 52 | params: Value, 53 | ) -> impl Future { 54 | info!(">>> request : method={}, params={}", method, ¶ms); 55 | self.0 56 | .request(method, params) 57 | .then(|response| match response { 58 | Ok(Ok(value)) => Ok(value), 59 | Ok(Err(value)) => Err(ClientError::ErrorReturned(value)), 60 | Err(_) => Err(ClientError::RequestFailed), 61 | }) 62 | } 63 | 64 | pub fn edit_request( 65 | &self, 66 | view_id: ViewId, 67 | method: &str, 68 | params: Option, 69 | ) -> impl Future { 70 | match get_edit_params(view_id, method, params) { 71 | Ok(value) => Either::A(self.request("edit", value)), 72 | Err(e) => Either::B(future::err(e)), 73 | } 74 | } 75 | 76 | /// Send an "edit" notification. Most (if not all) "edit" commands are 77 | /// already implemented, so this method should not be necessary in most 78 | /// cases. 79 | pub fn edit_notify( 80 | &self, 81 | view_id: ViewId, 82 | method: &str, 83 | params: Option, 84 | ) -> impl Future { 85 | match get_edit_params(view_id, method, params) { 86 | Ok(value) => Either::A(self.notify("edit", value)), 87 | Err(e) => Either::B(future::err(e)), 88 | } 89 | } 90 | 91 | /// Send an "scroll" notification 92 | /// ```ignore 93 | /// {"method":"edit","params":{"method":"scroll","params":[21,80], 94 | /// "view_id":"view-id-1"}} 95 | /// ``` 96 | pub fn scroll( 97 | &self, 98 | view_id: ViewId, 99 | first_line: u64, 100 | last_line: u64, 101 | ) -> impl Future { 102 | self.edit_notify(view_id, "scroll", Some(json!([first_line, last_line]))) 103 | } 104 | 105 | pub fn goto_line( 106 | &self, 107 | view_id: ViewId, 108 | line: u64, 109 | ) -> impl Future { 110 | self.edit_notify(view_id, "goto_line", Some(json!({ "line": line }))) 111 | } 112 | 113 | pub fn copy(&self, view_id: ViewId) -> impl Future { 114 | self.edit_request(view_id, "copy", None as Option) 115 | } 116 | 117 | pub fn paste( 118 | &self, 119 | view_id: ViewId, 120 | buffer: &str, 121 | ) -> impl Future { 122 | self.edit_notify(view_id, "paste", Some(json!({ "chars": buffer }))) 123 | } 124 | 125 | pub fn cut(&self, view_id: ViewId) -> impl Future { 126 | self.edit_request(view_id, "cut", None as Option) 127 | } 128 | 129 | pub fn undo(&self, view_id: ViewId) -> impl Future { 130 | self.edit_notify(view_id, "undo", None as Option) 131 | } 132 | 133 | pub fn redo(&self, view_id: ViewId) -> impl Future { 134 | self.edit_notify(view_id, "redo", None as Option) 135 | } 136 | 137 | pub fn find( 138 | &self, 139 | view_id: ViewId, 140 | search_term: &str, 141 | case_sensitive: bool, 142 | regex: bool, 143 | whole_words: bool, 144 | ) -> impl Future { 145 | self.edit_notify( 146 | view_id, 147 | "find", 148 | Some(json!({ 149 | "chars": search_term, 150 | "case_sensitive": case_sensitive, 151 | "regex": regex, 152 | "whole_words": whole_words})), 153 | ) 154 | } 155 | 156 | fn find_other( 157 | &self, 158 | view_id: ViewId, 159 | command: &str, 160 | wrap_around: bool, 161 | allow_same: bool, 162 | modify_selection: ModifySelection, 163 | ) -> impl Future { 164 | self.edit_notify( 165 | view_id, 166 | command, 167 | Some(json!({ 168 | "wrap_around": wrap_around, 169 | "allow_same": allow_same, 170 | "modify_selection": modify_selection})), 171 | ) 172 | } 173 | 174 | pub fn find_next( 175 | &self, 176 | view_id: ViewId, 177 | wrap_around: bool, 178 | allow_same: bool, 179 | modify_selection: ModifySelection, 180 | ) -> impl Future { 181 | self.find_other( 182 | view_id, 183 | "find_next", 184 | wrap_around, 185 | allow_same, 186 | modify_selection, 187 | ) 188 | } 189 | 190 | pub fn find_prev( 191 | &self, 192 | view_id: ViewId, 193 | wrap_around: bool, 194 | allow_same: bool, 195 | modify_selection: ModifySelection, 196 | ) -> impl Future { 197 | self.find_other( 198 | view_id, 199 | "find_previous", 200 | wrap_around, 201 | allow_same, 202 | modify_selection, 203 | ) 204 | } 205 | 206 | pub fn find_all(&self, view_id: ViewId) -> impl Future { 207 | self.edit_notify(view_id, "find_all", None as Option) 208 | } 209 | 210 | pub fn highlight_find( 211 | &self, 212 | view_id: ViewId, 213 | visible: bool, 214 | ) -> impl Future { 215 | self.edit_notify( 216 | view_id, 217 | "highlight_find", 218 | Some(json!({ "visible": visible })), 219 | ) 220 | } 221 | 222 | pub fn left(&self, view_id: ViewId) -> impl Future { 223 | self.edit_notify(view_id, "move_left", None as Option) 224 | } 225 | 226 | pub fn left_sel(&self, view_id: ViewId) -> impl Future { 227 | self.edit_notify( 228 | view_id, 229 | "move_left_and_modify_selection", 230 | None as Option, 231 | ) 232 | } 233 | 234 | pub fn right(&self, view_id: ViewId) -> impl Future { 235 | self.edit_notify(view_id, "move_right", None as Option) 236 | } 237 | 238 | pub fn right_sel(&self, view_id: ViewId) -> impl Future { 239 | self.edit_notify( 240 | view_id, 241 | "move_right_and_modify_selection", 242 | None as Option, 243 | ) 244 | } 245 | 246 | pub fn up(&self, view_id: ViewId) -> impl Future { 247 | self.edit_notify(view_id, "move_up", None as Option) 248 | } 249 | 250 | pub fn up_sel(&self, view_id: ViewId) -> impl Future { 251 | self.edit_notify( 252 | view_id, 253 | "move_up_and_modify_selection", 254 | None as Option, 255 | ) 256 | } 257 | 258 | pub fn down(&self, view_id: ViewId) -> impl Future { 259 | self.edit_notify(view_id, "move_down", None as Option) 260 | } 261 | 262 | pub fn down_sel(&self, view_id: ViewId) -> impl Future { 263 | self.edit_notify( 264 | view_id, 265 | "move_down_and_modify_selection", 266 | None as Option, 267 | ) 268 | } 269 | 270 | pub fn backspace(&self, view_id: ViewId) -> impl Future { 271 | self.del(view_id) 272 | } 273 | 274 | pub fn delete(&self, view_id: ViewId) -> impl Future { 275 | self.edit_notify(view_id, "delete_forward", None as Option) 276 | } 277 | 278 | pub fn del(&self, view_id: ViewId) -> impl Future { 279 | self.edit_notify(view_id, "delete_backward", None as Option) 280 | } 281 | 282 | pub fn delete_word_backward( 283 | &self, 284 | view_id: ViewId, 285 | ) -> impl Future { 286 | self.edit_notify(view_id, "delete_word_backward", None as Option) 287 | } 288 | 289 | pub fn page_up(&self, view_id: ViewId) -> impl Future { 290 | self.edit_notify(view_id, "scroll_page_up", None as Option) 291 | } 292 | 293 | pub fn page_up_sel(&self, view_id: ViewId) -> impl Future { 294 | self.edit_notify( 295 | view_id, 296 | "page_up_and_modify_selection", 297 | None as Option, 298 | ) 299 | } 300 | 301 | pub fn page_down(&self, view_id: ViewId) -> impl Future { 302 | self.edit_notify(view_id, "scroll_page_down", None as Option) 303 | } 304 | 305 | pub fn page_down_sel(&self, view_id: ViewId) -> impl Future { 306 | self.edit_notify( 307 | view_id, 308 | "page_down_and_modify_selection", 309 | None as Option, 310 | ) 311 | } 312 | 313 | pub fn line_start(&self, view_id: ViewId) -> impl Future { 314 | self.edit_notify(view_id, "move_to_left_end_of_line", None as Option) 315 | } 316 | 317 | pub fn line_start_sel(&self, view_id: ViewId) -> impl Future { 318 | self.edit_notify( 319 | view_id, 320 | "move_to_left_end_of_line_and_modify_selection", 321 | None as Option, 322 | ) 323 | } 324 | 325 | pub fn line_end(&self, view_id: ViewId) -> impl Future { 326 | self.edit_notify(view_id, "move_to_right_end_of_line", None as Option) 327 | } 328 | 329 | pub fn line_end_sel(&self, view_id: ViewId) -> impl Future { 330 | self.edit_notify( 331 | view_id, 332 | "move_to_right_end_of_line_and_modify_selection", 333 | None as Option, 334 | ) 335 | } 336 | 337 | pub fn document_begin(&self, view_id: ViewId) -> impl Future { 338 | self.edit_notify( 339 | view_id, 340 | "move_to_beginning_of_document", 341 | None as Option, 342 | ) 343 | } 344 | 345 | pub fn document_begin_sel( 346 | &self, 347 | view_id: ViewId, 348 | ) -> impl Future { 349 | self.edit_notify( 350 | view_id, 351 | "move_to_beginning_of_document_and_modify_selection", 352 | None as Option, 353 | ) 354 | } 355 | 356 | pub fn document_end(&self, view_id: ViewId) -> impl Future { 357 | self.edit_notify(view_id, "move_to_end_of_document", None as Option) 358 | } 359 | 360 | pub fn document_end_sel(&self, view_id: ViewId) -> impl Future { 361 | self.edit_notify( 362 | view_id, 363 | "move_to_end_of_document_and_modify_selection", 364 | None as Option, 365 | ) 366 | } 367 | 368 | pub fn select_all(&self, view_id: ViewId) -> impl Future { 369 | self.edit_notify(view_id, "select_all", None as Option) 370 | } 371 | 372 | pub fn collapse_selections( 373 | &self, 374 | view_id: ViewId, 375 | ) -> impl Future { 376 | self.edit_notify(view_id, "collapse_selections", None as Option) 377 | } 378 | 379 | pub fn insert( 380 | &self, 381 | view_id: ViewId, 382 | string: &str, 383 | ) -> impl Future { 384 | self.edit_notify(view_id, "insert", Some(json!({ "chars": string }))) 385 | } 386 | 387 | pub fn insert_newline(&self, view_id: ViewId) -> impl Future { 388 | self.edit_notify(view_id, "insert_newline", None as Option) 389 | } 390 | 391 | pub fn insert_tab(&self, view_id: ViewId) -> impl Future { 392 | self.edit_notify(view_id, "insert_tab", None as Option) 393 | } 394 | 395 | pub fn f1(&self, view_id: ViewId) -> impl Future { 396 | self.edit_notify(view_id, "debug_rewrap", None as Option) 397 | } 398 | 399 | pub fn f2(&self, view_id: ViewId) -> impl Future { 400 | self.edit_notify(view_id, "debug_test_fg_spans", None as Option) 401 | } 402 | 403 | pub fn char(&self, view_id: ViewId, ch: char) -> impl Future { 404 | self.edit_notify(view_id, "insert", Some(json!({ "chars": ch }))) 405 | } 406 | 407 | // FIXME: handle modifier and click count 408 | pub fn click( 409 | &self, 410 | view_id: ViewId, 411 | line: u64, 412 | column: u64, 413 | ) -> impl Future { 414 | self.edit_notify(view_id, "click", Some(json!([line, column, 0, 1]))) 415 | } 416 | 417 | pub fn click_point_select( 418 | &self, 419 | view_id: ViewId, 420 | line: u64, 421 | column: u64, 422 | ) -> impl Future { 423 | let ty = "point_select"; 424 | self.edit_notify( 425 | view_id, 426 | "gesture", 427 | Some(json!({"line": line, "col": column, "ty": ty,})), 428 | ) 429 | } 430 | 431 | pub fn click_toggle_sel( 432 | &self, 433 | view_id: ViewId, 434 | line: u64, 435 | column: u64, 436 | ) -> impl Future { 437 | let ty = "toggle_sel"; 438 | self.edit_notify( 439 | view_id, 440 | "gesture", 441 | Some(json!({"line": line, "col": column, "ty": ty,})), 442 | ) 443 | } 444 | 445 | pub fn click_range_select( 446 | &self, 447 | view_id: ViewId, 448 | line: u64, 449 | column: u64, 450 | ) -> impl Future { 451 | let ty = "range_select"; 452 | self.edit_notify( 453 | view_id, 454 | "gesture", 455 | Some(json!({"line": line, "col": column, "ty": ty,})), 456 | ) 457 | } 458 | 459 | pub fn click_line_select( 460 | &self, 461 | view_id: ViewId, 462 | line: u64, 463 | column: u64, 464 | ) -> impl Future { 465 | let ty = "range_select"; 466 | self.edit_notify( 467 | view_id, 468 | "gesture", 469 | Some(json!({"line": line, "col": column, "ty": ty,})), 470 | ) 471 | } 472 | 473 | pub fn click_word_select( 474 | &self, 475 | view_id: ViewId, 476 | line: u64, 477 | column: u64, 478 | ) -> impl Future { 479 | let ty = "word_select"; 480 | self.edit_notify( 481 | view_id, 482 | "gesture", 483 | Some(json!({"line": line, "col": column, "ty": ty,})), 484 | ) 485 | } 486 | 487 | pub fn click_multi_line_select( 488 | &self, 489 | view_id: ViewId, 490 | line: u64, 491 | column: u64, 492 | ) -> impl Future { 493 | let ty = "multi_line_select"; 494 | self.edit_notify( 495 | view_id, 496 | "gesture", 497 | Some(json!({"line": line, "col": column, "ty": ty,})), 498 | ) 499 | } 500 | 501 | pub fn click_multi_word_select( 502 | &self, 503 | view_id: ViewId, 504 | line: u64, 505 | column: u64, 506 | ) -> impl Future { 507 | let ty = "multi_word_select"; 508 | self.edit_notify( 509 | view_id, 510 | "gesture", 511 | Some(json!({"line": line, "col": column, "ty": ty,})), 512 | ) 513 | } 514 | 515 | pub fn drag( 516 | &self, 517 | view_id: ViewId, 518 | line: u64, 519 | column: u64, 520 | ) -> impl Future { 521 | self.edit_notify(view_id, "drag", Some(json!([line, column, 0]))) 522 | } 523 | 524 | /// send a `"new_view"` request to the core. 525 | /// ```ignore 526 | /// {"id":1,"method":"new_view","params":{"file_path":"foo/test.txt"}} 527 | /// ``` 528 | pub fn new_view( 529 | &self, 530 | file_path: Option, 531 | ) -> impl Future { 532 | let params = if let Some(file_path) = file_path { 533 | json!({ "file_path": file_path }) 534 | } else { 535 | json!({}) 536 | }; 537 | self.request("new_view", params) 538 | .and_then(|result| from_value::(result).map_err(From::from)) 539 | } 540 | 541 | /// send a `"close_view"` notifycation to the core. 542 | pub fn close_view(&self, view_id: ViewId) -> impl Future { 543 | self.notify("close_view", json!({ "view_id": view_id })) 544 | } 545 | 546 | pub fn save( 547 | &self, 548 | view_id: ViewId, 549 | file_path: &str, 550 | ) -> impl Future { 551 | let params = json!({"view_id": view_id, "file_path": file_path}); 552 | self.notify("save", params).and_then(|_| Ok(())) 553 | } 554 | 555 | pub fn set_theme(&self, theme: &str) -> impl Future { 556 | let params = json!({ "theme_name": theme }); 557 | self.notify("set_theme", params).and_then(|_| Ok(())) 558 | } 559 | 560 | pub fn client_started( 561 | &self, 562 | config_dir: Option<&str>, 563 | client_extras_dir: Option<&str>, 564 | ) -> impl Future { 565 | let mut params = Map::new(); 566 | if let Some(path) = config_dir { 567 | let _ = params.insert("config_dir".into(), json!(path)); 568 | } 569 | if let Some(path) = client_extras_dir { 570 | let _ = params.insert("client_extras_dir".into(), json!(path)); 571 | } 572 | self.notify("client_started", params.into()) 573 | } 574 | 575 | pub fn start_plugin( 576 | &self, 577 | view_id: ViewId, 578 | name: &str, 579 | ) -> impl Future { 580 | let params = json!({"view_id": view_id, "plugin_name": name}); 581 | self.notify("start", params).and_then(|_| Ok(())) 582 | } 583 | 584 | pub fn stop_plugin( 585 | &self, 586 | view_id: ViewId, 587 | name: &str, 588 | ) -> impl Future { 589 | let params = json!({"view_id": view_id, "plugin_name": name}); 590 | self.notify("stop", params).and_then(|_| Ok(())) 591 | } 592 | 593 | pub fn notify_plugin( 594 | &self, 595 | view_id: ViewId, 596 | plugin: &str, 597 | method: &str, 598 | params: &Value, 599 | ) -> impl Future { 600 | let params = json!({ 601 | "view_id": view_id, 602 | "receiver": plugin, 603 | "notification": { 604 | "method": method, 605 | "params": params, 606 | } 607 | }); 608 | self.notify("plugin_rpc", params).and_then(|_| Ok(())) 609 | } 610 | 611 | pub fn outdent(&self, view_id: ViewId) -> impl Future { 612 | self.edit_notify(view_id, "outdent", None as Option) 613 | } 614 | 615 | pub fn move_word_left(&self, view_id: ViewId) -> impl Future { 616 | self.edit_notify(view_id, "move_word_left", None as Option) 617 | } 618 | 619 | pub fn move_word_right(&self, view_id: ViewId) -> impl Future { 620 | self.edit_notify(view_id, "move_word_right", None as Option) 621 | } 622 | 623 | pub fn move_word_left_sel( 624 | &self, 625 | view_id: ViewId, 626 | ) -> impl Future { 627 | self.edit_notify( 628 | view_id, 629 | "move_word_left_and_modify_selection", 630 | None as Option, 631 | ) 632 | } 633 | 634 | pub fn move_word_right_sel( 635 | &self, 636 | view_id: ViewId, 637 | ) -> impl Future { 638 | self.edit_notify( 639 | view_id, 640 | "move_word_right_and_modify_selection", 641 | None as Option, 642 | ) 643 | } 644 | 645 | pub fn resize( 646 | &self, 647 | view_id: ViewId, 648 | width: i32, 649 | height: i32, 650 | ) -> impl Future { 651 | self.edit_notify( 652 | view_id, 653 | "resize", 654 | Some(json!({ 655 | "width": width, 656 | "height": height, 657 | })), 658 | ) 659 | } 660 | 661 | pub fn replace( 662 | &self, 663 | view_id: ViewId, 664 | chars: &str, 665 | preserve_case: bool, 666 | ) -> impl Future { 667 | self.edit_notify( 668 | view_id, 669 | "replace", 670 | Some(json!({ 671 | "chars": chars, 672 | "preserve_case": preserve_case, 673 | })), 674 | ) 675 | } 676 | 677 | pub fn replace_next(&self, view_id: ViewId) -> impl Future { 678 | self.edit_notify(view_id, "replace_next", None as Option) 679 | } 680 | 681 | pub fn replace_all(&self, view_id: ViewId) -> impl Future { 682 | self.edit_notify(view_id, "replace_all", None as Option) 683 | } 684 | 685 | pub fn set_language( 686 | &self, 687 | view_id: ViewId, 688 | lang_name: &str, 689 | ) -> impl Future { 690 | self.notify( 691 | "set_language", 692 | json!({ "view_id": view_id, "language_id": lang_name }), 693 | ) 694 | } 695 | 696 | pub fn selection_for_find( 697 | &self, 698 | view_id: ViewId, 699 | case_sensitive: bool, 700 | ) -> impl Future { 701 | self.notify( 702 | "selection_for_find", 703 | json!({ "view_id": view_id, "case_sensitive": case_sensitive }), 704 | ) 705 | } 706 | 707 | pub fn selection_for_replace( 708 | &self, 709 | view_id: ViewId, 710 | case_sensitive: bool, 711 | ) -> impl Future { 712 | self.notify( 713 | "selection_for_replace", 714 | json!({ "view_id": view_id, "case_sensitive": case_sensitive }), 715 | ) 716 | } 717 | 718 | pub fn selection_into_lines( 719 | &self, 720 | view_id: ViewId, 721 | ) -> impl Future { 722 | self.notify("selection_into_lines", json!({ "view_id": view_id })) 723 | } 724 | 725 | //TODO: Use something more elegant than a `Value` 726 | pub fn modify_user_config( 727 | &self, 728 | domain: &str, 729 | changes: Value, 730 | ) -> impl Future { 731 | self.notify( 732 | "modify_user_config", 733 | json!({ 734 | "domain": domain, 735 | "changes": changes, 736 | }), 737 | ) 738 | } 739 | 740 | pub fn request_lines( 741 | &self, 742 | view_id: ViewId, 743 | first_line: u64, 744 | last_line: u64, 745 | ) -> impl Future { 746 | self.edit_notify( 747 | view_id, 748 | "request_lines", 749 | Some(json!([first_line, last_line])), 750 | ) 751 | } 752 | 753 | pub fn shutdown(&self) { 754 | self.0.shutdown() 755 | } 756 | 757 | // TODO: requests for plugin_rpc 758 | } 759 | -------------------------------------------------------------------------------- /src/core.rs: -------------------------------------------------------------------------------- 1 | use crate::client::Client; 2 | use crate::frontend::{Frontend, FrontendBuilder}; 3 | use crate::protocol::Endpoint; 4 | use crate::ClientError; 5 | use bytes::BytesMut; 6 | use futures::{Future, Poll, Stream}; 7 | use std::io::{self, Read, Write}; 8 | use std::process::Command; 9 | use std::process::Stdio; 10 | use tokio::io::{AsyncRead, AsyncWrite}; 11 | use tokio_codec::{Decoder, FramedRead}; 12 | use tokio_process::{Child, ChildStderr, ChildStdin, ChildStdout, CommandExt}; 13 | 14 | struct Core { 15 | #[allow(dead_code)] 16 | core: Child, 17 | stdout: ChildStdout, 18 | stdin: ChildStdin, 19 | } 20 | 21 | impl Read for Core { 22 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 23 | self.stdout.read(buf) 24 | } 25 | } 26 | 27 | impl AsyncRead for Core { 28 | // FIXME: do I actually have to implement this? 29 | unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { 30 | self.stdout.prepare_uninitialized_buffer(buf) 31 | } 32 | } 33 | 34 | impl Write for Core { 35 | fn write(&mut self, buf: &[u8]) -> io::Result { 36 | self.stdin.write(buf) 37 | } 38 | 39 | fn flush(&mut self) -> io::Result<()> { 40 | self.stdin.flush() 41 | } 42 | } 43 | 44 | impl AsyncWrite for Core { 45 | fn shutdown(&mut self) -> Poll<(), io::Error> { 46 | self.stdin.shutdown() 47 | } 48 | } 49 | 50 | /// Start Xi core, and spawn an RPC client on the current tokio executor. 51 | /// 52 | /// # Panics 53 | /// 54 | /// This function calls 55 | /// [`tokio::spawn`](https://docs.rs/tokio/0.1.21/tokio/executor/fn.spawn.html) 56 | /// so it will panic if the default executor is not set or if spawning 57 | /// onto the default executor returns an error. 58 | pub fn spawn(executable: &str, builder: B) -> Result<(Client, CoreStderr), ClientError> 59 | where 60 | F: Frontend + 'static + Send, 61 | B: FrontendBuilder + 'static, 62 | { 63 | spawn_command(Command::new(executable), builder) 64 | } 65 | 66 | /// Same as [`spawn`] but accepts an arbitrary [`std::process::Command`]. 67 | pub fn spawn_command( 68 | mut command: Command, 69 | builder: B, 70 | ) -> Result<(Client, CoreStderr), ClientError> 71 | where 72 | F: Frontend + 'static + Send, 73 | B: FrontendBuilder + 'static, 74 | { 75 | info!("starting xi-core"); 76 | let mut xi_core = command 77 | .stdout(Stdio::piped()) 78 | .stdin(Stdio::piped()) 79 | .stderr(Stdio::piped()) 80 | .env("RUST_BACKTRACE", "1") 81 | .spawn_async()?; 82 | 83 | let stdout = xi_core.stdout().take().unwrap(); 84 | let stdin = xi_core.stdin().take().unwrap(); 85 | let stderr = xi_core.stderr().take().unwrap(); 86 | let core = Core { 87 | core: xi_core, 88 | stdout, 89 | stdin, 90 | }; 91 | 92 | let (endpoint, client) = Endpoint::new(core, builder); 93 | 94 | info!("spawning the Xi-RPC endpoint"); 95 | // XXX: THIS PANICS IF THE DEFAULT EXECUTOR IS NOT SET 96 | tokio::spawn(endpoint.map_err(|e| error!("Endpoint exited with an error: {:?}", e))); 97 | 98 | Ok((Client(client), CoreStderr::new(stderr))) 99 | } 100 | 101 | pub struct LineCodec; 102 | 103 | // straight from 104 | // https://github.com/tokio-rs/tokio-line/blob/master/simple/src/lib.rs 105 | impl Decoder for LineCodec { 106 | type Item = String; 107 | type Error = io::Error; 108 | 109 | fn decode(&mut self, buf: &mut BytesMut) -> Result, io::Error> { 110 | if let Some(n) = buf.as_ref().iter().position(|b| *b == b'\n') { 111 | let line = buf.split_to(n); 112 | buf.split_to(1); 113 | return match ::std::str::from_utf8(line.as_ref()) { 114 | Ok(s) => Ok(Some(s.to_string())), 115 | Err(_) => Err(io::Error::new(io::ErrorKind::Other, "invalid string")), 116 | }; 117 | } 118 | Ok(None) 119 | } 120 | } 121 | 122 | /// A stream of Xi core stderr lines 123 | pub struct CoreStderr(FramedRead); 124 | 125 | impl CoreStderr { 126 | fn new(stderr: ChildStderr) -> Self { 127 | CoreStderr(FramedRead::new(stderr, LineCodec {})) 128 | } 129 | } 130 | 131 | impl Stream for CoreStderr { 132 | type Item = String; 133 | type Error = io::Error; 134 | 135 | fn poll(&mut self) -> Poll, Self::Error> { 136 | self.0.poll() 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use serde_json::error::Error as SerdeError; 2 | use serde_json::Value; 3 | use std::error; 4 | use std::fmt; 5 | use std::io::Error as IoError; 6 | 7 | #[derive(Debug)] 8 | pub enum ClientError { 9 | /// A notification was not sent due to an internal error. 10 | NotifyFailed, 11 | /// A request failed due to an internal error. 12 | RequestFailed, 13 | 14 | /// A request or a notification could not be sent due to a 15 | /// serialization error. 16 | SerializeFailed(SerdeError), 17 | 18 | /// The server response is an error 19 | ErrorReturned(Value), 20 | 21 | /// We failed to spawn xi-core, e.g. because it's not installed, the binary is faulty, etc. 22 | CoreSpawnFailed(IoError), 23 | } 24 | 25 | impl fmt::Display for ClientError { 26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 | match *self { 28 | ClientError::NotifyFailed => write!(f, "Failed to send a notification"), 29 | ClientError::RequestFailed => { 30 | write!(f, "Failed to send a request, or receive its response") 31 | } 32 | ClientError::ErrorReturned(ref value) => { 33 | write!(f, "The core returned an error: {:?}", value) 34 | } 35 | ClientError::SerializeFailed(ref e) => { 36 | write!(f, "failed to serialize a message: {}", e) 37 | } 38 | ClientError::CoreSpawnFailed(ref s) => { 39 | write!(f, "Failed to spawn xi-core due to error: {}", s) 40 | } 41 | } 42 | } 43 | } 44 | 45 | impl error::Error for ClientError { 46 | fn description(&self) -> &str { 47 | match *self { 48 | ClientError::NotifyFailed => "Failed to send a notification", 49 | ClientError::RequestFailed => "Failed to send a request or receive its response", 50 | ClientError::ErrorReturned(_) => "The core answered with an error", 51 | ClientError::SerializeFailed(_) => "Failed to serialize message", 52 | ClientError::CoreSpawnFailed(_) => "Failed to spawn xi-core", 53 | } 54 | } 55 | 56 | fn cause(&self) -> Option<&dyn error::Error> { 57 | match *self { 58 | ClientError::SerializeFailed(ref serde_error) => Some(serde_error), 59 | ClientError::CoreSpawnFailed(ref io_error) => Some(io_error), 60 | _ => None, 61 | } 62 | } 63 | } 64 | 65 | impl From for ClientError { 66 | fn from(err: SerdeError) -> Self { 67 | ClientError::SerializeFailed(err) 68 | } 69 | } 70 | 71 | impl From for ClientError { 72 | fn from(err: IoError) -> Self { 73 | ClientError::CoreSpawnFailed(err) 74 | } 75 | } 76 | 77 | #[derive(Debug)] 78 | pub enum ServerError { 79 | UnknownMethod(String), 80 | DeserializeFailed(SerdeError), 81 | Other(String), 82 | } 83 | 84 | impl fmt::Display for ServerError { 85 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 86 | match *self { 87 | ServerError::UnknownMethod(ref method) => write!(f, "Unkown method {}", method), 88 | ServerError::Other(ref s) => write!(f, "Unkown error: {}", s), 89 | ServerError::DeserializeFailed(ref e) => write!( 90 | f, 91 | "Failed to deserialize the parameters of a request or notification: {}", 92 | e 93 | ), 94 | } 95 | } 96 | } 97 | 98 | impl error::Error for ServerError { 99 | fn description(&self) -> &str { 100 | match *self { 101 | ServerError::UnknownMethod(_) => "Unkown method", 102 | ServerError::Other(_) => "Unknown error", 103 | ServerError::DeserializeFailed(_) => { 104 | "Failed to deserialize the parameters of a request or notification" 105 | } 106 | } 107 | } 108 | 109 | fn cause(&self) -> Option<&dyn error::Error> { 110 | if let ServerError::DeserializeFailed(ref serde_error) = *self { 111 | Some(serde_error) 112 | } else { 113 | None 114 | } 115 | } 116 | } 117 | 118 | impl From for ServerError { 119 | fn from(s: String) -> Self { 120 | ServerError::Other(s) 121 | } 122 | } 123 | 124 | impl<'a> From<&'a str> for ServerError { 125 | fn from(s: &'a str) -> Self { 126 | ServerError::Other(s.to_string()) 127 | } 128 | } 129 | 130 | impl From for ServerError { 131 | fn from(err: SerdeError) -> Self { 132 | ServerError::DeserializeFailed(err) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/frontend.rs: -------------------------------------------------------------------------------- 1 | use crate::client::Client; 2 | use crate::protocol::{Client as InnerClient, IntoStaticFuture, Service, ServiceBuilder}; 3 | use crate::structs::{ 4 | Alert, AvailableLanguages, AvailablePlugins, AvailableThemes, ConfigChanged, FindStatus, 5 | LanguageChanged, MeasureWidth, PluginStarted, PluginStoped, ReplaceStatus, ScrollTo, Style, 6 | ThemeChanged, Update, UpdateCmds, 7 | }; 8 | use futures::{ 9 | future::{self, Either, FutureResult}, 10 | Future, 11 | }; 12 | use serde_json::{from_value, to_value, Value}; 13 | 14 | /// Represents all possible RPC messages recieved from xi-core. 15 | #[derive(Debug)] 16 | pub enum XiNotification { 17 | Update(Update), 18 | ScrollTo(ScrollTo), 19 | DefStyle(Style), 20 | AvailablePlugins(AvailablePlugins), 21 | UpdateCmds(UpdateCmds), 22 | PluginStarted(PluginStarted), 23 | PluginStoped(PluginStoped), 24 | ConfigChanged(ConfigChanged), 25 | ThemeChanged(ThemeChanged), 26 | Alert(Alert), 27 | AvailableThemes(AvailableThemes), 28 | FindStatus(FindStatus), 29 | ReplaceStatus(ReplaceStatus), 30 | AvailableLanguages(AvailableLanguages), 31 | LanguageChanged(LanguageChanged), 32 | } 33 | 34 | /// The `Frontend` trait must be implemented by clients. It defines how the 35 | /// client handles notifications and requests coming from `xi-core`. 36 | pub trait Frontend { 37 | type NotificationResult: IntoStaticFuture; 38 | fn handle_notification(&mut self, notification: XiNotification) -> Self::NotificationResult; 39 | 40 | type MeasureWidthResult: IntoStaticFuture>, Error = ()>; 41 | fn handle_measure_width(&mut self, request: MeasureWidth) -> Self::MeasureWidthResult; 42 | } 43 | 44 | /// A trait to build a type that implements `Frontend`. 45 | pub trait FrontendBuilder { 46 | /// The type to build 47 | type Frontend: Frontend; 48 | 49 | /// Build the frontend with the given client. 50 | fn build(self, client: Client) -> Self::Frontend; 51 | } 52 | 53 | impl ServiceBuilder for B 54 | where 55 | B: FrontendBuilder, 56 | B::Frontend: Send, 57 | { 58 | type Service = B::Frontend; 59 | 60 | fn build(self, client: InnerClient) -> B::Frontend { 61 | ::build(self, Client(client)) 62 | } 63 | } 64 | 65 | impl Service for F { 66 | type T = Value; 67 | type E = Value; 68 | type RequestFuture = Box + 'static + Send>; 69 | type NotificationFuture = Either< 70 | <::NotificationResult as IntoStaticFuture>::Future, 71 | FutureResult<(), ()>, 72 | >; 73 | 74 | fn handle_request(&mut self, method: &str, params: Value) -> Self::RequestFuture { 75 | info!("<<< request: method={}, params={}", method, ¶ms); 76 | match method { 77 | "measure_width" => { 78 | match from_value::(params) { 79 | Ok(request) => { 80 | let future = self 81 | .handle_measure_width(request) 82 | .into_static_future() 83 | .map(|response| { 84 | // TODO: justify why this can't fail 85 | // https://docs.serde.rs/serde_json/value/fn.to_value.html#errors 86 | to_value(response).expect("failed to convert response") 87 | }) 88 | .map_err(|_| panic!("errors are not supported")); 89 | Box::new(future) 90 | } 91 | Err(e) => { 92 | warn!("failed to deserialize measure_width message: {:?}", e); 93 | let err_msg = to_value("invalid measure_width message") 94 | // TODO: justify why string serialization cannot fail 95 | .expect("failed to serialize string"); 96 | Box::new(future::err(err_msg)) 97 | } 98 | } 99 | } 100 | _ => { 101 | let err_msg = to_value(format!("unknown method \"{}\"", method)) 102 | // TODO: justify why string serialization cannot fail 103 | .expect("failed to serialize string"); 104 | Box::new(future::err(err_msg)) 105 | } 106 | } 107 | } 108 | 109 | #[allow(clippy::cognitive_complexity)] 110 | fn handle_notification(&mut self, method: &str, params: Value) -> Self::NotificationFuture { 111 | info!("<<< notification: method={}, params={}", method, ¶ms); 112 | match method { 113 | "update" => match from_value::(params) { 114 | Ok(update) => Either::A( 115 | self.handle_notification(XiNotification::Update(update)) 116 | .into_static_future(), 117 | ), 118 | Err(e) => { 119 | error!("received invalid update notification: {:?}", e); 120 | Either::B(future::err(())) 121 | } 122 | }, 123 | 124 | "scroll_to" => match from_value::(params) { 125 | Ok(scroll_to) => Either::A( 126 | self.handle_notification(XiNotification::ScrollTo(scroll_to)) 127 | .into_static_future(), 128 | ), 129 | Err(e) => { 130 | error!("received invalid scroll_to notification: {:?}", e); 131 | Either::B(future::err(())) 132 | } 133 | }, 134 | 135 | "def_style" => match from_value::