diff --git a/crates/rune/Cargo.toml b/crates/rune/Cargo.toml index f97716475..30c8e49ad 100644 --- a/crates/rune/Cargo.toml +++ b/crates/rune/Cargo.toml @@ -20,7 +20,7 @@ bench = [] workspace = ["toml", "semver", "relative-path", "serde-hashkey"] doc = ["rust-embed", "handlebars", "pulldown-cmark", "syntect"] cli = ["doc", "bincode", "atty", "tracing-subscriber", "anyhow/std", "clap", "webbrowser", "capture-io", "disable-io", "languageserver", "fmt"] -languageserver = ["lsp", "ropey", "percent-encoding", "url", "serde_json", "tokio", "workspace", "doc"] +languageserver = ["lsp", "ropey", "percent-encoding", "url", "serde_json", "tokio", "tokio/macros", "tokio/io-std", "workspace", "doc"] capture-io = ["parking_lot"] disable-io = [] fmt = [] diff --git a/crates/rune/src/cli.rs b/crates/rune/src/cli.rs index 3506f17b5..2aaa32635 100644 --- a/crates/rune/src/cli.rs +++ b/crates/rune/src/cli.rs @@ -31,7 +31,7 @@ use crate::workspace::WorkspaceFilter; use crate::{Context, ContextError, Options}; /// Default about splash. -const DEFAULT_ABOUT: &[u8] = b"The Rune Language Interpreter"; +const DEFAULT_ABOUT: &str = "The Rune Language Interpreter"; /// Options for building context. #[non_exhaustive] @@ -102,16 +102,12 @@ impl<'a> Entry<'a> { let args = match Args::try_parse() { Ok(args) => args, Err(e) => { - let about = self - .about - .as_deref() - .map(|s| s.as_bytes()) - .unwrap_or(DEFAULT_ABOUT); + let about = self.about.as_deref().unwrap_or(DEFAULT_ABOUT); let code = if e.use_stderr() { let o = std::io::stderr(); let mut o = o.lock(); - o.write_all(about)?; + o.write_all(about.as_bytes())?; writeln!(o)?; writeln!(o)?; writeln!(o, "{}", e)?; @@ -120,7 +116,7 @@ impl<'a> Entry<'a> { } else { let o = std::io::stdout(); let mut o = o.lock(); - o.write_all(about)?; + o.write_all(about.as_bytes())?; writeln!(o)?; writeln!(o)?; writeln!(o, "{}", e)?; @@ -132,6 +128,15 @@ impl<'a> Entry<'a> { } }; + if args.version { + let o = std::io::stdout(); + let mut o = o.lock(); + let about = self.about.as_deref().unwrap_or(DEFAULT_ABOUT); + o.write_all(about.as_bytes())?; + o.flush()?; + return Ok(ExitCode::Success); + } + let choice = match args.color.as_str() { "always" => ColorChoice::Always, "ansi" => ColorChoice::AlwaysAnsi, @@ -182,30 +187,48 @@ struct Io<'a> { stderr: &'a mut StandardStream, } +#[derive(Parser, Debug, Clone)] +struct CommandShared +where + T: clap::Args, +{ + #[command(flatten)] + command: T, + #[command(flatten)] + shared: SharedFlags, +} + #[derive(Subcommand, Debug, Clone)] enum Command { /// Run checks but do not execute - Check(check::Flags), + Check(CommandShared), /// Build documentation. - Doc(doc::Flags), + Doc(CommandShared), /// Run all tests but do not execute - Test(tests::Flags), + Test(CommandShared), /// Run the given program as a benchmark - Bench(benches::Flags), + Bench(CommandShared), /// Run the designated script - Run(run::Flags), + Run(CommandShared), /// Format the provided file - Fmt(format::Flags), + Fmt(CommandShared), /// Run a language server. - LanguageServer(languageserver::Flags), + LanguageServer(SharedFlags), } impl Command { + const ALL: [&str; 7] = [ + "check", + "doc", + "test", + "bench", + "run", + "fmt", + "languageserver", + ]; + fn propagate_related_flags(&mut self, c: &mut Config) { match self { - Command::Check(..) => {} - Command::Doc(..) => {} - Command::Fmt(..) => {} Command::Test(..) => { c.test = true; } @@ -213,9 +236,9 @@ impl Command { c.test = true; } Command::Run(args) => { - args.propagate_related_flags(); + args.command.propagate_related_flags(); } - Command::LanguageServer(..) => {} + _ => {} } } @@ -239,7 +262,7 @@ impl Command { Command::Test(args) => &args.shared, Command::Bench(args) => &args.shared, Command::Run(args) => &args.shared, - Command::LanguageServer(args) => &args.shared, + Command::LanguageServer(shared) => shared, } } @@ -366,6 +389,10 @@ impl SharedFlags { #[derive(Parser, Debug, Clone)] #[command(name = "rune", about = None)] struct Args { + /// Print the version of the command. + #[arg(long)] + version: bool, + /// Control if output is colored or not. /// /// Valid options are: @@ -379,7 +406,7 @@ struct Args { /// The command to execute #[command(subcommand)] - cmd: Command, + cmd: Option, } impl Args { @@ -388,21 +415,16 @@ impl Args { let mut options = Options::default(); // Command-specific override defaults. - match &self.cmd { - Command::Test(_) | Command::Check(_) => { - options.debug_info(true); - options.test(true); - options.bytecode(false); - } - Command::Bench(_) - | Command::Doc(..) - | Command::Run(_) - | Command::LanguageServer(_) - | Command::Fmt(..) => {} + if let Some(Command::Test(..) | Command::Check(..)) = &self.cmd { + options.debug_info(true); + options.test(true); + options.bytecode(false); } - for option in &self.cmd.shared().compiler_options { - options.parse_option(option)?; + if let Some(cmd) = &self.cmd { + for option in &cmd.shared().compiler_options { + options.parse_option(option)?; + } } Ok(options) @@ -517,10 +539,9 @@ fn find_manifest() -> Result<(PathBuf, PathBuf)> { } } -fn populate_config(io: &mut Io<'_>, c: &mut Config, args: &Args) -> Result<()> { +fn populate_config(io: &mut Io<'_>, c: &mut Config, cmd: &Command) -> Result<()> { c.build_paths.extend( - args.cmd - .shared() + cmd.shared() .paths .iter() .map(|p| BuildPath::Path(p.as_path().into())), @@ -559,7 +580,7 @@ fn populate_config(io: &mut Io<'_>, c: &mut Config, args: &Args) -> Result<()> { let manifest = result?; - if let Some(bin) = args.cmd.bins_test() { + if let Some(bin) = cmd.bins_test() { for found in manifest.find_bins(bin)? { let package = Package { name: found.package.name.clone(), @@ -568,7 +589,7 @@ fn populate_config(io: &mut Io<'_>, c: &mut Config, args: &Args) -> Result<()> { } } - if let Some(test) = args.cmd.tests_test() { + if let Some(test) = cmd.tests_test() { for found in manifest.find_tests(test)? { let package = Package { name: found.package.name.clone(), @@ -577,7 +598,7 @@ fn populate_config(io: &mut Io<'_>, c: &mut Config, args: &Args) -> Result<()> { } } - if let Some(example) = args.cmd.examples_test() { + if let Some(example) = cmd.examples_test() { for found in manifest.find_examples(example)? { let package = Package { name: found.package.name.clone(), @@ -586,7 +607,7 @@ fn populate_config(io: &mut Io<'_>, c: &mut Config, args: &Args) -> Result<()> { } } - if let Some(bench) = args.cmd.benches_test() { + if let Some(bench) = cmd.benches_test() { for found in manifest.find_benches(bench)? { let package = Package { name: found.package.name.clone(), @@ -600,15 +621,28 @@ fn populate_config(io: &mut Io<'_>, c: &mut Config, args: &Args) -> Result<()> { async fn main_with_out(io: &mut Io<'_>, entry: &mut Entry<'_>, mut args: Args) -> Result { let mut c = Config::default(); - args.cmd.propagate_related_flags(&mut c); - populate_config(io, &mut c, &args)?; + + if let Some(cmd) = &mut args.cmd { + cmd.propagate_related_flags(&mut c); + } + + let cmd = match &args.cmd { + Some(cmd) => cmd, + None => { + let commands = Command::ALL.into_iter().collect::>().join(", "); + writeln!(io.stdout, "Expected a subcommand: {commands}")?; + return Ok(ExitCode::Failure); + } + }; + + populate_config(io, &mut c, cmd)?; let entries = std::mem::take(&mut c.build_paths); let options = args.options()?; - let what = args.cmd.describe(); + let what = cmd.describe(); let verbose = c.verbose; - let recursive = args.cmd.shared().recursive; + let recursive = cmd.shared().recursive; let mut entrys = Vec::new(); @@ -638,7 +672,7 @@ async fn main_with_out(io: &mut Io<'_>, entry: &mut Entry<'_>, mut args: Args) - entrys.push(EntryPoint { item, paths }); } - match run_path(io, &c, &args, entry, &options, entrys).await? { + match run_path(io, &c, cmd, entry, &options, entrys).await? { ExitCode::Success => (), other => { return Ok(other); @@ -652,7 +686,7 @@ async fn main_with_out(io: &mut Io<'_>, entry: &mut Entry<'_>, mut args: Args) - async fn run_path( io: &mut Io<'_>, c: &Config, - args: &Args, + cmd: &Command, entry: &mut Entry<'_>, options: &Options, entrys: I, @@ -660,18 +694,18 @@ async fn run_path( where I: IntoIterator, { - match &args.cmd { - Command::Check(flags) => { + match cmd { + Command::Check(f) => { for e in entrys { for path in &e.paths { - match check::run(io, entry, c, flags, options, path)? { + match check::run(io, entry, c, &f.command, &f.shared, options, path)? { ExitCode::Success => (), other => return Ok(other), } } } } - Command::Doc(flags) => return doc::run(io, entry, c, flags, options, entrys), + Command::Doc(f) => return doc::run(io, entry, c, &f.command, &f.shared, options, entrys), Command::Fmt(flags) => { let mut paths = vec![]; for e in entrys { @@ -680,20 +714,26 @@ where } } - return format::run(io, &paths, flags); + return format::run(io, &paths, &flags.command); } - Command::Test(flags) => { + Command::Test(f) => { for e in entrys { for path in &e.paths { let capture = crate::modules::capture_io::CaptureIo::new(); - let context = flags.shared.context(entry, c, Some(&capture))?; + let context = f.shared.context(entry, c, Some(&capture))?; - let load = - loader::load(io, &context, args, options, path, visitor::Attribute::Test)?; + let load = loader::load( + io, + &context, + &f.shared, + options, + path, + visitor::Attribute::Test, + )?; match tests::run( io, - flags, + &f.command, &context, Some(&capture), load.unit, @@ -708,18 +748,24 @@ where } } } - Command::Bench(flags) => { + Command::Bench(f) => { for e in entrys { for path in &e.paths { let capture_io = crate::modules::capture_io::CaptureIo::new(); - let context = flags.shared.context(entry, c, Some(&capture_io))?; + let context = f.shared.context(entry, c, Some(&capture_io))?; - let load = - loader::load(io, &context, args, options, path, visitor::Attribute::Bench)?; + let load = loader::load( + io, + &context, + &f.shared, + options, + path, + visitor::Attribute::Bench, + )?; match benches::run( io, - flags, + &f.command, &context, Some(&capture_io), load.unit, @@ -734,23 +780,29 @@ where } } } - Command::Run(flags) => { - let context = flags.shared.context(entry, c, None)?; + Command::Run(f) => { + let context = f.shared.context(entry, c, None)?; for e in entrys { for path in &e.paths { - let load = - loader::load(io, &context, args, options, path, visitor::Attribute::None)?; + let load = loader::load( + io, + &context, + &f.shared, + options, + path, + visitor::Attribute::None, + )?; - match run::run(io, c, flags, &context, load.unit, &load.sources).await? { + match run::run(io, c, &f.command, &context, load.unit, &load.sources).await? { ExitCode::Success => (), other => return Ok(other), } } } } - Command::LanguageServer(flags) => { - let context = flags.shared.context(entry, c, None)?; + Command::LanguageServer(shared) => { + let context = shared.context(entry, c, None)?; languageserver::run(context).await?; } } diff --git a/crates/rune/src/cli/benches.rs b/crates/rune/src/cli/benches.rs index a7cafc286..520a670fc 100644 --- a/crates/rune/src/cli/benches.rs +++ b/crates/rune/src/cli/benches.rs @@ -5,7 +5,7 @@ use std::time::Instant; use clap::Parser; -use crate::cli::{ExitCode, Io, SharedFlags}; +use crate::cli::{ExitCode, Io}; use crate::compile::{Item, ItemBuf}; use crate::modules::capture_io::CaptureIo; use crate::runtime::{Function, Unit, Value}; @@ -22,9 +22,6 @@ pub(super) struct Flags { /// Iterations to run of the benchmark #[arg(long, default_value = "100")] iterations: u32, - - #[command(flatten)] - pub(super) shared: SharedFlags, } #[derive(Default, Any)] diff --git a/crates/rune/src/cli/check.rs b/crates/rune/src/cli/check.rs index df026490c..ed006b0a2 100644 --- a/crates/rune/src/cli/check.rs +++ b/crates/rune/src/cli/check.rs @@ -13,9 +13,6 @@ pub(super) struct Flags { /// Exit with a non-zero exit-code even for warnings #[arg(long)] warnings_are_errors: bool, - - #[command(flatten)] - pub(super) shared: SharedFlags, } pub(super) fn run( @@ -23,12 +20,13 @@ pub(super) fn run( entry: &mut Entry<'_>, c: &Config, flags: &Flags, + shared: &SharedFlags, options: &Options, path: &Path, ) -> Result { writeln!(io.stdout, "Checking: {}", path.display())?; - let context = flags.shared.context(entry, c, None)?; + let context = shared.context(entry, c, None)?; let source = Source::from_path(path).with_context(|| format!("reading file: {}", path.display()))?; @@ -37,7 +35,7 @@ pub(super) fn run( sources.insert(source); - let mut diagnostics = if flags.shared.warnings || flags.warnings_are_errors { + let mut diagnostics = if shared.warnings || flags.warnings_are_errors { Diagnostics::new() } else { Diagnostics::without_warnings() diff --git a/crates/rune/src/cli/doc.rs b/crates/rune/src/cli/doc.rs index 2119cabd0..87c59f5e4 100644 --- a/crates/rune/src/cli/doc.rs +++ b/crates/rune/src/cli/doc.rs @@ -19,8 +19,6 @@ pub(super) struct Flags { /// Open the generated documentation in a browser. #[arg(long)] open: bool, - #[command(flatten)] - pub(super) shared: SharedFlags, } pub(super) fn run( @@ -28,6 +26,7 @@ pub(super) fn run( entry: &mut Entry<'_>, c: &Config, flags: &Flags, + shared: &SharedFlags, options: &Options, entrys: I, ) -> Result @@ -56,7 +55,7 @@ where writeln!(io.stdout, "Building documentation: {}", root.display())?; - let context = flags.shared.context(entry, c, None)?; + let context = shared.context(entry, c, None)?; let mut visitors = Vec::new(); @@ -70,7 +69,7 @@ where sources.insert(source); } - let mut diagnostics = if flags.shared.warnings || flags.warnings_are_errors { + let mut diagnostics = if shared.warnings || flags.warnings_are_errors { Diagnostics::new() } else { Diagnostics::without_warnings() diff --git a/crates/rune/src/cli/format.rs b/crates/rune/src/cli/format.rs index c85048e5f..7732827bc 100644 --- a/crates/rune/src/cli/format.rs +++ b/crates/rune/src/cli/format.rs @@ -3,7 +3,7 @@ use clap::Parser; use std::io::Write; use std::path::PathBuf; -use crate::cli::{ExitCode, Io, SharedFlags}; +use crate::cli::{ExitCode, Io}; use crate::termcolor::WriteColor; use crate::Source; @@ -12,10 +12,8 @@ pub(super) struct Flags { /// Exit with a non-zero exit-code even for warnings #[arg(long)] warnings_are_errors: bool, - - #[command(flatten)] - pub(super) shared: SharedFlags, - + /// Perform format checking. If there's any files which needs to be changed + /// returns a non-successful exitcode. #[arg(long)] check: bool, } diff --git a/crates/rune/src/cli/languageserver.rs b/crates/rune/src/cli/languageserver.rs index 67195dd2c..fa0920606 100644 --- a/crates/rune/src/cli/languageserver.rs +++ b/crates/rune/src/cli/languageserver.rs @@ -1,15 +1,7 @@ use anyhow::Result; -use clap::Parser; -use crate::cli::SharedFlags; use crate::{Context, Options}; -#[derive(Parser, Debug, Clone)] -pub(super) struct Flags { - #[command(flatten)] - pub(super) shared: SharedFlags, -} - pub(super) async fn run(context: Context) -> Result<()> { let options = Options::default(); crate::languageserver::run(context, options).await?; diff --git a/crates/rune/src/cli/loader.rs b/crates/rune/src/cli/loader.rs index 353daf687..2c24f5bc1 100644 --- a/crates/rune/src/cli/loader.rs +++ b/crates/rune/src/cli/loader.rs @@ -7,7 +7,7 @@ use std::{path::Path, sync::Arc}; use anyhow::{anyhow, Context as _, Result}; -use crate::cli::{visitor, Args, Io}; +use crate::cli::{visitor, Io, SharedFlags}; use crate::compile::{FileSourceLoader, ItemBuf}; use crate::Diagnostics; use crate::{Context, Hash, Options, Source, Sources, Unit}; @@ -22,13 +22,11 @@ pub(super) struct Load { pub(super) fn load( io: &mut Io<'_>, context: &Context, - args: &Args, + shared: &SharedFlags, options: &Options, path: &Path, attribute: visitor::Attribute, ) -> Result { - let shared = args.cmd.shared(); - let bytecode_path = path.with_extension("rnc"); let source = diff --git a/crates/rune/src/cli/run.rs b/crates/rune/src/cli/run.rs index 669a1fe15..802b9f733 100644 --- a/crates/rune/src/cli/run.rs +++ b/crates/rune/src/cli/run.rs @@ -5,7 +5,7 @@ use std::time::Instant; use anyhow::Result; use clap::Parser; -use crate::cli::{Config, ExitCode, Io, SharedFlags}; +use crate::cli::{Config, ExitCode, Io}; use crate::runtime::{VmError, VmExecution, VmResult}; use crate::{Context, Sources, Unit, Value, Vm}; @@ -46,8 +46,6 @@ pub(super) struct Flags { /// Include source code references where appropriate (only available if -O debug-info=true). #[arg(long)] with_source: bool, - #[command(flatten)] - pub(super) shared: SharedFlags, } impl Flags { diff --git a/crates/rune/src/cli/tests.rs b/crates/rune/src/cli/tests.rs index 950ab9f83..ffd9c6c13 100644 --- a/crates/rune/src/cli/tests.rs +++ b/crates/rune/src/cli/tests.rs @@ -5,7 +5,7 @@ use std::time::Instant; use anyhow::Result; use clap::Parser; -use crate::cli::{ExitCode, Io, SharedFlags}; +use crate::cli::{ExitCode, Io}; use crate::compile::ItemBuf; use crate::modules::capture_io::CaptureIo; use crate::runtime::{Unit, Value, Vm, VmError, VmResult}; @@ -20,9 +20,6 @@ pub(super) struct Flags { /// Run all tests regardless of failure #[arg(long)] no_fail_fast: bool, - - #[command(flatten)] - pub(super) shared: SharedFlags, } #[derive(Debug)] diff --git a/editors/code/package.json b/editors/code/package.json index 47abcf783..35d4cd8c5 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -109,14 +109,17 @@ "default": true, "type": "boolean" }, + "rune.server.cargoPackage": { + "title": "Cargo Package", + "markdownDescription": "Set the extension to use the given cargo package to run the language server, rather than a downloaded rune-languageserver.", + "type": "string", + "default": "" + }, "rune.server.path": { - "type": [ - "null", - "string" - ], + "markdownDescription": "Path to rune-languageserver executable (will be downloaded by default). If this is set, then `#rune.updates.channel#` setting is not used", + "type": "string", "scope": "machine-overridable", - "default": null, - "markdownDescription": "Path to rune-languageserver executable (will be downloaded by default). If this is set, then `#rune.updates.channel#` setting is not used" + "default": "" }, "rune.server.extraEnv": { "type": [ diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index 4f4b51d1a..b3eec14c3 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts @@ -7,15 +7,18 @@ import { log } from "./util"; export function createClient( serverPath: string, - extraEnv: Env + extraEnv: Env, + args?: string[] ): lc.LanguageClient { const newEnv = substituteVariablesInEnv(Object.assign({}, process.env, extraEnv)); log.debug('newEnv', newEnv); const run: lc.Executable = { command: serverPath, - options: { env: newEnv } + options: { env: newEnv }, + args }; + const serverOptions: lc.ServerOptions = { run, debug: run, diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 9037c6b8d..acb02e1a8 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -156,6 +156,10 @@ export class Config { return this.get("server.path"); } + get serverCargoPackage() { + return this.get("server.cargoPackage"); + } + get serverExtraEnv(): Env { const extraEnv = this.get<{ [key: string]: string | number } | null>("server.extraEnv") ?? {}; diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index dde12e350..73bb4899c 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -1,6 +1,8 @@ import * as vscode from "vscode"; import * as lc from "vscode-languageclient/node"; import * as os from "os"; +import * as cp from "child_process"; +import * as rl from "readline"; import { log, isValidExecutable, assert, uriExists, setContextValue } from "./util"; import { download, fetchRelease } from "./net"; @@ -10,12 +12,24 @@ import { PersistentState } from "./persistent_state"; const RUNE_PROJECT_CONTEXT_NAME = "inRuneProject"; +interface FoundBinary { + kind: "languageserver" | "cli", + path: string +} + +interface LastClientError { + message: string, + tooltip?: string, +} + export class Ctx { readonly context: vscode.ExtensionContext; readonly config: Config; readonly state: PersistentState; readonly statusBar: vscode.StatusBarItem; - + + // Stored initialization error. Cleared when reloaded. + lastClientError: LastClientError | null; client: lc.LanguageClient | null; commands: {[key:string]: lc.Disposable}; stopped: boolean; @@ -25,6 +39,7 @@ export class Ctx { this.config = new Config(context); this.state = new PersistentState(context.globalState); this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + this.lastClientError = null; this.client = null; this.commands = {}; this.stopped = false; @@ -69,57 +84,230 @@ export class Ctx { } async setupClient() { + this.lastClientError = null; + try { - const serverPath = await this.bootstrap(); - this.client = createClient(serverPath, this.config.serverExtraEnv); + const binary = await this.bootstrap(); + + switch (binary.kind) { + case "languageserver": + this.client = createClient(binary.path, this.config.serverExtraEnv); + break; + case "cli": + this.client = createClient(binary.path, this.config.serverExtraEnv, ["language-server"]); + break; + } + await this.client.start(); await setContextValue(RUNE_PROJECT_CONTEXT_NAME, true); - } catch(err) { + } catch (err: any) { let message = "Bootstrap error! "; message += 'See the logs in "OUTPUT > Rune" (should open automatically). '; message += 'To enable verbose logs use { "rune-vscode.trace.extension": true }'; vscode.window.showErrorMessage(message); + + this.lastClientError = { + message: err.toString(), + tooltip: err.details && err.details as string || null, + }; + log.error(err); } } - async bootstrap(): Promise { - const path = await this.getServer(); + async bootstrap(): Promise { + const binary = await this.getServer(); - if (!path) { + if (!binary) { throw new Error("Rune Language Server is not available."); } - log.info("Using server binary at", path); + log.info("Using server binary at", binary.path); - if (!isValidExecutable(path)) { + if (!isValidExecutable(binary.path)) { if (this.config.serverPath) { - throw new Error(`Failed to execute ${path} --version. \`config.server.path\` has been set explicitly.\ + throw new Error(`Failed to execute ${binary.path} --version. \`config.server.path\` has been set explicitly.\ Consider removing this config or making a valid server binary available at that path.`); + } else if (this.config.serverCargoPackage) { + throw new Error(`Failed to execute ${binary.path} --version. \`config.server.package\` has been set explicitly.\ + Consider removing this config or making a valid cargo package.`); } else { - throw new Error(`Failed to execute ${path} --version`); + throw new Error(`Failed to execute ${binary.path} --version`); } } - return path; + return binary; } - async getServer(): Promise { + /** + * Run cargo --version to determine if the command is available to begin + * with. + * + * @returns stdout from cargo --version + */ + async cargoVersion(): Promise { + let cargoVersion = cp.spawn("cargo", ["--version"]); + cargoVersion.stdout.setEncoding('utf-8'); + + let [code, out, err]: [number | null, string, string] = await new Promise((resolve, _) => { + let stdout = ""; + let stderr = ""; + + cargoVersion.stdout.on("data", (chunk) => { + stdout += chunk; + }); + + cargoVersion.stderr.on("data", (chunk) => { + stderr += chunk; + }); + + cargoVersion.on("exit", (code) => { + resolve([code, stdout, stderr]); + }); + }); + + if (code != 0) { + let e = new Error(`cargo --version: failed (${code})`); + // Smuggle details. + (e as any).details = err; + throw e; + } + + return out.trim(); + } + + /** + * + * @param name package to build. + * @returns a string to the path of the built binary. + */ + async buildCargoPackage(name: string): Promise { + interface Target { + kind: [string], + name: string, + } + + interface ReasonOutput { + reason: string, + } + + interface CompilerArtifact extends ReasonOutput { + package_id: string, + target: Target, + executable?: string, + } + + let cargo = await this.cargoVersion(); + log.info(`Cargo: ${cargo}`); + + if (!vscode.workspace.workspaceFolders) { + throw new Error("No workspace folders"); + } + + let folder = vscode.workspace.workspaceFolders[0]; + log.info(folder.uri.fsPath); + + let child = cp.spawn("cargo", ["build", "-p", name, "--message-format", "json"], { cwd: folder.uri.fsPath }); + child.stderr.setEncoding('utf8'); + child.stdout.setEncoding('utf8'); + let out = rl.createInterface(child.stdout, child.stdin); + + this.statusBar.text = `rune: cargo build -p ${name}`; + this.statusBar.tooltip = `rune: building package ${name}, to use as rune language server`; + this.statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); + this.statusBar.show(); + + let executable = null; + let error = ""; + + child.stderr.on('data', (data) => { + error += data; + }); + + let code = await new Promise((resolve, reject) => { + out.on('line', (data) => { + log.debug(data); + + let output = JSON.parse(data) as ReasonOutput; + + if (output.reason == "compiler-artifact") { + let artifact = output as CompilerArtifact; + this.statusBar.text = `rune: cargo (${artifact.target.name})`; + + let [id, ...rest] = artifact.package_id.split(" "); + + if (id == name && artifact.target.kind.includes("bin")) { + executable = artifact.executable; + } + } + }); + + out.on('close', () => { + log.debug("Closed"); + }); + + child.on("exit", (code) => { + resolve(code); + }); + }); + + if (code !== 0) { + let e = new Error(`cargo build -p ${name}: failed (${code})`); + // Smuggle details. + (e as any).details = error; + throw e; + } + + this.statusBar.hide(); + + if (!executable) { + log.info("No executable"); + return null; + } + + log.info(`Executable: ${executable}`); + return executable; + } + + async getServer(): Promise { // use explicit path from config const explicitPath = this.serverPath(); if (explicitPath) { if (explicitPath.startsWith("~/")) { - return os.homedir() + explicitPath.slice("~".length); + return { kind: "languageserver", path: os.homedir() + explicitPath.slice("~".length) }; } - return explicitPath; + return { kind: "languageserver", path: explicitPath }; + } + + const cargoPackage = this.config.serverCargoPackage; + + if (cargoPackage) { + let path = await this.buildCargoPackage(cargoPackage); + + if (!path) { + return null; + } + + return { kind: "cli", path }; + } + + let path = await this.downloadServer(); + + if (!path) { + return null; } + + return { kind: "languageserver", path }; + } + async downloadServer(): Promise { // unknown platform => no download available const platform = detectPlatform(); + if (!platform) { - return undefined; + return null; } // platform specific binary name / path @@ -193,8 +381,25 @@ export class Ctx { } setupStatusBar() { - this.statusBar.show(); - this.statusBar.text = "rune-languageserver"; + this.statusBar.text = "rune"; + + let tooltipExtra = null; + + if (!!this.lastClientError) { + this.statusBar.text += ": " + this.lastClientError.message; + + if (this.lastClientError.tooltip) { + tooltipExtra = "#### Error\n\n" + this.lastClientError.tooltip.split("\n").map((s) => s.trim()).join("\n"); + } else { + tooltipExtra = "#### Error"; + } + + this.statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground'); + } else { + tooltipExtra = "rune language server"; + this.statusBar.backgroundColor = undefined; + } + this.statusBar.command = "rune-vscode.reload"; let tooltip = new vscode.MarkdownString("", true); @@ -202,13 +407,20 @@ export class Ctx { tooltip.appendMarkdown("\n\n[Restart server](command:rune-vscode.reload)"); - if (!this.stopped) { - tooltip.appendMarkdown("\n\n[Stop server](command:rune-vscode.stopServer)"); - } else { - tooltip.appendMarkdown("\n\n[Start server](command:rune-vscode.startServer)"); + if (!!this.lastClientError) { + if (!this.stopped) { + tooltip.appendMarkdown("\n\n[Stop server](command:rune-vscode.stopServer)"); + } else { + tooltip.appendMarkdown("\n\n[Start server](command:rune-vscode.startServer)"); + } + } + + if (tooltipExtra) { + tooltip.appendMarkdown("\n\n" + tooltipExtra); } this.statusBar.tooltip = tooltip; + this.statusBar.show(); } /**