xi_core_lib/rpc.rs
1// Copyright 2016 The xi-editor Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! The main RPC protocol, for communication between `xi-core` and the client.
16//!
17//! We rely on [Serde] for serialization and deserialization between
18//! the JSON-RPC protocol and the types here.
19//!
20//! [Serde]: https://serde.rs
21use std::path::PathBuf;
22
23use serde::de::{self, Deserialize, Deserializer};
24use serde::ser::{self, Serialize, Serializer};
25use serde_json::{self, Value};
26
27use crate::config::{ConfigDomainExternal, Table};
28use crate::plugins::PlaceholderRpc;
29use crate::syntax::LanguageId;
30use crate::tabs::ViewId;
31use crate::view::Size;
32
33// =============================================================================
34// Command types
35// =============================================================================
36
37#[derive(Serialize, Deserialize, Debug, PartialEq)]
38#[doc(hidden)]
39pub struct EmptyStruct {}
40
41/// The notifications which make up the base of the protocol.
42///
43/// # Note
44///
45/// For serialization, all identifiers are converted to "snake_case".
46///
47/// # Examples
48///
49/// The `close_view` command:
50///
51/// ```
52/// # extern crate xi_core_lib as xi_core;
53/// extern crate serde_json;
54/// # fn main() {
55/// use crate::xi_core::rpc::CoreNotification;
56///
57/// let json = r#"{
58/// "method": "close_view",
59/// "params": { "view_id": "view-id-1" }
60/// }"#;
61///
62/// let cmd: CoreNotification = serde_json::from_str(&json).unwrap();
63/// match cmd {
64/// CoreNotification::CloseView { .. } => (), // expected
65/// other => panic!("Unexpected variant"),
66/// }
67/// # }
68/// ```
69///
70/// The `client_started` command:
71///
72/// ```
73/// # extern crate xi_core_lib as xi_core;
74/// extern crate serde_json;
75/// # fn main() {
76/// use crate::xi_core::rpc::CoreNotification;
77///
78/// let json = r#"{
79/// "method": "client_started",
80/// "params": {}
81/// }"#;
82///
83/// let cmd: CoreNotification = serde_json::from_str(&json).unwrap();
84/// match cmd {
85/// CoreNotification::ClientStarted { .. } => (), // expected
86/// other => panic!("Unexpected variant"),
87/// }
88/// # }
89/// ```
90#[derive(Serialize, Deserialize, Debug, PartialEq)]
91#[serde(rename_all = "snake_case")]
92#[serde(tag = "method", content = "params")]
93pub enum CoreNotification {
94 /// The 'edit' namespace, for view-specific editor actions.
95 ///
96 /// The params object has internal `method` and `params` members,
97 /// which are parsed into the appropriate `EditNotification`.
98 ///
99 /// # Note:
100 ///
101 /// All edit commands (notifications and requests) include in their
102 /// inner params object a `view_id` field. On the xi-core side, we
103 /// pull out this value during parsing, and use it for routing.
104 ///
105 /// For more on the edit commands, see [`EditNotification`] and
106 /// [`EditRequest`].
107 ///
108 /// [`EditNotification`]: enum.EditNotification.html
109 /// [`EditRequest`]: enum.EditRequest.html
110 ///
111 /// # Examples
112 ///
113 /// ```
114 /// # extern crate xi_core_lib as xi_core;
115 /// #[macro_use]
116 /// extern crate serde_json;
117 /// use crate::xi_core::rpc::*;
118 /// # fn main() {
119 /// let edit = EditCommand {
120 /// view_id: 1.into(),
121 /// cmd: EditNotification::Insert { chars: "hello!".into() },
122 /// };
123 /// let rpc = CoreNotification::Edit(edit);
124 /// let expected = json!({
125 /// "method": "edit",
126 /// "params": {
127 /// "method": "insert",
128 /// "view_id": "view-id-1",
129 /// "params": {
130 /// "chars": "hello!",
131 /// }
132 /// }
133 /// });
134 /// assert_eq!(serde_json::to_value(&rpc).unwrap(), expected);
135 /// # }
136 /// ```
137 Edit(EditCommand<EditNotification>),
138 /// The 'plugin' namespace, for interacting with plugins.
139 ///
140 /// As with edit commands, the params object has is a nested RPC,
141 /// with the name of the command included as the `command` field.
142 ///
143 /// (this should be changed to more accurately reflect the behaviour
144 /// of the edit commands).
145 ///
146 /// For the available commands, see [`PluginNotification`].
147 ///
148 /// [`PluginNotification`]: enum.PluginNotification.html
149 ///
150 /// # Examples
151 ///
152 /// ```
153 /// # extern crate xi_core_lib as xi_core;
154 /// #[macro_use]
155 /// extern crate serde_json;
156 /// use crate::xi_core::rpc::*;
157 /// # fn main() {
158 /// let rpc = CoreNotification::Plugin(
159 /// PluginNotification::Start {
160 /// view_id: 1.into(),
161 /// plugin_name: "syntect".into(),
162 /// });
163 ///
164 /// let expected = json!({
165 /// "method": "plugin",
166 /// "params": {
167 /// "command": "start",
168 /// "view_id": "view-id-1",
169 /// "plugin_name": "syntect",
170 /// }
171 /// });
172 /// assert_eq!(serde_json::to_value(&rpc).unwrap(), expected);
173 /// # }
174 /// ```
175 Plugin(PluginNotification),
176 /// Tells `xi-core` to close the specified view.
177 CloseView { view_id: ViewId },
178 /// Tells `xi-core` to save the contents of the specified view's
179 /// buffer to the specified path.
180 Save { view_id: ViewId, file_path: String },
181 /// Tells `xi-core` to set the theme.
182 SetTheme { theme_name: String },
183 /// Notifies `xi-core` that the client has started.
184 ClientStarted {
185 #[serde(default)]
186 config_dir: Option<PathBuf>,
187 /// Path to additional plugins, included by the client.
188 #[serde(default)]
189 client_extras_dir: Option<PathBuf>,
190 },
191 /// Updates the user's config for the given domain. Where keys in
192 /// `changes` are `null`, those keys are cleared in the user config
193 /// for that domain; otherwise the config is updated with the new
194 /// value.
195 ///
196 /// Note: If the client is using file-based config, the only valid
197 /// domain argument is `ConfigDomain::UserOverride(_)`, which
198 /// represents non-persistent view-specific settings, such as when
199 /// a user manually changes whitespace settings for a given view.
200 ModifyUserConfig { domain: ConfigDomainExternal, changes: Table },
201 /// Control whether the tracing infrastructure is enabled.
202 /// This propagates to all peers that should respond by toggling its own
203 /// infrastructure on/off.
204 TracingConfig { enabled: bool },
205 /// Save trace data to the given path. The core will first send
206 /// CoreRequest::CollectTrace to all peers to collect the samples.
207 SaveTrace { destination: PathBuf, frontend_samples: Value },
208 /// Tells `xi-core` to set the language id for the view.
209 SetLanguage { view_id: ViewId, language_id: LanguageId },
210}
211
212/// The requests which make up the base of the protocol.
213///
214/// All requests expect a response.
215///
216/// # Examples
217///
218/// The `new_view` command:
219///
220/// ```
221/// # extern crate xi_core_lib as xi_core;
222/// extern crate serde_json;
223/// # fn main() {
224/// use crate::xi_core::rpc::CoreRequest;
225///
226/// let json = r#"{
227/// "method": "new_view",
228/// "params": { "file_path": "~/my_very_fun_file.rs" }
229/// }"#;
230///
231/// let cmd: CoreRequest = serde_json::from_str(&json).unwrap();
232/// match cmd {
233/// CoreRequest::NewView { .. } => (), // expected
234/// other => panic!("Unexpected variant {:?}", other),
235/// }
236/// # }
237/// ```
238#[derive(Serialize, Deserialize, Debug, PartialEq)]
239#[serde(rename_all = "snake_case")]
240#[serde(tag = "method", content = "params")]
241pub enum CoreRequest {
242 /// The 'edit' namespace, for view-specific requests.
243 Edit(EditCommand<EditRequest>),
244 /// Tells `xi-core` to create a new view. If the `file_path`
245 /// argument is present, `xi-core` should attempt to open the file
246 /// at that location.
247 ///
248 /// Returns the view identifier that should be used to interact
249 /// with the newly created view.
250 NewView { file_path: Option<String> },
251 /// Returns the current collated config object for the given view.
252 GetConfig { view_id: ViewId },
253 /// Returns the contents of the buffer for a given `ViewId`.
254 /// In the future this might also be used to return structured data (such
255 /// as for printing).
256 DebugGetContents { view_id: ViewId },
257}
258
259/// A helper type, which extracts the `view_id` field from edit
260/// requests and notifications.
261///
262/// Edit requests and notifications have 'method', 'params', and
263/// 'view_id' param members. We use this wrapper, which has custom
264/// `Deserialize` and `Serialize` implementations, to pull out the
265/// `view_id` field.
266///
267/// # Examples
268///
269/// ```
270/// # extern crate xi_core_lib as xi_core;
271/// extern crate serde_json;
272/// # fn main() {
273/// use crate::xi_core::rpc::*;
274///
275/// let json = r#"{
276/// "view_id": "view-id-1",
277/// "method": "scroll",
278/// "params": [0, 6]
279/// }"#;
280///
281/// let cmd: EditCommand<EditNotification> = serde_json::from_str(&json).unwrap();
282/// match cmd.cmd {
283/// EditNotification::Scroll( .. ) => (), // expected
284/// other => panic!("Unexpected variant {:?}", other),
285/// }
286/// # }
287/// ```
288#[derive(Debug, Clone, PartialEq)]
289pub struct EditCommand<T> {
290 pub view_id: ViewId,
291 pub cmd: T,
292}
293
294/// The smallest unit of text that a gesture can select
295#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Copy, Clone)]
296#[serde(rename_all = "snake_case")]
297pub enum SelectionGranularity {
298 /// Selects any point or character range
299 Point,
300 /// Selects one word at a time
301 Word,
302 /// Selects one line at a time
303 Line,
304}
305
306/// An enum representing touch and mouse gestures applied to the text.
307#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Copy, Clone)]
308#[serde(rename_all = "snake_case")]
309pub enum GestureType {
310 Select { granularity: SelectionGranularity, multi: bool },
311 SelectExtend { granularity: SelectionGranularity },
312 Drag,
313
314 // Deprecated
315 PointSelect,
316 ToggleSel,
317 RangeSelect,
318 LineSelect,
319 WordSelect,
320 MultiLineSelect,
321 MultiWordSelect,
322}
323
324/// An inclusive range.
325///
326/// # Note:
327///
328/// Several core protocol commands use a params array to pass arguments
329/// which are named, internally. this type use custom Serialize /
330/// Deserialize impls to accommodate this.
331#[derive(PartialEq, Eq, Debug, Clone)]
332pub struct LineRange {
333 pub first: i64,
334 pub last: i64,
335}
336
337/// A mouse event. See the note for [`LineRange`].
338///
339/// [`LineRange`]: enum.LineRange.html
340#[derive(PartialEq, Eq, Debug, Clone)]
341pub struct MouseAction {
342 pub line: u64,
343 pub column: u64,
344 pub flags: u64,
345 pub click_count: Option<u64>,
346}
347
348#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
349pub struct Position {
350 pub line: usize,
351 pub column: usize,
352}
353
354/// Represents how the current selection is modified (used by find
355/// operations).
356#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
357#[serde(rename_all = "snake_case")]
358pub enum SelectionModifier {
359 None,
360 Set,
361 Add,
362 AddRemovingCurrent,
363}
364
365impl Default for SelectionModifier {
366 fn default() -> SelectionModifier {
367 SelectionModifier::Set
368 }
369}
370
371#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
372#[serde(rename_all = "snake_case")]
373pub struct FindQuery {
374 pub id: Option<usize>,
375 pub chars: String,
376 pub case_sensitive: bool,
377 #[serde(default)]
378 pub regex: bool,
379 #[serde(default)]
380 pub whole_words: bool,
381}
382
383/// The edit-related notifications.
384///
385/// Alongside the [`EditRequest`] members, these commands constitute
386/// the API for interacting with a particular window and document.
387#[derive(Serialize, Deserialize, Debug, PartialEq)]
388#[serde(rename_all = "snake_case")]
389#[serde(tag = "method", content = "params")]
390pub enum EditNotification {
391 Insert {
392 chars: String,
393 },
394 Paste {
395 chars: String,
396 },
397 DeleteForward,
398 DeleteBackward,
399 DeleteWordForward,
400 DeleteWordBackward,
401 DeleteToEndOfParagraph,
402 DeleteToBeginningOfLine,
403 InsertNewline,
404 InsertTab,
405 MoveUp,
406 MoveUpAndModifySelection,
407 MoveDown,
408 MoveDownAndModifySelection,
409 MoveLeft,
410 // synoynm for `MoveLeft`
411 MoveBackward,
412 MoveLeftAndModifySelection,
413 MoveRight,
414 // synoynm for `MoveRight`
415 MoveForward,
416 MoveRightAndModifySelection,
417 MoveWordLeft,
418 MoveWordLeftAndModifySelection,
419 MoveWordRight,
420 MoveWordRightAndModifySelection,
421 MoveToBeginningOfParagraph,
422 MoveToBeginningOfParagraphAndModifySelection,
423 MoveToEndOfParagraph,
424 MoveToEndOfParagraphAndModifySelection,
425 MoveToLeftEndOfLine,
426 MoveToLeftEndOfLineAndModifySelection,
427 MoveToRightEndOfLine,
428 MoveToRightEndOfLineAndModifySelection,
429 MoveToBeginningOfDocument,
430 MoveToBeginningOfDocumentAndModifySelection,
431 MoveToEndOfDocument,
432 MoveToEndOfDocumentAndModifySelection,
433 ScrollPageUp,
434 PageUpAndModifySelection,
435 ScrollPageDown,
436 PageDownAndModifySelection,
437 SelectAll,
438 AddSelectionAbove,
439 AddSelectionBelow,
440 Scroll(LineRange),
441 Resize(Size),
442 GotoLine {
443 line: u64,
444 },
445 RequestLines(LineRange),
446 Yank,
447 Transpose,
448 Click(MouseAction),
449 Drag(MouseAction),
450 Gesture {
451 line: u64,
452 col: u64,
453 ty: GestureType,
454 },
455 Undo,
456 Redo,
457 Find {
458 chars: String,
459 case_sensitive: bool,
460 #[serde(default)]
461 regex: bool,
462 #[serde(default)]
463 whole_words: bool,
464 },
465 MultiFind {
466 queries: Vec<FindQuery>,
467 },
468 FindNext {
469 #[serde(default)]
470 wrap_around: bool,
471 #[serde(default)]
472 allow_same: bool,
473 #[serde(default)]
474 modify_selection: SelectionModifier,
475 },
476 FindPrevious {
477 #[serde(default)]
478 wrap_around: bool,
479 #[serde(default)]
480 allow_same: bool,
481 #[serde(default)]
482 modify_selection: SelectionModifier,
483 },
484 FindAll,
485 DebugRewrap,
486 DebugWrapWidth,
487 /// Prints the style spans present in the active selection.
488 DebugPrintSpans,
489 DebugToggleComment,
490 Uppercase,
491 Lowercase,
492 Capitalize,
493 Reindent,
494 Indent,
495 Outdent,
496 /// Indicates whether find highlights should be rendered
497 HighlightFind {
498 visible: bool,
499 },
500 SelectionForFind {
501 #[serde(default)]
502 case_sensitive: bool,
503 },
504 Replace {
505 chars: String,
506 #[serde(default)]
507 preserve_case: bool,
508 },
509 ReplaceNext,
510 ReplaceAll,
511 SelectionForReplace,
512 RequestHover {
513 request_id: usize,
514 position: Option<Position>,
515 },
516 SelectionIntoLines,
517 DuplicateLine,
518 IncreaseNumber,
519 DecreaseNumber,
520 ToggleRecording {
521 recording_name: Option<String>,
522 },
523 PlayRecording {
524 recording_name: String,
525 },
526 ClearRecording {
527 recording_name: String,
528 },
529 CollapseSelections,
530}
531
532/// The edit related requests.
533#[derive(Serialize, Deserialize, Debug, PartialEq)]
534#[serde(rename_all = "snake_case")]
535#[serde(tag = "method", content = "params")]
536pub enum EditRequest {
537 /// Cuts the active selection, returning their contents,
538 /// or `Null` if the selection was empty.
539 Cut,
540 /// Copies the active selection, returning their contents or
541 /// or `Null` if the selection was empty.
542 Copy,
543}
544
545/// The plugin related notifications.
546#[derive(Serialize, Deserialize, Debug, PartialEq)]
547#[serde(tag = "command")]
548#[serde(rename_all = "snake_case")]
549pub enum PluginNotification {
550 Start { view_id: ViewId, plugin_name: String },
551 Stop { view_id: ViewId, plugin_name: String },
552 PluginRpc { view_id: ViewId, receiver: String, rpc: PlaceholderRpc },
553}
554
555// Serialize / Deserialize
556
557impl<T: Serialize> Serialize for EditCommand<T> {
558 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
559 where
560 S: Serializer,
561 {
562 let mut v = serde_json::to_value(&self.cmd).map_err(ser::Error::custom)?;
563 v["view_id"] = json!(self.view_id);
564 v.serialize(serializer)
565 }
566}
567
568impl<'de, T: Deserialize<'de>> Deserialize<'de> for EditCommand<T> {
569 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
570 where
571 D: Deserializer<'de>,
572 {
573 #[derive(Deserialize)]
574 struct InnerId {
575 view_id: ViewId,
576 }
577
578 let mut v = Value::deserialize(deserializer)?;
579 let helper = InnerId::deserialize(&v).map_err(de::Error::custom)?;
580 let InnerId { view_id } = helper;
581
582 // if params are empty, remove them
583 let remove_params = match v.get("params") {
584 Some(&Value::Object(ref obj)) => obj.is_empty() && T::deserialize(v.clone()).is_err(),
585 Some(&Value::Array(ref arr)) => arr.is_empty() && T::deserialize(v.clone()).is_err(),
586 Some(_) => {
587 return Err(de::Error::custom(
588 "'params' field, if present, must be object or array.",
589 ));
590 }
591 None => false,
592 };
593
594 if remove_params {
595 v.as_object_mut().map(|v| v.remove("params"));
596 }
597
598 let cmd = T::deserialize(v).map_err(de::Error::custom)?;
599 Ok(EditCommand { view_id, cmd })
600 }
601}
602
603impl Serialize for MouseAction {
604 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
605 where
606 S: Serializer,
607 {
608 #[derive(Serialize)]
609 struct Helper(u64, u64, u64, Option<u64>);
610
611 let as_tup = Helper(self.line, self.column, self.flags, self.click_count);
612 as_tup.serialize(serializer)
613 }
614}
615
616impl<'de> Deserialize<'de> for MouseAction {
617 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
618 where
619 D: Deserializer<'de>,
620 {
621 let v: Vec<u64> = Vec::deserialize(deserializer)?;
622 let click_count = if v.len() == 4 { Some(v[3]) } else { None };
623 Ok(MouseAction { line: v[0], column: v[1], flags: v[2], click_count })
624 }
625}
626
627impl Serialize for LineRange {
628 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
629 where
630 S: Serializer,
631 {
632 let as_tup = (self.first, self.last);
633 as_tup.serialize(serializer)
634 }
635}
636
637impl<'de> Deserialize<'de> for LineRange {
638 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
639 where
640 D: Deserializer<'de>,
641 {
642 #[derive(Deserialize)]
643 struct TwoTuple(i64, i64);
644
645 let tup = TwoTuple::deserialize(deserializer)?;
646 Ok(LineRange { first: tup.0, last: tup.1 })
647 }
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653 use crate::tabs::ViewId;
654
655 #[test]
656 fn test_serialize_edit_command() {
657 // Ensure that an EditCommand can be serialized and then correctly deserialized.
658 let message: String = "hello world".into();
659 let edit = EditCommand {
660 view_id: ViewId(1),
661 cmd: EditNotification::Insert { chars: message.clone() },
662 };
663 let json = serde_json::to_string(&edit).unwrap();
664 let cmd: EditCommand<EditNotification> = serde_json::from_str(&json).unwrap();
665 assert_eq!(cmd.view_id, edit.view_id);
666 if let EditNotification::Insert { chars } = cmd.cmd {
667 assert_eq!(chars, message);
668 }
669 }
670}