Skip to content

Commit

Permalink
Implement snippet support in ark (#183)
Browse files Browse the repository at this point in the history
* Implement snippet support in ark

* Switch to `Option`, include `documentation`
  • Loading branch information
DavisVaughan authored Dec 15, 2023
1 parent 50d3cf5 commit aa252b3
Show file tree
Hide file tree
Showing 3 changed files with 305 additions and 0 deletions.
181 changes: 181 additions & 0 deletions crates/ark/resources/snippets/r.code-snippets
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
{
"lib": {
"prefix": "lib",
"body": "library(${1:package})",
"description": "Attach an R package"
},
"src": {
"prefix": "src",
"body": "source(\"${1:file.R}\")",
"description": "Source an R file"
},
"ret": {
"prefix": "ret",
"body": "return(${1:code})",
"description": "Return a value from a function"
},
"mat": {
"prefix": "mat",
"body": "matrix(${1:data}, nrow = ${2:rows}, ncol = ${3:cols})",
"description": "Define a matrix"
},
"sg": {
"prefix": "sg",
"body": [
"setGeneric(\"${1:generic}\", function(${2:x, ...}) {",
"\tstandardGeneric(\"${1:generic}\")",
"})"
],
"description": "Define a generic"
},
"sm": {
"prefix": "sm",
"body": [
"setMethod(\"${1:generic}\", ${2:class}, function(${2:x, ...}) {",
"\t${0}",
"})"
],
"description": "Define a method for a generic function"
},
"sc": {
"prefix": "sc",
"body": "setClass(\"${1:Class}\", slots = c(${2:name = \"type\"}))",
"description": "Define a class definition"
},
"if": {
"prefix": "if",
"body": [
"if (${1:condition}) {",
"\t${0}",
"}"
],
"description": "Conditional expression"
},
"el": {
"prefix": "el",
"body": [
"else {",
"\t${0}",
"}"
],
"description": "Conditional expression"
},
"ei": {
"prefix": "ei",
"body": [
"else if (${1:condition}) {",
"\t${0}",
"}"
],
"description": "Conditional expression"
},
"fun": {
"prefix": "fun",
"body": [
"${1:name} <- function(${2:variables}) {",
"\t${0}",
"}"
],
"description": "Function skeleton"
},
"for": {
"prefix": "for",
"body": [
"for (${1:variable} in ${2:vector}) {",
"\t${0}",
"}"
],
"description": "Define a loop"
},
"while": {
"prefix": "while",
"body": [
"while (${1:condition}) {",
"\t${0}",
"}"
],
"description": "Define a loop"
},
"switch": {
"prefix": "switch",
"body": [
"switch (${1:object},",
"\t${2:case} = ${3:action}",
")"
],
"description": "Define a switch statement"
},
"apply": {
"prefix": "apply",
"body": "apply(${1:array}, ${2:margin}, ${3:...})",
"description": "Use the apply family"
},
"lapply": {
"prefix": "lapply",
"body": "lapply(${1:list}, ${2:function})",
"description": "Use the apply family"
},
"sapply": {
"prefix": "sapply",
"body": "sapply(${1:list}, ${2:function})",
"description": "Use the apply family"
},
"mapply": {
"prefix": "mapply",
"body": "mapply(${1:function}, ${2:...})",
"description": "Use the apply family"
},
"tapply": {
"prefix": "tapply",
"body": "tapply(${1:vector}, ${2:index}, ${3:function})",
"description": "Use the apply family"
},
"vapply": {
"prefix": "vapply",
"body": "vapply(${1:list}, ${2:function}, FUN.VALUE = ${3:type}, ${4:...})",
"description": "Use the apply family"
},
"rapply": {
"prefix": "rapply",
"body": "rapply(${1:list}, ${2:function})",
"description": "Use the apply family"
},
"ts": {
"prefix": "ts",
"body": "`r paste(\"#\", date(), \"------------------------------\\n\")`",
"description": "Insert a datetime"
},
"shinyapp": {
"prefix": "shinyapp",
"body": [
"library(shiny)",
"",
"ui <- fluidPage(",
" ${0}",
")",
"",
"server <- function(input, output, session) {",
" ",
"}",
"",
"shinyApp(ui, server)"
],
"description": "Define a Shiny app"
},
"shinymod": {
"prefix": "shinymod",
"body": [
"${1:name}_UI <- function(id) {",
" ns <- NS(id)",
" tagList(",
"\t${0}",
" )",
"}",
"",
"${1:name} <- function(input, output, session) {",
" ",
"}"
],
"description": "Define a Shiny module"
}
}
3 changes: 3 additions & 0 deletions crates/ark/src/lsp/completions/sources/composite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod document;
mod keyword;
mod pipe;
mod search_path;
mod snippets;
mod subset;
mod workspace;

Expand All @@ -22,6 +23,7 @@ use keyword::completions_from_keywords;
use pipe::completions_from_pipe;
use pipe::find_pipe_root;
use search_path::completions_from_search_path;
use snippets::completions_from_snippets;
use stdext::*;
use subset::completions_from_subset;
use tower_lsp::lsp_types::CompletionItem;
Expand Down Expand Up @@ -63,6 +65,7 @@ pub fn completions_from_composite_sources(
// anything.
if context.node.kind() == "identifier" {
completions.append(&mut completions_from_keywords());
completions.append(&mut completions_from_snippets());
completions.append(&mut completions_from_search_path(context)?);

if let Some(mut additional_completions) = completions_from_document(context)? {
Expand Down
121 changes: 121 additions & 0 deletions crates/ark/src/lsp/completions/sources/composite/snippets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//
// snippets.rs
//
// Copyright (C) 2023 Posit Software, PBC. All rights reserved.
//
//

use std::collections::HashMap;
use std::sync::Once;

use rust_embed::RustEmbed;
use serde::Deserialize;
use tower_lsp::lsp_types::CompletionItem;
use tower_lsp::lsp_types::CompletionItemKind;
use tower_lsp::lsp_types::Documentation;
use tower_lsp::lsp_types::InsertTextFormat;
use tower_lsp::lsp_types::MarkupContent;
use tower_lsp::lsp_types::MarkupKind;

use crate::lsp::completions::completion_item::completion_item;
use crate::lsp::completions::types::CompletionData;

#[derive(RustEmbed)]
#[folder = "resources/snippets/"]
struct Asset;

#[derive(Deserialize)]
struct Snippet {
prefix: String,
body: SnippetBody,
description: String,
}

#[derive(Deserialize)]
#[serde(untagged)]
enum SnippetBody {
Scalar(String),
Vector(Vec<String>),
}

pub(super) fn completions_from_snippets() -> Vec<CompletionItem> {
log::info!("completions_from_snippets()");

// Return clone of cached snippet completion items
let completions = get_completions_from_snippets().clone();

completions
}

fn get_completions_from_snippets() -> &'static Vec<CompletionItem> {
static INIT: Once = Once::new();
static mut SNIPPETS: Option<Vec<CompletionItem>> = None;

INIT.call_once(|| unsafe {
SNIPPETS = Some(init_completions_from_snippets());
});

unsafe { SNIPPETS.as_ref().unwrap() }
}

fn init_completions_from_snippets() -> Vec<CompletionItem> {
// Load snippets JSON from embedded file
let file = Asset::get("r.code-snippets").unwrap();
let snippets: HashMap<String, Snippet> = serde_json::from_slice(&file.data).unwrap();

let mut completions = vec![];

for snippet in snippets.values() {
let label = snippet.prefix.clone();
let details = snippet.description.clone();

let body = match &snippet.body {
SnippetBody::Scalar(body) => body.clone(),
SnippetBody::Vector(body) => body.join("\n"),
};

// Markup shows up in the quick suggestion documentation window,
// so you can see what the snippet expands to
let markup = vec!["```r", body.as_str(), "```"].join("\n");
let markup = MarkupContent {
kind: MarkupKind::Markdown,
value: markup,
};

let mut item =
completion_item(label, CompletionData::Snippet { text: body.clone() }).unwrap();

item.detail = Some(details);
item.documentation = Some(Documentation::MarkupContent(markup));
item.kind = Some(CompletionItemKind::SNIPPET);

item.insert_text = Some(body);
item.insert_text_format = Some(InsertTextFormat::SNIPPET);

completions.push(item);
}

completions
}

#[cfg(test)]
mod tests {
use crate::lsp::completions::sources::composite::snippets::completions_from_snippets;

#[test]
fn test_snippets() {
let snippets = completions_from_snippets();

// Hash map isn't stable with regards to ordering
let item = snippets.iter().find(|item| item.label == "lib").unwrap();
assert_eq!(item.detail, Some("Attach an R package".to_string()));
assert_eq!(item.insert_text, Some("library(${1:package})".to_string()));

// Multiline body
let item = snippets.iter().find(|item| item.label == "if").unwrap();
assert_eq!(
item.insert_text,
Some("if (${1:condition}) {\n\t${0}\n}".to_string())
);
}
}

0 comments on commit aa252b3

Please sign in to comment.