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}