From 25dd4c9f093636048f2657b352dfd0d6247f8d50 Mon Sep 17 00:00:00 2001 From: WillLillis Date: Thu, 17 Oct 2024 01:46:33 -0400 Subject: [PATCH] WIP: multi-config --- src/bin/main.rs | 87 ++++---- src/handle.rs | 1 + src/lsp.rs | 521 ++++++++++++++++++++++++++++++++---------------- src/test.rs | 16 +- src/types.rs | 280 +++++++++++++++++++++++++- 5 files changed, 664 insertions(+), 241 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index dc8b2593..2f01c131 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -10,9 +10,9 @@ use asm_lsp::handle::{ handle_references_request, handle_signature_help_request, }; use asm_lsp::{ - get_compile_cmds, get_completes, get_config, get_include_dirs, instr_filter_targets, - populate_name_to_directive_map, populate_name_to_instruction_map, - populate_name_to_register_map, Arch, Assembler, Config, Instruction, NameToInfoMaps, TreeStore, + get_compile_cmds, get_completes, get_config, get_include_dirs, populate_name_to_directive_map, + populate_name_to_instruction_map, populate_name_to_register_map, Arch, Assembler, Instruction, + NameToInfoMaps, RootConfig, TreeStore, }; use compile_commands::{CompilationDatabase, SourceFile}; @@ -53,7 +53,7 @@ pub fn main() -> Result<()> { flexi_logger::Logger::try_with_str("info")?.start()?; // LSP server initialisation ------------------------------------------------------------------ - info!("Starting asm_lsp..."); + info!("Starting asm_lsp-{}", env!("CARGO_PKG_VERSION")); // Create the transport let (connection, _io_threads) = Connection::stdio(); @@ -119,7 +119,7 @@ pub fn main() -> Result<()> { if let Some(ref client_info) = params.client_info { if client_info.name.eq("helix") { info!("Helix LSP client detected"); - config.client = Some(LspClient::Helix); + config.set_client(LspClient::Helix); } } @@ -127,17 +127,10 @@ pub fn main() -> Result<()> { // create a map of &Instruction_name -> &Instruction - Use that in user queries // The Instruction(s) themselves are stored in a vector and we only keep references to the // former map - let x86_instructions = if config.instruction_sets.x86.unwrap_or(false) { + let x86_instructions = if config.is_isa_enabled(Arch::X86) { let start = std::time::Instant::now(); let x86_instrs = include_bytes!("../../docs_store/opcodes/serialized/x86"); - let instrs = bincode::deserialize::>(x86_instrs)? - .into_iter() - .map(|instruction| { - // filter out assemblers by user config - instr_filter_targets(&instruction, &config) - }) - .filter(|instruction| !instruction.forms.is_empty()) - .collect(); + let instrs = bincode::deserialize::>(x86_instrs)?; info!( "x86 instruction set loaded in {}ms", start.elapsed().as_millis() @@ -147,17 +140,10 @@ pub fn main() -> Result<()> { Vec::new() }; - let x86_64_instructions = if config.instruction_sets.x86_64.unwrap_or(false) { + let x86_64_instructions = if config.is_isa_enabled(Arch::X86_64) { let start = std::time::Instant::now(); let x86_64_instrs = include_bytes!("../../docs_store/opcodes/serialized/x86_64"); - let instrs = bincode::deserialize::>(x86_64_instrs)? - .into_iter() - .map(|instruction| { - // filter out assemblers by user config - instr_filter_targets(&instruction, &config) - }) - .filter(|instruction| !instruction.forms.is_empty()) - .collect(); + let instrs = bincode::deserialize::>(x86_64_instrs)?; info!( "x86-64 instruction set loaded in {}ms", start.elapsed().as_millis() @@ -167,17 +153,10 @@ pub fn main() -> Result<()> { Vec::new() }; - let z80_instructions = if config.instruction_sets.z80.unwrap_or(false) { + let z80_instructions = if config.is_isa_enabled(Arch::Z80) { let start = std::time::Instant::now(); let z80_instrs = include_bytes!("../../docs_store/opcodes/serialized/z80"); - let instrs = bincode::deserialize::>(z80_instrs)? - .into_iter() - .map(|instruction| { - // filter out assemblers by user config - instr_filter_targets(&instruction, &config) - }) - .filter(|instruction| !instruction.forms.is_empty()) - .collect(); + let instrs = bincode::deserialize::>(z80_instrs)?; info!( "z80 instruction set loaded in {}ms", start.elapsed().as_millis() @@ -187,7 +166,7 @@ pub fn main() -> Result<()> { Vec::new() }; - let arm_instructions = if config.instruction_sets.arm.unwrap_or(false) { + let arm_instructions = if config.is_isa_enabled(Arch::ARM) { let start = std::time::Instant::now(); let arm_instrs = include_bytes!("../../docs_store/opcodes/serialized/arm"); // NOTE: No need to filter these instructions by assembler like we do for @@ -202,7 +181,7 @@ pub fn main() -> Result<()> { Vec::new() }; - let riscv_instructions = if config.instruction_sets.riscv.unwrap_or(false) { + let riscv_instructions = if config.is_isa_enabled(Arch::RISCV) { let start = std::time::Instant::now(); let riscv_instrs = include_bytes!("../../docs_store/opcodes/serialized/riscv"); // NOTE: No need to filter these instructions by assembler like we do for @@ -246,7 +225,7 @@ pub fn main() -> Result<()> { // create a map of &Register_name -> &Register - Use that in user queries // The Register(s) themselves are stored in a vector and we only keep references to the // former map - let x86_registers = if config.instruction_sets.x86.unwrap_or(false) { + let x86_registers = if config.is_isa_enabled(Arch::X86) { let start = std::time::Instant::now(); let regs_x86 = include_bytes!("../../docs_store/registers/serialized/x86"); let regs = bincode::deserialize(regs_x86)?; @@ -259,7 +238,7 @@ pub fn main() -> Result<()> { Vec::new() }; - let x86_64_registers = if config.instruction_sets.x86_64.unwrap_or(false) { + let x86_64_registers = if config.is_isa_enabled(Arch::X86_64) { let start = std::time::Instant::now(); let regs_x86_64 = include_bytes!("../../docs_store/registers/serialized/x86_64"); let regs = bincode::deserialize(regs_x86_64)?; @@ -272,7 +251,7 @@ pub fn main() -> Result<()> { Vec::new() }; - let z80_registers = if config.instruction_sets.z80.unwrap_or(false) { + let z80_registers = if config.is_isa_enabled(Arch::Z80) { let start = std::time::Instant::now(); let regs_z80 = include_bytes!("../../docs_store/registers/serialized/z80"); let regs = bincode::deserialize(regs_z80)?; @@ -285,7 +264,7 @@ pub fn main() -> Result<()> { Vec::new() }; - let arm_registers = if config.instruction_sets.arm.unwrap_or(false) { + let arm_registers = if config.is_isa_enabled(Arch::ARM) { let start = std::time::Instant::now(); let regs_arm = include_bytes!("../../docs_store/registers/serialized/arm"); let regs = bincode::deserialize(regs_arm)?; @@ -298,7 +277,7 @@ pub fn main() -> Result<()> { Vec::new() }; - let riscv_registers = if config.instruction_sets.riscv.unwrap_or(false) { + let riscv_registers = if config.is_isa_enabled(Arch::RISCV) { let start = std::time::Instant::now(); let regs_riscv = include_bytes!("../../docs_store/registers/serialized/riscv"); let regs = bincode::deserialize(regs_riscv)?; @@ -321,7 +300,7 @@ pub fn main() -> Result<()> { populate_name_to_register_map(Arch::ARM, &arm_registers, &mut names_to_info.registers); populate_name_to_register_map(Arch::RISCV, &riscv_registers, &mut names_to_info.registers); - let gas_directives = if config.assemblers.gas.unwrap_or(false) { + let gas_directives = if config.is_assembler_enabled(Assembler::Gas) { let start = std::time::Instant::now(); let gas_dirs = include_bytes!("../../docs_store/directives/serialized/gas"); let dirs = bincode::deserialize(gas_dirs)?; @@ -334,7 +313,7 @@ pub fn main() -> Result<()> { Vec::new() }; - let masm_directives = if config.assemblers.masm.unwrap_or(false) { + let masm_directives = if config.is_assembler_enabled(Assembler::Masm) { let start = std::time::Instant::now(); let masm_dirs = include_bytes!("../../docs_store/directives/serialized/masm"); let dirs = bincode::deserialize(masm_dirs)?; @@ -347,7 +326,7 @@ pub fn main() -> Result<()> { Vec::new() }; - let nasm_directives = if config.assemblers.nasm.unwrap_or(false) { + let nasm_directives = if config.is_assembler_enabled(Assembler::Nasm) { let start = std::time::Instant::now(); let nasm_dirs = include_bytes!("../../docs_store/directives/serialized/nasm"); let dirs = bincode::deserialize(nasm_dirs)?; @@ -414,7 +393,7 @@ pub fn main() -> Result<()> { #[allow(clippy::too_many_arguments, clippy::too_many_lines)] fn main_loop( connection: &Connection, - config: &Config, + config: &RootConfig, names_to_info: &NameToInfoMaps, instruction_completion_items: &[CompletionItem], directive_completion_items: &[CompletionItem], @@ -437,7 +416,7 @@ fn main_loop( handle_hover_request( connection, id, - config, + config.get_config(¶ms.text_document_position_params.text_document.uri), ¶ms, &text_store, &mut tree_store, @@ -453,7 +432,7 @@ fn main_loop( connection, id, ¶ms, - config, + config.get_config(¶ms.text_document_position.text_document.uri), &text_store, &mut tree_store, instruction_completion_items, @@ -469,7 +448,7 @@ fn main_loop( connection, id, ¶ms, - config, + config.get_config(¶ms.text_document_position_params.text_document.uri), &text_store, &mut tree_store, )?; @@ -482,7 +461,7 @@ fn main_loop( connection, id, ¶ms, - config, + config.get_config(¶ms.text_document.uri), &text_store, &mut tree_store, )?; @@ -495,7 +474,7 @@ fn main_loop( connection, id, ¶ms, - config, + config.get_config(¶ms.text_document_position_params.text_document.uri), &text_store, &mut tree_store, &names_to_info.instructions, @@ -509,7 +488,7 @@ fn main_loop( connection, id, ¶ms, - config, + config.get_config(¶ms.text_document_position.text_document.uri), &text_store, &mut tree_store, )?; @@ -519,12 +498,13 @@ fn main_loop( ); } else if let Ok((_id, params)) = cast_req::(req.clone()) { + let project_config = config.get_config(¶ms.text_document.uri); // Ok to unwrap, this should never be `None` - if config.opts.diagnostics.unwrap() { + if project_config.opts.diagnostics.unwrap() { handle_diagnostics( connection, ¶ms.text_document.uri, - config, + project_config, compile_cmds, )?; info!( @@ -568,12 +548,13 @@ fn main_loop( start.elapsed().as_millis() ); } else if let Ok(params) = cast_notif::(notif.clone()) { + let project_config = config.get_config(¶ms.text_document.uri); // Ok to unwrap, this should never be `None` - if config.opts.diagnostics.unwrap() { + if project_config.opts.diagnostics.unwrap() { handle_diagnostics( connection, ¶ms.text_document.uri, - config, + project_config, compile_cmds, )?; info!( diff --git a/src/handle.rs b/src/handle.rs index 1cf51a3c..8875ba42 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -218,6 +218,7 @@ pub fn handle_signature_help_request( let sig_resp = get_sig_help_resp( doc.get_content(None), params, + config, tree_entry, names_to_instructions, ); diff --git a/src/lsp.rs b/src/lsp.rs index 67b09b82..be592ace 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -28,8 +28,8 @@ use tree_sitter::InputEdit; use crate::types::Column; use crate::{ - Arch, ArchOrAssembler, Assembler, Completable, Config, Hoverable, Instruction, LspClient, - NameToInstructionMap, TreeEntry, TreeStore, + Arch, ArchOrAssembler, Assembler, Completable, Config, Directive, Instruction, LspClient, + NameToInstructionMap, Register, RootConfig, TreeEntry, TreeStore, }; /// Sends an empty, non-error response to the lsp client via `connection` @@ -602,46 +602,47 @@ pub fn get_completes( #[allow(clippy::too_many_arguments)] #[must_use] -pub fn get_hover_resp( +pub fn get_hover_resp( params: &HoverParams, config: &Config, word: &str, text_store: &TextDocuments, tree_store: &mut TreeStore, - instruction_map: &HashMap<(Arch, &str), T>, - register_map: &HashMap<(Arch, &str), U>, - directive_map: &HashMap<(Assembler, &str), V>, + instruction_map: &HashMap<(Arch, &str), &Instruction>, + register_map: &HashMap<(Arch, &str), &Register>, + directive_map: &HashMap<(Assembler, &str), &Directive>, include_dirs: &HashMap>, ) -> Option { - let instr_lookup = lookup_hover_resp_by_arch(word, instruction_map); + let instr_lookup = get_instr_hover_resp(word, instruction_map, config); if instr_lookup.is_some() { return instr_lookup; } + // TODO: Do the directive lookups next // directive lookup { if config.assemblers.gas.unwrap_or(false) || config.assemblers.masm.unwrap_or(false) { // all gas directives have a '.' prefix, some masm directives do - let directive_lookup = lookup_hover_resp_by_assembler(word, directive_map); + let directive_lookup = get_directive_hover_resp(word, directive_map, config); if directive_lookup.is_some() { return directive_lookup; } } else if config.assemblers.nasm.unwrap_or(false) { // most nasm directives have no prefix, 2 have a '.' prefix - let directive_lookup = lookup_hover_resp_by_assembler(word, directive_map); + let directive_lookup = get_directive_hover_resp(word, directive_map, config); if directive_lookup.is_some() { return directive_lookup; } // Some nasm directives have a % prefix let prefixed = format!("%{word}"); - let directive_lookup = lookup_hover_resp_by_assembler(&prefixed, directive_map); + let directive_lookup = get_directive_hover_resp(&prefixed, directive_map, config); if directive_lookup.is_some() { return directive_lookup; } } } - let reg_lookup = lookup_hover_resp_by_arch(word, register_map); + let reg_lookup = get_reg_hover_resp(word, register_map, config); if reg_lookup.is_some() { return reg_lookup; } @@ -673,115 +674,320 @@ pub fn get_hover_resp( None } -fn lookup_hover_resp_by_arch( - word: &str, - map: &HashMap<(Arch, &str), T>, -) -> Option { - // switch over to vec? - let (x86_resp, x86_64_resp, z80_resp, arm_resp, riscv_resp) = - search_for_hoverable_by_arch(word, map); - match ( - x86_resp.is_some(), - x86_64_resp.is_some(), - z80_resp.is_some(), - arm_resp.is_some(), - riscv_resp.is_some(), - ) { - (true, _, _, _, _) - | (_, true, _, _, _) - | (_, _, true, _, _) - | (_, _, _, true, _) - | (_, _, _, _, true) => { - let mut value = String::new(); - if let Some(x86_resp) = x86_resp { - value += &format!("{x86_resp}"); - } - if let Some(x86_64_resp) = x86_64_resp { - value += &format!( - "{}{}", - if value.is_empty() { "" } else { "\n\n" }, - x86_64_resp - ); - } - if let Some(z80_resp) = z80_resp { - value += &format!("{}{}", if value.is_empty() { "" } else { "\n\n" }, z80_resp); - } - if let Some(arm_resp) = arm_resp { - value += &format!("{}{}", if value.is_empty() { "" } else { "\n\n" }, arm_resp); - } - if let Some(riscv_resp) = riscv_resp { - value += &format!( - "{}{}", - if value.is_empty() { "" } else { "\n\n" }, - riscv_resp - ); - } - Some(Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value, - }), - range: None, - }) +#[derive(Debug, Clone, Copy)] +struct InstructionResp<'a> { + pub x86: Option<&'a Instruction>, + pub x86_64: Option<&'a Instruction>, + pub z80: Option<&'a Instruction>, + pub arm: Option<&'a Instruction>, + pub riscv: Option<&'a Instruction>, +} + +impl InstructionResp<'_> { + const fn has_resp(&self) -> bool { + self.x86.is_some() + || self.x86_64.is_some() + || self.z80.is_some() + || self.arm.is_some() + || self.riscv.is_some() + } +} + +#[derive(Debug, Clone, Copy)] +struct RegisterResp<'a> { + pub x86: Option<&'a Register>, + pub x86_64: Option<&'a Register>, + pub z80: Option<&'a Register>, + pub arm: Option<&'a Register>, + pub riscv: Option<&'a Register>, +} + +impl RegisterResp<'_> { + const fn has_resp(&self) -> bool { + self.x86.is_some() + || self.x86_64.is_some() + || self.z80.is_some() + || self.arm.is_some() + || self.riscv.is_some() + } +} + +impl std::fmt::Display for RegisterResp<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut has_entry = false; + if let Some(resp) = self.x86 { + write!(f, "{}{}", if has_entry { "\n\n" } else { "" }, resp)?; + has_entry = true; + } + if let Some(resp) = self.x86_64 { + write!(f, "{}{}", if has_entry { "\n\n" } else { "" }, resp)?; + has_entry = true; + } + if let Some(resp) = self.z80 { + write!(f, "{}{}", if has_entry { "\n\n" } else { "" }, resp)?; + has_entry = true; + } + if let Some(resp) = self.arm { + write!(f, "{}{}", if has_entry { "\n\n" } else { "" }, resp)?; + has_entry = true; + } + if let Some(resp) = self.riscv { + write!(f, "{}{}", if has_entry { "\n\n" } else { "" }, resp)?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, Copy)] +struct DirectiveResp<'a> { + pub gas: Option<&'a Directive>, + pub go: Option<&'a Directive>, + pub z80: Option<&'a Directive>, + pub masm: Option<&'a Directive>, + pub nasm: Option<&'a Directive>, +} + +impl DirectiveResp<'_> { + const fn has_resp(&self) -> bool { + self.gas.is_some() + || self.go.is_some() + || self.z80.is_some() + || self.masm.is_some() + || self.nasm.is_some() + } +} + +impl std::fmt::Display for DirectiveResp<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut has_entry = false; + if let Some(resp) = self.gas { + write!(f, "{}{}", if has_entry { "\n\n" } else { "" }, resp)?; + has_entry = true; + } + if let Some(resp) = self.go { + write!(f, "{}{}", if has_entry { "\n\n" } else { "" }, resp)?; + has_entry = true; + } + if let Some(resp) = self.z80 { + write!(f, "{}{}", if has_entry { "\n\n" } else { "" }, resp)?; + has_entry = true; + } + if let Some(resp) = self.masm { + write!(f, "{}{}", if has_entry { "\n\n" } else { "" }, resp)?; + has_entry = true; } - _ => { - // don't know of this word + if let Some(resp) = self.nasm { + write!(f, "{}{}", if has_entry { "\n\n" } else { "" }, resp)?; + } + Ok(()) + } +} + +fn search_for_instr_by_arch<'a>( + word: &'a str, + instr_map: &'a HashMap<(Arch, &str), &Instruction>, + config: &Config, +) -> InstructionResp<'a> { + let lookup = |instr_map: &'a HashMap<(Arch, &'a str), &Instruction>, + arch: Arch, + word: &'a str, + check: bool| + -> Option<&'a Instruction> { + if check { + instr_map.get(&(arch, word)).copied() + } else { None } + }; + + let instr_sets = &config.instruction_sets; + InstructionResp { + x86: lookup(instr_map, Arch::X86, word, instr_sets.x86.unwrap_or(false)), + x86_64: lookup( + instr_map, + Arch::X86_64, + word, + instr_sets.x86_64.unwrap_or(false), + ), + z80: lookup(instr_map, Arch::Z80, word, instr_sets.z80.unwrap_or(false)), + arm: lookup(instr_map, Arch::ARM, word, instr_sets.arm.unwrap_or(false)), + riscv: lookup( + instr_map, + Arch::RISCV, + word, + instr_sets.riscv.unwrap_or(false), + ), } } -fn lookup_hover_resp_by_assembler( - word: &str, - map: &HashMap<(Assembler, &str), T>, -) -> Option { - let (gas_resp, go_resp, masm_resp, nasm_resp) = search_for_hoverable_by_assembler(word, map); - - match ( - gas_resp.is_some(), - go_resp.is_some(), - masm_resp.is_some(), - nasm_resp.is_some(), - ) { - (true, _, _, _) | (_, true, _, _) | (_, _, true, _) | (_, _, _, true) => { - let mut value = String::new(); - if let Some(gas_resp) = gas_resp { - value += &format!("{gas_resp}"); - } - if let Some(go_resp) = go_resp { - value += &format!( - "{}{}", - if gas_resp.is_some() { "\n\n" } else { "" }, - go_resp - ); - } - if let Some(masm_resp) = masm_resp { - value += &format!( - "{}{}", - if !value.is_empty() { "\n\n" } else { "" }, - masm_resp - ); - } - if let Some(nasm_resp) = nasm_resp { - value += &format!( - "{}{}", - if !value.is_empty() { "\n\n" } else { "" }, - nasm_resp - ); - } - Some(Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value, - }), - range: None, - }) +fn search_for_reg_by_arch<'a>( + word: &'a str, + reg_map: &'a HashMap<(Arch, &str), &Register>, + config: &Config, +) -> RegisterResp<'a> { + let lookup = |reg_map: &'a HashMap<(Arch, &'a str), &Register>, + arch: Arch, + word: &'a str, + check: bool| + -> Option<&'a Register> { + if check { + reg_map.get(&(arch, word)).copied() + } else { + None } - _ => { - // don't know of this word + }; + let instr_sets = &config.instruction_sets; + RegisterResp { + x86: lookup(reg_map, Arch::X86, word, instr_sets.x86.unwrap_or(false)), + x86_64: lookup( + reg_map, + Arch::X86_64, + word, + instr_sets.x86_64.unwrap_or(false), + ), + z80: lookup(reg_map, Arch::Z80, word, instr_sets.z80.unwrap_or(false)), + arm: lookup(reg_map, Arch::ARM, word, instr_sets.arm.unwrap_or(false)), + riscv: lookup( + reg_map, + Arch::RISCV, + word, + instr_sets.riscv.unwrap_or(false), + ), + } +} + +fn search_for_dir_by_assembler<'a>( + word: &'a str, + reg_map: &'a HashMap<(Assembler, &str), &Directive>, + config: &Config, +) -> DirectiveResp<'a> { + let lookup = |reg_map: &'a HashMap<(Assembler, &'a str), &Directive>, + arch: Assembler, + word: &'a str, + check: bool| + -> Option<&'a Directive> { + if check { + reg_map.get(&(arch, word)).copied() + } else { None } + }; + + let assemblers = &config.assemblers; + DirectiveResp { + gas: lookup( + reg_map, + Assembler::Gas, + word, + assemblers.gas.unwrap_or(false), + ), + go: lookup(reg_map, Assembler::Go, word, assemblers.go.unwrap_or(false)), + z80: lookup( + reg_map, + Assembler::Z80, + word, + assemblers.z80.unwrap_or(false), + ), + masm: lookup( + reg_map, + Assembler::Masm, + word, + assemblers.masm.unwrap_or(false), + ), + nasm: lookup( + reg_map, + Assembler::Nasm, + word, + assemblers.nasm.unwrap_or(false), + ), + } +} + +fn get_instr_hover_resp( + word: &str, + instr_map: &HashMap<(Arch, &str), &Instruction>, + config: &Config, +) -> Option { + let instr_resp = search_for_instr_by_arch(word, instr_map, config); + if !instr_resp.has_resp() { + return None; + } + + // lookups are already gated by `config` in `search_for_instr_by_arch`, no + // need to check `config` for each arch here + let mut has_entry = false; + let mut value = String::new(); + if let Some(resp) = instr_resp.x86 { + // have to handle assembler-dependent information for x86/x86_64 + value += &format!("{}", instr_filter_targets(resp, config)); + has_entry = true; + } + if let Some(resp) = instr_resp.x86_64 { + // have to handle assembler-dependent information for x86/x86_64 + value += &format!( + "{}{}", + if has_entry { "\n\n" } else { "" }, + instr_filter_targets(resp, config) + ); + has_entry = true; + } + if let Some(resp) = instr_resp.z80 { + value += &format!("{}{}", if has_entry { "\n\n" } else { "" }, resp); + has_entry = true; + } + if let Some(resp) = instr_resp.arm { + value += &format!("{}{}", if has_entry { "\n\n" } else { "" }, resp); + has_entry = true; } + if let Some(resp) = instr_resp.riscv { + value += &format!("{}{}", if has_entry { "\n\n" } else { "" }, resp); + } + + Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value, + }), + range: None, + }) +} + +fn get_reg_hover_resp( + word: &str, + reg_map: &HashMap<(Arch, &str), &Register>, + config: &Config, +) -> Option { + let reg_resp = search_for_reg_by_arch(word, reg_map, config); + if !reg_resp.has_resp() { + return None; + } + + Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: reg_resp.to_string(), + }), + range: None, + }) +} + +fn get_directive_hover_resp( + word: &str, + dir_map: &HashMap<(Assembler, &str), &Directive>, + config: &Config, +) -> Option { + let dir_resp = search_for_dir_by_assembler(word, dir_map, config); + if !dir_resp.has_resp() { + return None; + } + + Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: dir_resp.to_string(), + }), + range: None, + }) } /// Returns the data associated with a given label `word` @@ -1065,6 +1271,7 @@ pub fn get_comp_resp( let arg_start = cap.node.range().start_point; let arg_end = cap.node.range().end_point; if cursor_matches!(cursor_line, cursor_char, arg_start, arg_end) { + // TODO: Pass in config here to filter let items = filtered_comp_list(dir_comps); return Some(CompletionList { is_incomplete: true, @@ -1083,6 +1290,7 @@ pub fn get_comp_resp( static QUERY_LABEL: Lazy = Lazy::new(|| { tree_sitter::Query::new(&tree_sitter_asm::language(), "(label (ident) @label)").unwrap() }); + // TODO: Filter shit here too let captures = doc_cursor.captures(&QUERY_LABEL, tree.root_node(), curr_doc); let mut labels = HashSet::new(); for caps in captures.map(|c| c.0) { @@ -1142,6 +1350,7 @@ pub fn get_comp_resp( let arg_start = cap.node.range().start_point; let arg_end = cap.node.range().end_point; if cursor_matches!(cursor_line, cursor_char, arg_start, arg_end) { + // TODO: More filtering here... // an instruction is always capture #0 for this query, any capture // number after must be a register or label let is_instr = cap_num == 0; @@ -1270,9 +1479,11 @@ pub fn get_document_symbols( } } +#[allow(clippy::too_many_lines)] pub fn get_sig_help_resp( curr_doc: &str, params: &SignatureHelpParams, + config: &Config, tree_entry: &mut TreeEntry, instr_info: &NameToInstructionMap, ) -> Option { @@ -1314,9 +1525,11 @@ pub fn get_sig_help_resp( let mut has_x86_64 = false; let mut has_z80 = false; let mut has_arm = false; - let (x86_info, x86_64_info, z80_info, arm_info, riscv_info) = - search_for_hoverable_by_arch(instr_name, instr_info); - if let Some(sig) = x86_info { + let instr_resp = search_for_instr_by_arch(instr_name, instr_info, config); + if !instr_resp.has_resp() { + return None; + } + if let Some(sig) = instr_resp.x86 { for form in &sig.forms { if let Some(ref gas_name) = form.gas_name { if instr_name.eq_ignore_ascii_case(gas_name) { @@ -1337,7 +1550,7 @@ pub fn get_sig_help_resp( } } } - if let Some(sig) = x86_64_info { + if let Some(sig) = instr_resp.x86_64 { for form in &sig.forms { if let Some(ref gas_name) = form.gas_name { if instr_name.eq_ignore_ascii_case(gas_name) { @@ -1358,7 +1571,7 @@ pub fn get_sig_help_resp( } } } - if let Some(sig) = z80_info { + if let Some(sig) = instr_resp.z80 { for form in &sig.forms { if let Some(ref z80_name) = form.z80_name { if instr_name.eq_ignore_ascii_case(z80_name) { @@ -1371,7 +1584,7 @@ pub fn get_sig_help_resp( } } } - if let Some(sig) = arm_info { + if let Some(sig) = instr_resp.arm { for form in &sig.asm_templates { if !has_arm { value += "**arm**\n"; @@ -1380,7 +1593,7 @@ pub fn get_sig_help_resp( value += &format!("{form}\n"); } } - if let Some(sig) = riscv_info { + if let Some(sig) = instr_resp.riscv { for form in &sig.asm_templates { if !has_arm { value += "**riscv**\n"; @@ -1548,68 +1761,39 @@ pub fn get_ref_resp( refs.into_iter().collect() } -// Note: Some issues here regarding entangled lifetimes -// -- https://github.com/rust-lang/rust/issues/80389 -// If issue is resolved, can add a separate lifetime "'b" to "word" -// parameter such that 'a: 'b -// For now, using 'a for both isn't strictly necessary, but fits our use case -#[allow(clippy::type_complexity)] -fn search_for_hoverable_by_arch<'a, T: Hoverable>( - word: &'a str, - map: &'a HashMap<(Arch, &str), T>, -) -> ( - Option<&'a T>, - Option<&'a T>, - Option<&'a T>, - Option<&'a T>, - Option<&'a T>, -) { - let x86_resp = map.get(&(Arch::X86, word)); - let x86_64_resp = map.get(&(Arch::X86_64, word)); - let z80_resp = map.get(&(Arch::Z80, word)); - let arm_resp = map.get(&(Arch::ARM, word)); - let riscv_resp = map.get(&(Arch::RISCV, word)); - - (x86_resp, x86_64_resp, z80_resp, arm_resp, riscv_resp) -} - -fn search_for_hoverable_by_assembler<'a, T: Hoverable>( - word: &'a str, - map: &'a HashMap<(Assembler, &str), T>, -) -> (Option<&'a T>, Option<&'a T>, Option<&'a T>, Option<&'a T>) { - let gas_resp = map.get(&(Assembler::Gas, word)); - let go_resp = map.get(&(Assembler::Go, word)); - let masm_resp = map.get(&(Assembler::Masm, word)); - let nasm_resp = map.get(&(Assembler::Nasm, word)); - - (gas_resp, go_resp, masm_resp, nasm_resp) -} - /// Searches for global config in ~/.config/asm-lsp, then the project's directory /// Project specific configs will override global configs #[must_use] -pub fn get_config(params: &InitializeParams) -> Config { +pub fn get_config(params: &InitializeParams) -> RootConfig { let mut config = match (get_global_config(), get_project_config(params)) { (_, Some(proj_cfg)) => proj_cfg, (Some(global_cfg), None) => global_cfg, - (None, None) => Config::default(), + (None, None) => RootConfig::default(), }; - // Want diagnostics enabled by default - if config.opts.diagnostics.is_none() { - config.opts.diagnostics = Some(true); - } + // TODO: Canonicalize `path`s in `projects` + + if let Some(ref mut default_cfg) = config.default_config { + // Want diagnostics enabled by default + if default_cfg.opts.diagnostics.is_none() { + default_cfg.opts.diagnostics = Some(true); + } - // Want default diagnostics enabled by default - if config.opts.default_diagnostics.is_none() { - config.opts.default_diagnostics = Some(true); + // Want default diagnostics enabled by default + if default_cfg.opts.default_diagnostics.is_none() { + default_cfg.opts.default_diagnostics = Some(true); + } + } else { + // provide a default empty configuration for sub-directories + // not specified in `projects` + config.default_config = Some(Config::empty()); } config } /// Checks ~/.config/asm-lsp for a config file, creating directories along the way as necessary -fn get_global_config() -> Option { +fn get_global_config() -> Option { let mut paths = if cfg!(target_os = "macos") { // `$HOME`/Library/Application Support/ and `$HOME`/.config/ vec![config_dir(), alt_mac_config_dir()] @@ -1628,7 +1812,7 @@ fn get_global_config() -> Option { #[allow(clippy::needless_borrows_for_generic_args)] if let Ok(config) = std::fs::read_to_string(&cfg_path) { let cfg_path_s = cfg_path.display(); - match toml::from_str::(&config) { + match toml::from_str::(&config) { Ok(config) => { info!("Parsing global asm-lsp config from file -> {cfg_path_s}\n"); return Some(config); @@ -1706,13 +1890,13 @@ fn get_project_root(params: &InitializeParams) -> Option { } /// checks for a config specific to the project's root directory -fn get_project_config(params: &InitializeParams) -> Option { +fn get_project_config(params: &InitializeParams) -> Option { if let Some(mut path) = get_project_root(params) { path.push(".asm-lsp.toml"); match std::fs::read_to_string(&path) { Ok(config) => { let path_s = path.display(); - match toml::from_str::(&config) { + match toml::from_str::(&config) { Ok(config) => { info!("Parsing asm-lsp project config from file -> {path_s}"); return Some(config); @@ -1752,9 +1936,6 @@ pub fn instr_filter_targets(instr: &Instruction, config: &Config) -> Instruction if !config.assemblers.go.unwrap_or(false) { filtered.go_name = None; } - if !config.assemblers.z80.unwrap_or(false) { - filtered.z80_name = None; - } filtered }) .collect(); diff --git a/src/test.rs b/src/test.rs index 85ab6a42..3c715797 100644 --- a/src/test.rs +++ b/src/test.rs @@ -26,7 +26,7 @@ mod tests { fn empty_test_config() -> Config { Config { - version: "0.1".to_string(), + version: Some("0.1".to_string()), assemblers: Assemblers { gas: Some(false), go: Some(false), @@ -52,7 +52,7 @@ mod tests { fn z80_test_config() -> Config { Config { - version: "0.1".to_string(), + version: Some("0.1".to_string()), assemblers: Assemblers { gas: Some(false), go: Some(false), @@ -78,7 +78,7 @@ mod tests { fn arm_test_config() -> Config { Config { - version: "0.1".to_string(), + version: Some("0.1".to_string()), assemblers: Assemblers { gas: Some(false), go: Some(false), @@ -104,7 +104,7 @@ mod tests { fn riscv_test_config() -> Config { Config { - version: "0.1".to_string(), + version: Some("0.1".to_string()), assemblers: Assemblers { gas: Some(false), go: Some(false), @@ -130,7 +130,7 @@ mod tests { fn x86_x86_64_test_config() -> Config { Config { - version: "0.1".to_string(), + version: Some("0.1".to_string()), assemblers: Assemblers { gas: Some(true), go: Some(true), @@ -156,7 +156,7 @@ mod tests { fn gas_test_config() -> Config { Config { - version: "0.1".to_string(), + version: Some("0.1".to_string()), assemblers: Assemblers { gas: Some(true), go: Some(false), @@ -182,7 +182,7 @@ mod tests { fn masm_test_config() -> Config { Config { - version: "0.1".to_string(), + version: Some("0.1".to_string()), assemblers: Assemblers { gas: Some(false), go: Some(false), @@ -208,7 +208,7 @@ mod tests { fn nasm_test_config() -> Config { Config { - version: "0.1".to_string(), + version: Some("0.1".to_string()), assemblers: Assemblers { gas: Some(false), go: Some(false), diff --git a/src/types.rs b/src/types.rs index cc13cb0d..b48b028c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,6 +1,7 @@ use std::{ collections::{BTreeMap, HashMap}, fmt::Display, + path::PathBuf, str::FromStr, }; @@ -712,6 +713,8 @@ pub enum Assembler { Masm, #[strum(serialize = "nasm")] Nasm, + #[strum(serialize = "z80")] + Z80, } impl ArchOrAssembler for Assembler {} @@ -823,6 +826,18 @@ impl Default for Assemblers { } } +impl Assemblers { + fn empty() -> Self { + Self { + gas: Some(false), + go: Some(false), + masm: Some(false), + nasm: Some(false), + z80: Some(false), + } + } +} + #[allow(non_snake_case)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InstructionSets { @@ -845,36 +860,238 @@ impl Default for InstructionSets { } } +impl InstructionSets { + fn empty() -> Self { + Self { + x86: Some(false), + x86_64: Some(false), + z80: Some(false), + arm: Some(false), + riscv: Some(false), + } + } +} + +// TODO: Add logic handling empty configs (both `default_config` and `projects` = `None`) + #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ConfigOptions { - pub compiler: Option, - pub diagnostics: Option, - pub default_diagnostics: Option, +pub struct RootConfig { + #[serde(flatten)] + pub default_config: Option, + pub projects: Option>, } -impl Default for ConfigOptions { +impl Default for RootConfig { fn default() -> Self { Self { - compiler: None, - diagnostics: Some(true), - default_diagnostics: Some(true), + default_config: Some(Config::default()), + projects: None, + } + } +} + +impl RootConfig { + /// Returns the project-specific associated with `uri`, or the default if no + /// matching configuration is found + /// + /// # Panics + /// + /// Will panic if `req_uri` cannot be canonicalized + pub fn get_config<'a>(&'a self, req_uri: &'a Uri) -> &'a Config { + let Ok(req_path) = PathBuf::from_str(req_uri.path().as_str()) else { + unreachable!() + }; + let request_path = match req_path.canonicalize() { + Ok(path) => path, + Err(e) => panic!("Invalid request path: {} - {e}", req_path.display()), + }; + if let Some(projects) = &self.projects { + for project in projects { + if project.path.starts_with(&request_path) { + return &project.config; + } + } + } + if let Some(root) = &self.default_config { + return root; + } + + panic!( + "Invalid configuration for {} -- Must contain a per-project configuration or default", + request_path.display() + ); + } + + /// Sets the `client` field of the default config and all project configs + pub fn set_client(&mut self, client: LspClient) { + if let Some(ref mut root) = self.default_config { + root.client = Some(client); + } + + if let Some(ref mut projects) = self.projects { + for project in projects { + project.config.client = Some(client); + } + } + } + + // TODO: Can probably clean this up with some macro magic + #[must_use] + pub fn is_isa_enabled(&self, isa: Arch) -> bool { + if let Some(ref root) = self.default_config { + match isa { + Arch::X86 => { + if root.instruction_sets.x86.unwrap_or(false) { + return true; + } + } + Arch::X86_64 => { + if root.instruction_sets.x86_64.unwrap_or(false) { + return true; + } + } + Arch::ARM => { + if root.instruction_sets.arm.unwrap_or(false) { + return true; + } + } + Arch::RISCV => { + if root.instruction_sets.riscv.unwrap_or(false) { + return true; + } + } + Arch::Z80 => { + if root.instruction_sets.z80.unwrap_or(false) { + return true; + } + } + } + } + + if let Some(ref projects) = self.projects { + for project in projects { + match isa { + Arch::X86 => { + if project.config.instruction_sets.x86.unwrap_or(false) { + return true; + } + } + Arch::X86_64 => { + if project.config.instruction_sets.x86_64.unwrap_or(false) { + return true; + } + } + Arch::ARM => { + if project.config.instruction_sets.arm.unwrap_or(false) { + return true; + } + } + Arch::RISCV => { + if project.config.instruction_sets.riscv.unwrap_or(false) { + return true; + } + } + Arch::Z80 => { + if project.config.instruction_sets.z80.unwrap_or(false) { + return true; + } + } + } + } + } + + false + } + + #[must_use] + pub fn is_assembler_enabled(&self, assembler: Assembler) -> bool { + if let Some(ref root) = self.default_config { + match assembler { + Assembler::Gas => { + if root.assemblers.gas.unwrap_or(false) { + return true; + } + } + Assembler::Go => { + if root.assemblers.go.unwrap_or(false) { + return true; + } + } + Assembler::Masm => { + if root.assemblers.masm.unwrap_or(false) { + return true; + } + } + Assembler::Nasm => { + if root.assemblers.nasm.unwrap_or(false) { + return true; + } + } + Assembler::Z80 => { + if root.assemblers.z80.unwrap_or(false) { + return true; + } + } + } + } + + if let Some(ref projects) = self.projects { + for project in projects { + match assembler { + Assembler::Gas => { + if project.config.assemblers.gas.unwrap_or(false) { + return true; + } + } + Assembler::Go => { + if project.config.assemblers.go.unwrap_or(false) { + return true; + } + } + Assembler::Masm => { + if project.config.assemblers.masm.unwrap_or(false) { + return true; + } + } + Assembler::Nasm => { + if project.config.assemblers.nasm.unwrap_or(false) { + return true; + } + } + Assembler::Z80 => { + if project.config.assemblers.z80.unwrap_or(false) { + return true; + } + } + } + } } + + false } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProjectConfig { + pub path: PathBuf, + #[serde(flatten)] + pub config: Config, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { - pub version: String, + pub version: Option, pub assemblers: Assemblers, pub instruction_sets: InstructionSets, pub opts: ConfigOptions, + #[serde(skip)] pub client: Option, } impl Default for Config { fn default() -> Self { Self { - version: String::from("0.1"), + version: Some(String::from("0.1")), assemblers: Assemblers::default(), instruction_sets: InstructionSets::default(), opts: ConfigOptions::default(), @@ -883,6 +1100,49 @@ impl Default for Config { } } +impl Config { + #[must_use] + pub fn empty() -> Self { + Self { + version: None, + assemblers: Assemblers::empty(), + instruction_sets: InstructionSets::empty(), + opts: ConfigOptions::empty(), + client: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConfigOptions { + // Specify compiler to generate diagnostics via `compile_flags.txt` + pub compiler: Option, + // Turn diagnostics feature on/off + pub diagnostics: Option, + // Turn default diagnostics (no compilation db detected) on/off + pub default_diagnostics: Option, +} + +impl Default for ConfigOptions { + fn default() -> Self { + Self { + compiler: None, + diagnostics: Some(true), + default_diagnostics: Some(true), + } + } +} + +impl ConfigOptions { + fn empty() -> Self { + Self { + compiler: None, + diagnostics: Some(false), + default_diagnostics: Some(false), + } + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum LspClient { Helix,