xi_core_lib/plugins/
rpc.rs

1// Copyright 2017 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//! RPC types, corresponding to protocol requests, notifications & responses.
16
17use std::borrow::Borrow;
18use std::path::PathBuf;
19
20use serde::de::{self, Deserialize, Deserializer};
21use serde::ser::{self, Serialize, Serializer};
22use serde_json::{self, Value};
23
24use super::PluginPid;
25use crate::annotations::AnnotationType;
26use crate::config::Table;
27use crate::syntax::LanguageId;
28use crate::tabs::{BufferIdentifier, ViewId};
29use xi_rope::{LinesMetric, Rope, RopeDelta};
30use xi_rpc::RemoteError;
31
32//TODO: At the moment (May 08, 2017) this is all very much in flux.
33// At some point, it will be stabalized and then perhaps will live in another crate,
34// shared with the plugin lib.
35
36// ====================================================================
37// core -> plugin RPC method types + responses
38// ====================================================================
39
40/// Buffer information sent on plugin init.
41#[derive(Serialize, Deserialize, Debug, Clone)]
42pub struct PluginBufferInfo {
43    /// The buffer's unique identifier.
44    pub buffer_id: BufferIdentifier,
45    /// The buffer's current views.
46    pub views: Vec<ViewId>,
47    pub rev: u64,
48    pub buf_size: usize,
49    pub nb_lines: usize,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub path: Option<String>,
52    pub syntax: LanguageId,
53    pub config: Table,
54}
55
56//TODO: very likely this should be merged with PluginDescription
57//TODO: also this does not belong here.
58/// Describes an available plugin to the client.
59#[derive(Serialize, Deserialize, Debug)]
60pub struct ClientPluginInfo {
61    pub name: String,
62    pub running: bool,
63}
64
65/// A simple update, sent to a plugin.
66#[derive(Serialize, Deserialize, Debug, Clone)]
67pub struct PluginUpdate {
68    pub view_id: ViewId,
69    /// The delta representing changes to the document.
70    ///
71    /// Note: Is `Some` in the general case; only if the delta involves
72    /// inserting more than some maximum number of bytes, will this be `None`,
73    /// indicating the plugin should flush cache and fetch manually.
74    pub delta: Option<RopeDelta>,
75    /// The size of the document after applying this delta.
76    pub new_len: usize,
77    /// The total number of lines in the document after applying this delta.
78    pub new_line_count: usize,
79    pub rev: u64,
80    /// The undo_group associated with this update. The plugin may pass
81    /// this value back to core when making an edit, to associate the
82    /// plugin's edit with this undo group. Core uses undo_group
83    //  to undo actions occurred due to plugins after a user action
84    // in a single step.
85    pub undo_group: Option<usize>,
86    pub edit_type: String,
87    pub author: String,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct EmptyStruct {}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(rename_all = "snake_case")]
95#[serde(tag = "method", content = "params")]
96/// RPC requests sent from the host
97pub enum HostRequest {
98    Update(PluginUpdate),
99    CollectTrace(EmptyStruct),
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "snake_case")]
104#[serde(tag = "method", content = "params")]
105/// RPC Notifications sent from the host
106pub enum HostNotification {
107    Ping(EmptyStruct),
108    Initialize { plugin_id: PluginPid, buffer_info: Vec<PluginBufferInfo> },
109    DidSave { view_id: ViewId, path: PathBuf },
110    ConfigChanged { view_id: ViewId, changes: Table },
111    NewBuffer { buffer_info: Vec<PluginBufferInfo> },
112    DidClose { view_id: ViewId },
113    GetHover { view_id: ViewId, request_id: usize, position: usize },
114    Shutdown(EmptyStruct),
115    TracingConfig { enabled: bool },
116    LanguageChanged { view_id: ViewId, new_lang: LanguageId },
117    CustomCommand { view_id: ViewId, method: String, params: Value },
118}
119
120// ====================================================================
121// plugin -> core RPC method types
122// ====================================================================
123
124/// A simple edit, received from a plugin.
125#[derive(Serialize, Deserialize, Debug, Clone)]
126pub struct PluginEdit {
127    pub rev: u64,
128    pub delta: RopeDelta,
129    /// the edit priority determines the resolution strategy when merging
130    /// concurrent edits. The highest priority edit will be applied last.
131    pub priority: u64,
132    /// whether the inserted text prefers to be to the right of the cursor.
133    pub after_cursor: bool,
134    /// the originator of this edit: some identifier (plugin name, 'core', etc)
135    /// undo_group associated with this edit
136    pub undo_group: Option<usize>,
137    pub author: String,
138}
139
140#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
141pub struct ScopeSpan {
142    pub start: usize,
143    pub end: usize,
144    pub scope_id: u32,
145}
146
147#[derive(Serialize, Deserialize, Debug, Clone)]
148pub struct DataSpan {
149    pub start: usize,
150    pub end: usize,
151    pub data: Value,
152}
153
154/// The object returned by the `get_data` RPC.
155#[derive(Debug, Serialize, Deserialize)]
156pub struct GetDataResponse {
157    pub chunk: String,
158    pub offset: usize,
159    pub first_line: usize,
160    pub first_line_offset: usize,
161}
162
163/// The unit of measure when requesting data.
164#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
165#[serde(rename_all = "snake_case")]
166pub enum TextUnit {
167    /// The requested offset is in bytes. The returned chunk will be valid
168    /// UTF8, and is guaranteed to include the byte specified the offset.
169    Utf8,
170    /// The requested offset is a line number. The returned chunk will begin
171    /// at the offset of the requested line.
172    Line,
173}
174
175#[derive(Serialize, Deserialize, Debug, Clone)]
176#[serde(rename_all = "snake_case")]
177#[serde(tag = "method", content = "params")]
178/// RPC requests sent from plugins.
179pub enum PluginRequest {
180    GetData { start: usize, unit: TextUnit, max_size: usize, rev: u64 },
181    LineCount,
182    GetSelections,
183}
184
185#[derive(Serialize, Deserialize, Debug, Clone)]
186#[serde(rename_all = "snake_case")]
187#[serde(tag = "method", content = "params")]
188/// RPC commands sent from plugins.
189pub enum PluginNotification {
190    AddScopes {
191        scopes: Vec<Vec<String>>,
192    },
193    UpdateSpans {
194        start: usize,
195        len: usize,
196        spans: Vec<ScopeSpan>,
197        rev: u64,
198    },
199    Edit {
200        edit: PluginEdit,
201    },
202    Alert {
203        msg: String,
204    },
205    AddStatusItem {
206        key: String,
207        value: String,
208        alignment: String,
209    },
210    UpdateStatusItem {
211        key: String,
212        value: String,
213    },
214    RemoveStatusItem {
215        key: String,
216    },
217    ShowHover {
218        request_id: usize,
219        result: Result<Hover, RemoteError>,
220    },
221    UpdateAnnotations {
222        start: usize,
223        len: usize,
224        spans: Vec<DataSpan>,
225        annotation_type: AnnotationType,
226        rev: u64,
227    },
228}
229
230/// Range expressed in terms of PluginPosition. Meant to be sent from
231/// plugin to core.
232#[derive(Serialize, Deserialize, Debug, Clone)]
233#[serde(rename_all = "snake_case")]
234pub struct Range {
235    pub start: usize,
236    pub end: usize,
237}
238
239/// Hover Item sent from Plugin to Core
240#[derive(Serialize, Deserialize, Debug, Clone)]
241#[serde(rename_all = "snake_case")]
242pub struct Hover {
243    pub content: String,
244    pub range: Option<Range>,
245}
246
247/// Common wrapper for plugin-originating RPCs.
248pub struct PluginCommand<T> {
249    pub view_id: ViewId,
250    pub plugin_id: PluginPid,
251    pub cmd: T,
252}
253
254impl<T: Serialize> Serialize for PluginCommand<T> {
255    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
256    where
257        S: Serializer,
258    {
259        let mut v = serde_json::to_value(&self.cmd).map_err(ser::Error::custom)?;
260        v["params"]["view_id"] = json!(self.view_id);
261        v["params"]["plugin_id"] = json!(self.plugin_id);
262        v.serialize(serializer)
263    }
264}
265
266impl<'de, T: Deserialize<'de>> Deserialize<'de> for PluginCommand<T> {
267    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
268    where
269        D: Deserializer<'de>,
270    {
271        #[derive(Deserialize)]
272        struct InnerIds {
273            view_id: ViewId,
274            plugin_id: PluginPid,
275        }
276        #[derive(Deserialize)]
277        struct IdsWrapper {
278            params: InnerIds,
279        }
280
281        let v = Value::deserialize(deserializer)?;
282        let helper = IdsWrapper::deserialize(&v).map_err(de::Error::custom)?;
283        let InnerIds { view_id, plugin_id } = helper.params;
284        let cmd = T::deserialize(v).map_err(de::Error::custom)?;
285        Ok(PluginCommand { view_id, plugin_id, cmd })
286    }
287}
288
289impl PluginBufferInfo {
290    pub fn new(
291        buffer_id: BufferIdentifier,
292        views: &[ViewId],
293        rev: u64,
294        buf_size: usize,
295        nb_lines: usize,
296        path: Option<PathBuf>,
297        syntax: LanguageId,
298        config: Table,
299    ) -> Self {
300        //TODO: do make any current assertions about paths being valid utf-8? do we want to?
301        let path = path.map(|p| p.to_str().unwrap().to_owned());
302        let views = views.to_owned();
303        PluginBufferInfo { buffer_id, views, rev, buf_size, nb_lines, path, syntax, config }
304    }
305}
306
307impl PluginUpdate {
308    pub fn new<D>(
309        view_id: ViewId,
310        rev: u64,
311        delta: D,
312        new_len: usize,
313        new_line_count: usize,
314        undo_group: Option<usize>,
315        edit_type: String,
316        author: String,
317    ) -> Self
318    where
319        D: Into<Option<RopeDelta>>,
320    {
321        let delta = delta.into();
322        PluginUpdate { view_id, delta, new_len, new_line_count, rev, undo_group, edit_type, author }
323    }
324}
325
326// maybe this should be in xi_rope? has a strong resemblance to the various
327// concrete `Metric` types.
328impl TextUnit {
329    /// Converts an offset in some unit to a concrete byte offset. Returns
330    /// `None` if the input offset is out of bounds in its unit space.
331    pub fn resolve_offset<T: Borrow<Rope>>(self, text: T, offset: usize) -> Option<usize> {
332        let text = text.borrow();
333        match self {
334            TextUnit::Utf8 => {
335                if offset > text.len() {
336                    None
337                } else {
338                    text.at_or_prev_codepoint_boundary(offset)
339                }
340            }
341            TextUnit::Line => {
342                let max_line_number = text.measure::<LinesMetric>() + 1;
343                if offset > max_line_number {
344                    None
345                } else {
346                    text.offset_of_line(offset).into()
347                }
348            }
349        }
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356    use serde_json;
357
358    #[test]
359    fn test_plugin_update() {
360        let json = r#"{
361            "view_id": "view-id-42",
362            "delta": {"base_len": 6, "els": [{"copy": [0,5]}, {"insert":"rofls"}, {"copy": [5,6]}]},
363            "new_len": 11,
364            "new_line_count": 1,
365            "rev": 5,
366            "undo_group": 6,
367            "edit_type": "something",
368            "author": "me"
369    }"#;
370
371        let val: PluginUpdate = match serde_json::from_str(json) {
372            Ok(val) => val,
373            Err(err) => panic!("{:?}", err),
374        };
375        assert!(val.delta.is_some());
376        assert!(val.delta.unwrap().as_simple_insert().is_some());
377    }
378
379    #[test]
380    fn test_deserde_init() {
381        let json = r#"
382            {"buffer_id": 42,
383             "views": ["view-id-4"],
384             "rev": 1,
385             "buf_size": 20,
386             "nb_lines": 5,
387             "path": "some_path",
388             "syntax": "toml",
389             "config": {"some_key": 420}}"#;
390
391        let val: PluginBufferInfo = match serde_json::from_str(json) {
392            Ok(val) => val,
393            Err(err) => panic!("{:?}", err),
394        };
395        assert_eq!(val.rev, 1);
396        assert_eq!(val.path, Some("some_path".to_owned()));
397        assert_eq!(val.syntax, "toml".into());
398    }
399
400    #[test]
401    fn test_de_plugin_rpc() {
402        let json = r#"{"method": "alert", "params": {"view_id": "view-id-1", "plugin_id": 42, "msg": "ahhh!"}}"#;
403        let de: PluginCommand<PluginNotification> = serde_json::from_str(json).unwrap();
404        assert_eq!(de.view_id, ViewId(1));
405        assert_eq!(de.plugin_id, PluginPid(42));
406        match de.cmd {
407            PluginNotification::Alert { ref msg } if msg == "ahhh!" => (),
408            _ => panic!("{:?}", de.cmd),
409        }
410    }
411}