xi_core_lib/plugins/
manifest.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//! Structured representation of a plugin's features and capabilities.
16
17use std::path::PathBuf;
18
19use serde::{Deserialize, Deserializer, Serialize};
20use serde_json::{self, Value};
21
22use crate::syntax::{LanguageDefinition, LanguageId};
23
24/// Describes attributes and capabilities of a plugin.
25///
26/// Note: - these will eventually be loaded from manifest files.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub struct PluginDescription {
30    pub name: String,
31    pub version: String,
32    #[serde(default)]
33    pub scope: PluginScope,
34    // more metadata ...
35    /// path to plugin executable
36    #[serde(deserialize_with = "platform_exec_path")]
37    pub exec_path: PathBuf,
38    /// Events that cause this plugin to run
39    #[serde(default)]
40    pub activations: Vec<PluginActivation>,
41    #[serde(default)]
42    pub commands: Vec<Command>,
43    #[serde(default)]
44    pub languages: Vec<LanguageDefinition>,
45}
46
47fn platform_exec_path<'de, D: Deserializer<'de>>(deserializer: D) -> Result<PathBuf, D::Error> {
48    let exec_path = PathBuf::deserialize(deserializer)?;
49    if cfg!(windows) {
50        Ok(exec_path.with_extension("exe"))
51    } else {
52        Ok(exec_path)
53    }
54}
55
56/// `PluginActivation`s represent events that trigger running a plugin.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum PluginActivation {
60    /// Always run this plugin, when available.
61    Autorun,
62    /// Run this plugin if the provided SyntaxDefinition is active.
63    #[allow(dead_code)]
64    OnSyntax(LanguageId),
65    /// Run this plugin in response to a given command.
66    #[allow(dead_code)]
67    OnCommand,
68}
69
70/// Describes the scope of events a plugin receives.
71#[derive(Debug, Clone, Deserialize, Serialize)]
72#[serde(rename_all = "snake_case")]
73pub enum PluginScope {
74    /// The plugin receives events from multiple buffers.
75    Global,
76    /// The plugin receives events for a single buffer.
77    BufferLocal,
78    /// The plugin is launched in response to a command, and receives no
79    /// further updates.
80    SingleInvocation,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84/// Represents a custom command provided by a plugin.
85pub struct Command {
86    /// Human readable title, for display in (for example) a menu.
87    pub title: String,
88    /// A short description of the command.
89    pub description: String,
90    /// Template of the command RPC as it should be sent to the plugin.
91    pub rpc_cmd: PlaceholderRpc,
92    /// A list of `CommandArgument`s, which the client should use to build the RPC.
93    pub args: Vec<CommandArgument>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
97/// A user provided argument to a plugin command.
98pub struct CommandArgument {
99    /// A human readable name for this argument, for use as placeholder
100    /// text or equivelant.
101    pub title: String,
102    /// A short (single sentence) description of this argument's use.
103    pub description: String,
104    pub key: String,
105    pub arg_type: ArgumentType,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    /// If `arg_type` is `Choice`, `options` must contain a list of options.
108    pub options: Option<Vec<ArgumentOption>>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
112pub enum ArgumentType {
113    Number,
114    Int,
115    PosInt,
116    Bool,
117    String,
118    Choice,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
122/// Represents an option for a user-selectable argument.
123pub struct ArgumentOption {
124    pub title: String,
125    pub value: Value,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
129#[serde(rename_all = "snake_case")]
130/// A placeholder type which can represent a generic RPC.
131///
132/// This is the type used for custom plugin commands, which may have arbitrary
133/// method names and parameters.
134pub struct PlaceholderRpc {
135    pub method: String,
136    pub params: Value,
137    pub rpc_type: RpcType,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
141#[serde(rename_all = "snake_case")]
142pub enum RpcType {
143    Notification,
144    Request,
145}
146
147impl Command {
148    pub fn new<S, V>(title: S, description: S, rpc_cmd: PlaceholderRpc, args: V) -> Self
149    where
150        S: AsRef<str>,
151        V: Into<Option<Vec<CommandArgument>>>,
152    {
153        let title = title.as_ref().to_owned();
154        let description = description.as_ref().to_owned();
155        let args = args.into().unwrap_or_else(Vec::new);
156        Command { title, description, rpc_cmd, args }
157    }
158}
159
160impl CommandArgument {
161    pub fn new<S: AsRef<str>>(
162        title: S,
163        description: S,
164        key: S,
165        arg_type: ArgumentType,
166        options: Option<Vec<ArgumentOption>>,
167    ) -> Self {
168        let key = key.as_ref().to_owned();
169        let title = title.as_ref().to_owned();
170        let description = description.as_ref().to_owned();
171        if arg_type == ArgumentType::Choice {
172            assert!(options.is_some())
173        }
174        CommandArgument { title, description, key, arg_type, options }
175    }
176}
177
178impl ArgumentOption {
179    pub fn new<S: AsRef<str>, V: Serialize>(title: S, value: V) -> Self {
180        let title = title.as_ref().to_owned();
181        let value = serde_json::to_value(value).unwrap();
182        ArgumentOption { title, value }
183    }
184}
185
186impl PlaceholderRpc {
187    pub fn new<S, V>(method: S, params: V, request: bool) -> Self
188    where
189        S: AsRef<str>,
190        V: Into<Option<Value>>,
191    {
192        let method = method.as_ref().to_owned();
193        let params = params.into().unwrap_or(json!({}));
194        let rpc_type = if request { RpcType::Request } else { RpcType::Notification };
195
196        PlaceholderRpc { method, params, rpc_type }
197    }
198
199    pub fn is_request(&self) -> bool {
200        self.rpc_type == RpcType::Request
201    }
202
203    /// Returns a reference to the placeholder's params.
204    pub fn params_ref(&self) -> &Value {
205        &self.params
206    }
207
208    /// Returns a mutable reference to the placeholder's params.
209    pub fn params_ref_mut(&mut self) -> &mut Value {
210        &mut self.params
211    }
212
213    /// Returns a reference to the placeholder's method.
214    pub fn method_ref(&self) -> &str {
215        &self.method
216    }
217}
218
219impl PluginDescription {
220    /// Returns `true` if this plugin is globally scoped, else `false`.
221    pub fn is_global(&self) -> bool {
222        match self.scope {
223            PluginScope::Global => true,
224            _ => false,
225        }
226    }
227}
228
229impl Default for PluginScope {
230    fn default() -> Self {
231        PluginScope::BufferLocal
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use serde_json;
239
240    #[test]
241    fn platform_exec_path() {
242        let json = r#"
243        {
244            "name": "test_plugin",
245            "version": "0.0.0",
246            "scope": "global",
247            "exec_path": "path/to/binary",
248            "activations": [],
249            "commands": [],
250            "languages": []
251        }
252        "#;
253
254        let plugin_desc: PluginDescription = serde_json::from_str(&json).unwrap();
255        if cfg!(windows) {
256            assert!(plugin_desc.exec_path.ends_with("binary.exe"));
257        } else {
258            assert!(plugin_desc.exec_path.ends_with("binary"));
259        }
260    }
261
262    #[test]
263    fn test_serde_command() {
264        let json = r#"
265    {
266        "title": "Test Command",
267        "description": "Passes the current test",
268        "rpc_cmd": {
269            "rpc_type": "notification",
270            "method": "test.cmd",
271            "params": {
272                "view": "",
273                "non_arg": "plugin supplied value",
274                "arg_one": "",
275                "arg_two": ""
276            }
277        },
278        "args": [
279            {
280                "title": "First argument",
281                "description": "Indicates something",
282                "key": "arg_one",
283                "arg_type": "Bool"
284            },
285            {
286                "title": "Favourite Number",
287                "description": "A number used in a test.",
288                "key": "arg_two",
289                "arg_type": "Choice",
290                "options": [
291                    {"title": "Five", "value": 5},
292                    {"title": "Ten", "value": 10}
293                ]
294            }
295        ]
296    }
297        "#;
298
299        let command: Command = serde_json::from_str(&json).unwrap();
300        assert_eq!(command.title, "Test Command");
301        assert_eq!(command.args[0].arg_type, ArgumentType::Bool);
302        assert_eq!(command.rpc_cmd.params_ref()["non_arg"], "plugin supplied value");
303        assert_eq!(command.args[1].options.clone().unwrap()[1].value, json!(10));
304    }
305}