From e36fcc34609b9f532f6966338c9440d4b3237938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=BCtte?= Date: Tue, 6 Aug 2024 16:56:35 +0200 Subject: [PATCH] cli: ace editor integration (#714) --- crates/rune/src/cli/ace.rs | 127 +++++++++ crates/rune/src/cli/mod.rs | 12 +- crates/rune/src/compile/prelude.rs | 11 + crates/rune/src/doc/autocomplete.rs | 392 ++++++++++++++++++++++++++ crates/rune/src/doc/build.rs | 2 +- crates/rune/src/doc/build/markdown.rs | 2 +- crates/rune/src/doc/mod.rs | 7 +- examples/README.md | 6 + examples/ace/.gitignore | 1 + examples/ace/README.md | 10 + examples/ace/index.html | 41 +++ 11 files changed, 607 insertions(+), 4 deletions(-) create mode 100644 crates/rune/src/cli/ace.rs create mode 100644 crates/rune/src/doc/autocomplete.rs create mode 100644 examples/README.md create mode 100644 examples/ace/.gitignore create mode 100644 examples/ace/README.md create mode 100644 examples/ace/index.html diff --git a/crates/rune/src/cli/ace.rs b/crates/rune/src/cli/ace.rs new file mode 100644 index 000000000..922074525 --- /dev/null +++ b/crates/rune/src/cli/ace.rs @@ -0,0 +1,127 @@ +use std::io::Write; +use std::path::PathBuf; + +use crate::doc::Artifacts; + +use anyhow::{Context, Result}; +use clap::Parser; + +use crate::alloc::prelude::*; +use crate::alloc::Vec; +use crate::cli::naming::Naming; +use crate::cli::{AssetKind, CommandBase, Config, Entry, EntryPoint, ExitCode, Io, SharedFlags}; +use crate::compile::FileSourceLoader; +use crate::{Diagnostics, Options, Source, Sources}; + +#[derive(Parser, Debug)] +pub(super) struct Flags { + /// Exit with a non-zero exit-code even for warnings + #[arg(long)] + warnings_are_errors: bool, + /// Output directory to write documentation to. + #[arg(long)] + output: Option, + /// Generate .await and ? extension for functions. + #[arg(long)] + extensions: bool, +} + +impl CommandBase for Flags { + #[inline] + fn is_workspace(&self, _: AssetKind) -> bool { + true + } + + #[inline] + fn describe(&self) -> &str { + "Documenting" + } +} + +pub(super) fn run<'p, I>( + io: &mut Io<'_>, + entry: &mut Entry<'_>, + c: &Config, + flags: &Flags, + shared: &SharedFlags, + options: &Options, + entries: I, +) -> Result +where + I: IntoIterator>, +{ + let root = match &flags.output { + Some(root) => root.clone(), + None => match &c.manifest_root { + Some(path) => path.join("target").join("rune-ace"), + None => match std::env::var_os("CARGO_TARGET_DIR") { + Some(target) => { + let mut target = PathBuf::from(target); + target.push("rune-ace"); + target + } + None => { + let mut target = PathBuf::new(); + target.push("target"); + target.push("rune-ace"); + target + } + }, + }, + }; + + writeln!(io.stdout, "Building ace autocompletion: {}", root.display())?; + + let context = shared.context(entry, c, None)?; + + let mut visitors = Vec::new(); + + let mut naming = Naming::default(); + + for e in entries { + let item = naming.item(&e)?; + + let mut visitor = crate::doc::Visitor::new(&item)?; + let mut sources = Sources::new(); + + let source = match Source::from_path(e.path()) { + Ok(source) => source, + Err(error) => return Err(error).context(e.path().display().try_to_string()?), + }; + + sources.insert(source)?; + + let mut diagnostics = if shared.warnings || flags.warnings_are_errors { + Diagnostics::new() + } else { + Diagnostics::without_warnings() + }; + + let mut source_loader = FileSourceLoader::new(); + + let _ = crate::prepare(&mut sources) + .with_context(&context) + .with_diagnostics(&mut diagnostics) + .with_options(options) + .with_visitor(&mut visitor)? + .with_source_loader(&mut source_loader) + .build(); + + diagnostics.emit(&mut io.stdout.lock(), &sources)?; + + if diagnostics.has_error() || flags.warnings_are_errors && diagnostics.has_warning() { + return Ok(ExitCode::Failure); + } + + visitors.try_push(visitor)?; + } + + let mut artifacts = Artifacts::new(); + crate::doc::build_autocomplete(&mut artifacts, &context, &visitors, flags.extensions)?; + + for asset in artifacts.assets() { + asset.build(&root)?; + } + + Ok(ExitCode::Success) +} diff --git a/crates/rune/src/cli/mod.rs b/crates/rune/src/cli/mod.rs index 7bd665605..52a5b5aab 100644 --- a/crates/rune/src/cli/mod.rs +++ b/crates/rune/src/cli/mod.rs @@ -6,6 +6,7 @@ //! * Build a language server, which is aware of things only available in your //! context. +mod ace; mod benches; mod check; mod doc; @@ -431,6 +432,8 @@ enum Command { Check(CommandShared), /// Build documentation. Doc(CommandShared), + /// Build ace autocompletion. + Ace(CommandShared), /// Run all tests but do not execute Test(CommandShared), /// Run the given program as a benchmark @@ -446,9 +449,10 @@ enum Command { } impl Command { - const ALL: [&'static str; 8] = [ + const ALL: [&'static str; 9] = [ "check", "doc", + "ace", "test", "bench", "run", @@ -461,6 +465,7 @@ impl Command { let (shared, command): (_, &mut dyn CommandBase) = match self { Command::Check(shared) => (&mut shared.shared, &mut shared.command), Command::Doc(shared) => (&mut shared.shared, &mut shared.command), + Command::Ace(shared) => (&mut shared.shared, &mut shared.command), Command::Test(shared) => (&mut shared.shared, &mut shared.command), Command::Bench(shared) => (&mut shared.shared, &mut shared.command), Command::Run(shared) => (&mut shared.shared, &mut shared.command), @@ -476,6 +481,7 @@ impl Command { let (shared, command): (_, &dyn CommandBase) = match self { Command::Check(shared) => (&shared.shared, &shared.command), Command::Doc(shared) => (&shared.shared, &shared.command), + Command::Ace(shared) => (&shared.shared, &shared.command), Command::Test(shared) => (&shared.shared, &shared.command), Command::Bench(shared) => (&shared.shared, &shared.command), Command::Run(shared) => (&shared.shared, &shared.command), @@ -911,6 +917,10 @@ where let options = f.options()?; return doc::run(io, entry, c, &f.command, &f.shared, &options, entries); } + Command::Ace(f) => { + let options = f.options()?; + return ace::run(io, entry, c, &f.command, &f.shared, &options, entries); + } Command::Fmt(f) => { let options = f.options()?; return format::run(io, entry, c, entries, &f.command, &f.shared, &options); diff --git a/crates/rune/src/compile/prelude.rs b/crates/rune/src/compile/prelude.rs index d1220e057..49b27fd4e 100644 --- a/crates/rune/src/compile/prelude.rs +++ b/crates/rune/src/compile/prelude.rs @@ -1,3 +1,5 @@ +use core::ops::Deref; + use crate::alloc::{self, Box, HashMap}; use crate::item::{IntoComponent, Item, ItemBuf}; @@ -54,6 +56,15 @@ impl Prelude { Some(self.prelude.get(name)?) } + /// Return the local name of an item + #[allow(dead_code)] + pub(crate) fn get_local<'a>(&'a self, item: &ItemBuf) -> Option<&'a str> { + self.prelude + .iter() + .find(|(_, i)| i == &item) + .map(|(s, _)| s.deref()) + } + /// Define a prelude item. fn add_prelude(&mut self, local: &str, path: I) -> alloc::Result<()> where diff --git a/crates/rune/src/doc/autocomplete.rs b/crates/rune/src/doc/autocomplete.rs new file mode 100644 index 000000000..088530b7d --- /dev/null +++ b/crates/rune/src/doc/autocomplete.rs @@ -0,0 +1,392 @@ +use core::fmt::Display; + +use anyhow::{Context as _, Result}; +use pulldown_cmark::{Options, Parser}; +use syntect::parsing::SyntaxSet; + +use crate as rune; +use crate::alloc::fmt::TryWrite; +use crate::alloc::prelude::*; +use crate::alloc::{HashMap, String}; +use crate::compile::Prelude; +use crate::doc::{Artifacts, Context, Visitor}; +use crate::{hash, ItemBuf}; + +use super::build::markdown; +use super::context::{Function, Kind, Meta, Signature}; + +pub(crate) fn build( + artifacts: &mut Artifacts, + context: &crate::Context, + visitors: &[Visitor], + extensions: bool, +) -> Result<()> { + let context = Context::new(Some(context), visitors); + + let mut acx = AutoCompleteCtx::new(&context, extensions); + for item in context.iter_modules() { + let item = item?; + acx.collect_meta(&item)?; + } + acx.build(artifacts)?; + + Ok(()) +} + +struct AutoCompleteCtx<'a> { + ctx: &'a Context<'a>, + extensions: bool, + syntax_set: SyntaxSet, + fixed: HashMap>, + instance: HashMap>, + prelude: Prelude, +} + +impl<'a> AutoCompleteCtx<'a> { + fn new(ctx: &'a Context, extensions: bool) -> AutoCompleteCtx<'a> { + AutoCompleteCtx { + ctx, + extensions, + syntax_set: SyntaxSet::load_defaults_newlines(), + fixed: HashMap::new(), + instance: HashMap::new(), + prelude: Prelude::with_default_prelude().unwrap_or_default(), + } + } + + fn collect_meta(&mut self, item: &ItemBuf) -> Result<()> { + for meta in self.ctx.meta(item)?.into_iter() { + match meta.kind { + Kind::Type => { + self.fixed.try_insert(item.try_clone()?, meta)?; + } + Kind::Struct => { + for (_, name) in self.ctx.iter_components(item)? { + let item = item.join([name])?; + self.collect_meta(&item)?; + } + self.fixed.try_insert(item.try_clone()?, meta)?; + } + Kind::Variant => { + self.fixed.try_insert(item.try_clone()?, meta)?; + } + Kind::Enum => { + for (_, name) in self.ctx.iter_components(item)? { + let item = item.join([name])?; + self.collect_meta(&item)?; + } + self.fixed.try_insert(item.try_clone()?, meta)?; + } + Kind::Macro => { + self.fixed.try_insert(item.try_clone()?, meta)?; + } + Kind::Function(f) => { + if matches!(f.signature, Signature::Instance) { + self.instance.try_insert(item.try_clone()?, meta)?; + } else { + self.fixed.try_insert(item.try_clone()?, meta)?; + } + } + Kind::Const(_) => { + self.fixed.try_insert(item.try_clone()?, meta)?; + } + Kind::Module => { + for (_, name) in self.ctx.iter_components(item)? { + let item = item.join([name])?; + self.collect_meta(&item)?; + } + self.fixed.try_insert(item.try_clone()?, meta)?; + } + Kind::Unsupported | Kind::Trait => {} + } + } + + Ok(()) + } + + fn build(&mut self, artifacts: &mut Artifacts) -> Result<()> { + let mut content = std::string::String::new(); + self.write(&mut content)?; + + artifacts.asset(false, "autocomplete.js", || { + let string = String::try_from(content)?; + Ok(string.into_bytes().into()) + })?; + + Ok(()) + } + + fn doc_to_html(&self, meta: &Meta) -> Result> { + let mut input = String::new(); + + for line in meta.docs { + let line = line.strip_prefix(' ').unwrap_or(line); + input.try_push_str(line)?; + input.try_push('\n')?; + } + + let mut o = String::new(); + write!(o, "
")?; + let mut options = Options::empty(); + options.insert(Options::ENABLE_STRIKETHROUGH); + + let iter = Parser::new_ext(&input, options); + + markdown::push_html(&self.syntax_set, &mut o, iter, None)?; + + write!(o, "
")?; + let o = String::try_from(o.replace('`', "\\`"))?; + + Ok(Some(o)) + } + + fn get_name(&self, item: &ItemBuf) -> Result { + // shorten item name with auto prelude when available + if let Some(name) = self.prelude.get_local(item) { + return Ok(name.try_to_string()?); + } + + // take default name and remove starting double points + let mut name = item.try_to_string()?; + if name.starts_with("::") { + name.try_replace_range(..2, "")?; + } + + Ok(name) + } + + fn get_fn_ext(f: &Function) -> Result { + let mut ext = String::new(); + // automatic .await for async functions + if f.is_async { + ext.try_push_str(".await")?; + } + + // automatic questionmark for result and option + if matches!( + f.return_type.base, + hash!(::std::option::Option) | hash!(::std::result::Result) + ) { + ext.try_push_str("?")?; + } + + Ok(ext) + } + + fn get_fn_param(f: &Function) -> Result { + let mut param = String::try_from("(")?; + + // add arguments when no argument names are provided + if let Some(args) = f.arguments { + for (n, arg) in args.iter().enumerate() { + if n > 0 { + param.try_push_str(", ")?; + } + + write!(param, "{}", arg.name)?; + } + } + + param.try_push(')')?; + Ok(param) + } + + fn get_fn_ret_typ(&self, f: &Function) -> Result { + let mut param = String::new(); + + if !self.extensions { + return Ok(param); + } + + if let Some(item) = self + .ctx + .meta_by_hash(f.return_type.base) + .ok() + .and_then(|v| v.into_iter().next()) + .and_then(|m| m.item.last()) + { + param.try_push_str(" -> ")?; + param.try_push_str(&item.try_to_string()?)?; + } + + Ok(param) + } + + fn write_hint( + &self, + f: &mut dyn core::fmt::Write, + value: &str, + meta: &str, + score: usize, + caption: Option<&str>, + doc: Option<&str>, + ) -> Result<()> { + write!(f, r#"{{"#)?; + write!(f, r#"value: "{value}""#)?; + if let Some(caption) = caption { + write!(f, r#", caption: "{caption}""#)?; + } + write!(f, r#", meta: "{meta}""#)?; + write!(f, r#", score: {score}"#)?; + if let Some(doc) = doc { + write!(f, r#", docHTML: `{}`"#, doc)?; + } + writeln!(f, "}}")?; + Ok(()) + } + + fn write_instances(&self, f: &mut dyn core::fmt::Write) -> Result<()> { + write!(f, r#"var instance = ["#)?; + + let mut no_comma = true; + + for (item, meta) in self.instance.iter() { + let Kind::Function(fnc) = meta.kind else { + continue; + }; + + if no_comma { + no_comma = false; + } else { + write!(f, ",")?; + } + + let mut iter = item.iter().rev(); + let mut value = iter + .next() + .context("No function name found for instance function")? + .try_to_string()?; + value.try_push_str(&Self::get_fn_param(&fnc)?)?; + + let mut typ = String::new(); + typ.try_push_str(&value)?; + typ.try_push_str(&self.get_fn_ret_typ(&fnc)?)?; + value.try_push_str(&Self::get_fn_ext(&fnc)?)?; + if let Some(pre) = iter.next().and_then(|t| t.try_to_string().ok()) { + typ.try_push_str(" [")?; + typ.try_push_str(&pre)?; + typ.try_push(']')?; + } + + let doc = self.doc_to_html(meta).ok().flatten(); + + let info = if fnc.is_async { + "async Instance" + } else { + "Instance" + }; + + self.write_hint(f, &value, info, 0, Some(&typ), doc.as_deref())?; + } + write!(f, "];")?; + + Ok(()) + } + + fn write_fixed(&self, f: &mut dyn core::fmt::Write) -> Result<()> { + write!(f, r#"var fixed = ["#)?; + + let mut no_comma = true; + + for (item, meta) in self.fixed.iter() { + if no_comma { + no_comma = false; + } else { + write!(f, ",")?; + } + + match meta.kind { + Kind::Type => { + let name = self.get_name(item)?; + let doc = self.doc_to_html(meta).ok().flatten(); + self.write_hint(f, &name, "Type", 0, None, doc.as_deref())?; + } + Kind::Struct => { + let name = self.get_name(item)?; + let doc = self.doc_to_html(meta).ok().flatten(); + self.write_hint(f, &name, "Struct", 0, None, doc.as_deref())?; + } + Kind::Variant => { + let name = self.get_name(item)?; + let doc = self.doc_to_html(meta).ok().flatten(); + self.write_hint(f, &name, "Variant", 0, None, doc.as_deref())?; + } + Kind::Enum => { + let name = self.get_name(item)?; + let doc = self.doc_to_html(meta).ok().flatten(); + self.write_hint(f, &name, "Enum", 0, None, doc.as_deref())?; + } + Kind::Macro => { + let mut name = self.get_name(item)?; + name.try_push_str("!()")?; + let doc = self.doc_to_html(meta).ok().flatten(); + self.write_hint(f, &name, "Type", 0, None, doc.as_deref())?; + } + Kind::Function(fnc) => { + let mut value = self.get_name(item)?; + value.try_push_str(&Self::get_fn_param(&fnc)?)?; + let mut caption = value.try_clone()?; + caption.try_push_str(&self.get_fn_ret_typ(&fnc)?)?; + value.try_push_str(&Self::get_fn_ext(&fnc)?)?; + let doc = self.doc_to_html(meta).ok().flatten(); + let info = if fnc.is_async { + "async Function" + } else { + "Function" + }; + self.write_hint(f, &value, info, 0, Some(&caption), doc.as_deref())?; + } + Kind::Const(_) => { + let name = self.get_name(item)?; + let doc = self.doc_to_html(meta).ok().flatten(); + self.write_hint(f, &name, "Const", 10, None, doc.as_deref())?; + } + Kind::Module => { + let name = self.get_name(item)?; + let doc = self.doc_to_html(meta).ok().flatten(); + self.write_hint(f, &name, "Module", 9, None, doc.as_deref())?; + } + Kind::Unsupported | Kind::Trait => { + no_comma = true; + } + } + } + write!(f, "];")?; + + Ok(()) + } + + fn write(&self, f: &mut dyn core::fmt::Write) -> Result<()> { + write!(f, "{COMPLETER}\n\n")?; + self.write_fixed(f)?; + write!(f, "\n\n")?; + self.write_instances(f)?; + Ok(()) + } +} + +impl<'a> Display for AutoCompleteCtx<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.write(f).map_err(|_| core::fmt::Error) + } +} + +static COMPLETER: &str = r#" +const runeCompleter = { + getCompletions: (editor, session, pos, prefix, callback) => { + if (prefix.length === 0) { + callback(null, []); + return; + } + + var token = session.getTokenAt(pos.row, pos.column - 1).value; + + if (token.includes(".")) { + callback(null, instance); + } else { + callback(null, fixed); + } + }, +}; +export default runeCompleter; +"#; diff --git a/crates/rune/src/doc/build.rs b/crates/rune/src/doc/build.rs index 15b33208f..7bff8da0a 100644 --- a/crates/rune/src/doc/build.rs +++ b/crates/rune/src/doc/build.rs @@ -1,5 +1,5 @@ mod js; -mod markdown; +pub(crate) mod markdown; mod type_; use core::fmt; diff --git a/crates/rune/src/doc/build/markdown.rs b/crates/rune/src/doc/build/markdown.rs index 69c079326..496720410 100644 --- a/crates/rune/src/doc/build/markdown.rs +++ b/crates/rune/src/doc/build/markdown.rs @@ -420,7 +420,7 @@ where } /// Process markdown html and captures tests. -pub(super) fn push_html<'a, I>( +pub(crate) fn push_html<'a, I>( syntax_set: &'a SyntaxSet, string: &'a mut String, iter: I, diff --git a/crates/rune/src/doc/mod.rs b/crates/rune/src/doc/mod.rs index b1d82d3ef..346c1185d 100644 --- a/crates/rune/src/doc/mod.rs +++ b/crates/rune/src/doc/mod.rs @@ -3,7 +3,7 @@ #[cfg(feature = "cli")] mod context; #[cfg(feature = "cli")] -use self::context::Context; +pub(crate) use self::context::Context; #[cfg(feature = "cli")] mod artifacts; @@ -22,3 +22,8 @@ pub(crate) use self::build::build; mod visitor; #[cfg(any(feature = "languageserver", feature = "cli"))] pub(crate) use self::visitor::{Visitor, VisitorData}; + +#[cfg(feature = "cli")] +mod autocomplete; +#[cfg(feature = "cli")] +pub(crate) use self::autocomplete::build as build_autocomplete; diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..1266e8f80 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,6 @@ +# rune examples + +See: + +* [examples] - wide range of rust code examples. +* [ace] - ace editor integration. \ No newline at end of file diff --git a/examples/ace/.gitignore b/examples/ace/.gitignore new file mode 100644 index 000000000..0a033783c --- /dev/null +++ b/examples/ace/.gitignore @@ -0,0 +1 @@ +autocomplete.js diff --git a/examples/ace/README.md b/examples/ace/README.md new file mode 100644 index 000000000..bca8d4d6e --- /dev/null +++ b/examples/ace/README.md @@ -0,0 +1,10 @@ +# ace editor example + +Steps to run and build this example: + +```sh +cargo run -p rune-cli -- ace --output . +python -m http.server +``` + +Then you can navigate to http://localhost:8000 \ No newline at end of file diff --git a/examples/ace/index.html b/examples/ace/index.html new file mode 100644 index 000000000..6b0dcf5ae --- /dev/null +++ b/examples/ace/index.html @@ -0,0 +1,41 @@ + + + + + + + + +
+println!("Hello World!"); +
+ + +