diff --git a/Cargo.lock b/Cargo.lock index 6fcbda81..b6a5e7f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -129,7 +129,9 @@ version = "0.9.0" dependencies = [ "anyhow", "bincode", + "clap", "compile_commands", + "dialoguer", "dirs", "flexi_logger", "home", @@ -357,6 +359,19 @@ dependencies = [ "serde_json", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -427,6 +442,20 @@ dependencies = [ "syn", ] +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "fuzzy-matcher", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + [[package]] name = "dirs" version = "5.0.1" @@ -477,6 +506,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -632,6 +667,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -1729,6 +1773,12 @@ dependencies = [ "serde", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" @@ -1993,6 +2043,16 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -2164,6 +2224,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 6e5d34a6..b72e61b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,9 @@ license = "BSD-2-Clause" [workspace.dependencies] anyhow = "1.0.86" bincode = "1.3.3" +dirs = "5.0.1" +toml = "0.8.1" +clap = { version = "4.5.8", features = ["derive", "cargo"] } serde = { version = "1.0.204", features = ["derive"] } [workspace.lints.clippy] diff --git a/README.md b/README.md index b87c99cd..b51672fa 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,28 @@ created for different sub-directories or files within your project as `project`s Source files not contained within any `project` configs will use the default configuration if provided. +#### Config Builder + +Creating a `.asm-lsp.toml` file manually is fine, but can be error-prone as projects +grow in complexity. Running `asm-lsp gen-config` will walk you through the creation +of a config interactively, with informative prompts and extra validation checks +along the way. + +``` +$ asm-lsp gen-config --help +Generate a .asm-lsp.toml config file + +Usage: asm-lsp gen-config [OPTIONS] + +Options: + -o, --output-dir Directory to place .asm-lsp.toml into. (Default is the current directory) + -g, --global-cfg Place the config in the global config directory + -p, --project-path Path to the project this config is being generated for. (Default is the current directory) + -w, --overwrite Overwrite any existing .asm-lsp.toml in the target directory + -q, --quiet Don't display the generated config file after generation + -h, --help Print help +``` + #### NOTE If the server reads in an invalid configuration file, it will display an error diff --git a/asm-lsp/Cargo.toml b/asm-lsp/Cargo.toml index 9f66436b..963a9a7e 100644 --- a/asm-lsp/Cargo.toml +++ b/asm-lsp/Cargo.toml @@ -25,6 +25,9 @@ path = "bin/main.rs" anyhow.workspace = true bincode.workspace = true serde.workspace = true +dirs.workspace = true +toml.workspace = true +clap.workspace = true # write to stderr instead of stdout flexi_logger = "0.29.3" log = { version = "0.4.17" } @@ -35,10 +38,8 @@ reqwest = { version = "0.12.8", features = ["blocking"] } strum = "0.26.3" strum_macros = "0.26.4" serde_json = "1.0.94" -toml = "0.8.1" home = "0.5.5" once_cell = "1.18.0" -dirs = "5.0.1" symbolic = { version = "12.8.0", features = ["demangle"] } symbolic-demangle = "12.8.0" url-escape = "0.1.1" @@ -47,6 +48,7 @@ lsp-textdocument = "0.4.0" tree-sitter = "0.22.6" tree-sitter-asm = "0.22.6" compile_commands = "0.3.0" +dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } [dev-dependencies] mockito = "1.2.0" diff --git a/asm-lsp/bin/main.rs b/asm-lsp/bin/main.rs index 626bb769..65455a21 100644 --- a/asm-lsp/bin/main.rs +++ b/asm-lsp/bin/main.rs @@ -1,6 +1,8 @@ use std::thread::sleep; use std::time::Duration; +use asm_lsp::config_builder::{gen_config, GenerateArgs, GenerateOpts}; +use asm_lsp::run_info; use asm_lsp::types::LspClient; use asm_lsp::handle::{handle_notification, handle_request}; @@ -9,6 +11,7 @@ use asm_lsp::{ send_notification, DocumentStore, RootConfig, ServerStore, }; +use clap::{Command, FromArgMatches as _, Subcommand}; use lsp_types::{ CompletionItemKind, CompletionOptions, CompletionOptionsCompletionItem, DiagnosticOptions, DiagnosticServerCapabilities, HoverProviderCapability, InitializeParams, MessageType, OneOf, @@ -20,7 +23,61 @@ use anyhow::Result; use log::{info, warn}; use lsp_server::{Connection, Message}; -/// Entry point of the server. Connects to the client, loads documentation resources, +#[derive(Subcommand)] +enum Commands { + GenConfig(GenerateArgs), + /// Print information about asm-lsp + Info, + #[clap(hide(true))] + Run, +} + +/// Entry point of the lsp. Runs a subcommand is specified, otherwise starts the +/// lsp server +/// +/// # Errors +/// +/// Returns `Err` on failure to parse arguments, if a sub command fails, or if the +/// server returns an error +pub fn main() -> Result<()> { + let cli = Command::new("asm-lsp").subcommand_required(false); + let cli = Commands::augment_subcommands(cli); + + let command = match Commands::from_arg_matches(&cli.get_matches()) { + Ok(cmd) => cmd, + Err(e) => { + // If no subcommand is provided, run the server as normal + if e.kind() == clap::error::ErrorKind::MissingSubcommand { + Commands::Run + } else { + eprintln!("{e}"); + std::process::exit(1); + } + } + }; + match command { + Commands::GenConfig(args) => { + let opts: GenerateOpts = match args.try_into() { + Ok(opts) => opts, + Err(e) => { + eprintln!("{e}"); + std::process::exit(1); + } + }; + + if let Err(e) = gen_config(&opts) { + eprintln!("Error: {e}"); + std::process::exit(1); + } + } + Commands::Info => run_info(), + Commands::Run => run_lsp()?, + } + + Ok(()) +} + +/// Entry point of the lsp server. Connects to the client, loads documentation resources, /// and then enters the main loop /// /// # Errors @@ -30,7 +87,7 @@ use lsp_server::{Connection, Message}; /// # Panics /// /// Panics if JSON serialization of the server capabilities fails -pub fn main() -> Result<()> { +pub fn run_lsp() -> Result<()> { // initialisation // Set up logging. Because `stdio_transport` gets a lock on stdout and stdin, we must have our // logging only write out to stderr. diff --git a/asm-lsp/config_builder.rs b/asm-lsp/config_builder.rs new file mode 100644 index 00000000..57f0358f --- /dev/null +++ b/asm-lsp/config_builder.rs @@ -0,0 +1,597 @@ +use std::path::Path; +use std::string::ToString; +use std::{env::current_dir, path::PathBuf}; + +use anyhow::{anyhow, Result}; +use clap::{arg, Args}; +use dirs::config_dir; + +use crate::types::{Arch, Assembler, Config, ConfigOptions, ProjectConfig, RootConfig}; + +use dialoguer::{theme::ColorfulTheme, Confirm, FuzzySelect, Input}; + +const ARCH_LIST: [Arch; 7] = [ + Arch::X86, + Arch::X86_64, + Arch::X86_AND_X86_64, + Arch::ARM, + Arch::ARM64, + Arch::RISCV, + Arch::Z80, +]; + +const ASSEMBLER_LIST: [Assembler; 4] = [ + Assembler::Gas, + Assembler::Go, + Assembler::Masm, + Assembler::Nasm, +]; + +#[derive(Args, Debug, Clone)] +#[command(about = "Generate a .asm-lsp.toml config file")] +pub struct GenerateArgs { + #[arg( + long, + short, + help = "Directory to place .asm-lsp.toml into. (Default is the current directory)" + )] + pub output_dir: Option, + #[arg( + long, + short, + conflicts_with = "output_dir", + help = "Place the config in the global config directory" + )] + pub global_cfg: bool, + #[arg( + long, + short, + conflicts_with = "global_cfg", + help = "Path to the project this config is being generated for. (Default is the current directory)" + )] + pub project_path: Option, + #[arg( + short = 'w', + long, + help = "Overwrite any existing .asm-lsp.toml in the target directory" + )] + pub overwrite: bool, + #[arg( + short, + long, + help = "Don't display the generated config file after generation" + )] + pub quiet: bool, +} + +#[derive(Debug, Clone)] +pub struct GenerateOpts { + pub output_path: PathBuf, + pub project_path: PathBuf, + pub overwrite: bool, + pub quiet: bool, +} + +impl TryFrom for GenerateOpts { + type Error = String; + fn try_from(value: GenerateArgs) -> Result { + let output_path = { + if value.global_cfg { + let mut path = config_dir().ok_or_else(|| "Failed to detect config directory, try specifying it manually with `--output_dir`".to_string())?; + path.push("asm-lsp"); + path.push(".asm-lsp.toml"); + path + } else if let Some(path) = value.output_dir.as_ref() { + let mut canonicalized_path = path.canonicalize().map_err(|e| { + format!( + "Failed to canonicalize target path: \"{}\" -- {e}", + path.display() + ) + })?; + if !canonicalized_path.is_dir() { + let gave_file_name = canonicalized_path.ends_with(".asm-lsp.toml"); + return Err(format!( + "Target path \"{}\" is not a directory.{}", + canonicalized_path.display(), + if gave_file_name { + " Hint: Don't include the filename \".asm-lsp.toml\" at the end of your target path." + } else { + "" + } + )); + } + canonicalized_path.push(".asm-lsp.toml"); + canonicalized_path + } else { + let mut path = current_dir() + .map_err(|e| format!("Failed to detect current directory -- {e}"))?; + path.push(".asm-lsp.toml"); + path + } + }; + let project_path = { + if let Some(path) = value.project_path.as_ref().or(value.output_dir.as_ref()) { + let canonicalized_path = path.canonicalize().map_err(|e| { + format!( + "Failed to canonicalize project path: \"{}\" -- {e}", + path.display() + ) + })?; + if !canonicalized_path.is_dir() { + return Err(format!( + "Project path \"{}\" is not a directory.", + canonicalized_path.display(), + )); + } + canonicalized_path + } else { + current_dir().map_err(|e| format!("Failed to detect current directory -- {e}"))? + } + }; + + Ok(Self { + output_path, + project_path, + overwrite: value.overwrite, + quiet: value.quiet, + }) + } +} + +fn prompt_arch() -> Arch { + let arch_choices: Vec = ARCH_LIST.iter().map(ToString::to_string).collect(); + let arch_selection = FuzzySelect::with_theme(&ColorfulTheme::default()) + .with_prompt("Select architecture") + .default(0) + .items(&arch_choices[..]) + .interact() + .unwrap(); + + ARCH_LIST[arch_selection] +} + +fn prompt_assembler() -> Assembler { + let assem_choices: Vec = ASSEMBLER_LIST.iter().map(ToString::to_string).collect(); + let assem_selection = FuzzySelect::with_theme(&ColorfulTheme::default()) + .with_prompt("Select assembler") + .default(0) + .items(&assem_choices[..]) + .interact() + .unwrap(); + + ASSEMBLER_LIST[assem_selection] +} + +fn prompt_project_path(opts: &GenerateOpts) -> PathBuf { + println!("Provide a project path:"); + let fallback_enter = |true_path: &mut PathBuf| { + println!( + "Warning: Failed to create directory reader for path \"{}\"", + true_path.display() + ); + let remaining_path: String = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter remaining path (Enter an empty string to use the current path)") + .allow_empty(true) + .interact_text() + .unwrap(); + true_path.push(remaining_path); + }; + let mut true_path = opts.project_path.clone(); + let mut display_entries = Vec::new(); + let mut path_entries = Vec::new(); + loop { + let selection_text = format!("{}", true_path.display()); + // Dummy entry to account for the accept option as the first displayed + // option + path_entries.push(PathBuf::new()); + display_entries.push("