Specify asm-lsp target configuration via config file.

Enable specifying which assemblers and which architectures to be enabled/disabled when interacting with the LSP server. Use this feature to e.g., disable showing the `go` opcode documentation or to show only the documentation of the x86_64 architecture.

Closes #4
This commit is contained in:
Will Lillis 2023-10-01 05:03:37 -04:00 committed by Nikos Koukis
parent 9914861af8
commit 2943e33e3b
7 changed files with 291 additions and 37 deletions

2
.gitignore vendored
View file

@ -167,3 +167,5 @@ tags
# End of https://www.toptal.com/developers/gitignore/api/rust,vim,python
rusty-tags.vi
/.asm-lsp.toml

79
Cargo.lock generated
View file

@ -63,6 +63,7 @@ dependencies = [
"serde_json",
"strum",
"strum_macros",
"toml",
]
[[package]]
@ -187,6 +188,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.3"
@ -332,7 +339,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap",
"indexmap 1.9.3",
"slab",
"tokio",
"tokio-util",
@ -345,6 +352,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]]
name = "heck"
version = "0.4.1"
@ -468,7 +481,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown",
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
]
[[package]]
@ -920,6 +943,15 @@ dependencies = [
"syn 2.0.29",
]
[[package]]
name = "serde_spanned"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@ -1090,6 +1122,40 @@ dependencies = [
"tracing",
]
[[package]]
name = "toml"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc1433177506450fe920e46a4f9812d0c211f5dd556da10e731a0a3dfa151f0"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca676d9ba1a322c1b64eb8045a5ec5c0cfb0c9d08e15e9ff622589ad5221c8fe"
dependencies = [
"indexmap 2.0.0",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "tower-service"
version = "0.3.2"
@ -1349,6 +1415,15 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "winnow"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.50.0"

View file

@ -43,5 +43,6 @@ strum = "0.24.1"
strum_macros = "0.24.3"
serde_json = "1.0.94"
serde = "1.0.158"
toml = "0.8.1"
# [dev-dependencies]

View file

@ -40,6 +40,23 @@ Add a section like the following in your `settings.json` file:
}
```
### [OPTIONAL] Configure via `.asm-lsp.toml`
Add a `.asm-lsp.toml` file like the following to your project's root directory
to selectively target specific assemblers and/or instruction sets:
```toml
version = "0.1"
[assemblers]
gas = true
go = false
[instruction_sets]
x86 = false
x86_64 = true
```
## Demo
### Hovering / Documentation support

View file

@ -4,6 +4,8 @@ use log::{error, info};
use lsp_types::request::HoverRequest;
use lsp_types::*;
use crate::lsp::{filter_targets, get_target_config};
use lsp_server::{Connection, Message, Request, RequestId, Response};
use serde_json::json;
@ -14,39 +16,8 @@ pub fn main() -> anyhow::Result<()> {
// logging only write out to stderr.
flexi_logger::Logger::try_with_str("info")?.start()?;
// 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
info!("Populating instruction set -> x86...");
let xml_conts_x86 = include_str!("../../opcodes/x86.xml");
let x86_instructions = populate_instructions(xml_conts_x86)?
.into_iter()
.map(|mut instruction| {
instruction.arch = Some(Arch::X86);
instruction
})
.collect();
info!("Populating instruction set -> x86_64...");
let xml_conts_x86_64 = include_str!("../../opcodes/x86_64.xml");
let x86_64_instructions = populate_instructions(xml_conts_x86_64)?
.into_iter()
.map(|mut instruction| {
instruction.arch = Some(Arch::X86_64);
instruction
})
.collect();
let mut names_to_instructions = NameToInstructionMap::new();
populate_name_to_instruction_map(Arch::X86, &x86_instructions, &mut names_to_instructions);
populate_name_to_instruction_map(
Arch::X86_64,
&x86_64_instructions,
&mut names_to_instructions,
);
// LSP server initialisation ------------------------------------------------------------------
info!("Starting lsp server...");
info!("Starting LSP server...");
// Create the transport
let (connection, io_threads) = Connection::stdio();
@ -57,13 +28,66 @@ pub fn main() -> anyhow::Result<()> {
hover_provider,
..ServerCapabilities::default()
};
let server_capabilities = serde_json::to_value(&capabilities).unwrap();
let server_capabilities = serde_json::to_value(capabilities).unwrap();
let initialization_params = connection.initialize(server_capabilities)?;
let params: InitializeParams = serde_json::from_value(initialization_params.clone()).unwrap();
let target_config = get_target_config(&params);
// 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 target_config.instruction_sets.x86 {
info!("Populating instruction set -> x86...");
let xml_conts_x86 = include_str!("../../opcodes/x86.xml");
populate_instructions(xml_conts_x86)?
.into_iter()
.map(|mut instruction| {
instruction.arch = Some(Arch::X86);
instruction
})
.map(|instruction| {
// filter out assemblers by user config
filter_targets(&instruction, &target_config)
})
.filter(|instruction| !instruction.forms.is_empty())
.collect()
} else {
Vec::new()
};
let x86_64_instructions = if target_config.instruction_sets.x86_64 {
info!("Populating instruction set -> x86_64...");
let xml_conts_x86_64 = include_str!("../../opcodes/x86_64.xml");
populate_instructions(xml_conts_x86_64)?
.into_iter()
.map(|mut instruction| {
instruction.arch = Some(Arch::X86_64);
instruction
})
.map(|instruction| {
// filter out assemblers by user config
filter_targets(&instruction, &target_config)
})
.filter(|instruction| !instruction.forms.is_empty())
.collect()
} else {
Vec::new()
};
let mut names_to_instructions = NameToInstructionMap::new();
populate_name_to_instruction_map(Arch::X86, &x86_instructions, &mut names_to_instructions);
populate_name_to_instruction_map(
Arch::X86_64,
&x86_64_instructions,
&mut names_to_instructions,
);
main_loop(&connection, initialization_params, &names_to_instructions)?;
io_threads.join()?;
// Shut down gracefully.
info!("Shutting down lsp server");
info!("Shutting down LSP server");
Ok(())
}

View file

@ -1,7 +1,11 @@
use crate::types::Column;
use lsp_types::TextDocumentPositionParams;
use crate::{Instruction, TargetConfig};
use log::{error, info};
use lsp_types::{InitializeParams, TextDocumentPositionParams, Url};
use std::fs::File;
use std::io::BufRead;
use std::path::PathBuf;
/// Find the start and end indices of a word inside the given line
/// Borrowed from RLS
pub fn find_word_at_pos(line: &str, col: Column) -> (Column, Column) {
@ -48,3 +52,86 @@ pub fn get_word_from_file_params(
Err(_) => Err(anyhow::anyhow!("filepath get error")),
}
}
pub fn get_target_config(params: &InitializeParams) -> TargetConfig {
// 1. if we have workspace folders, then iterate through them and assign the first valid one to
// the root path
// 2. If we don't have worksace folders or none of them is a valid path, check the root_uri
// variable
// 3. If we do have a root_path, check whether we can find a .asm-lsp file at its root.
// 4. If everything fails return TargetConfig::default()
let mut root_path: Option<PathBuf> = None;
// first check workspace folders
if let Some(folders) = &params.workspace_folders {
// if there's multiple, just visit in order until we find a valid folder
let mut path = None;
for folder in folders.iter() {
if let Ok(parsed) = Url::parse(folder.uri.as_str()) {
if let Ok(parsed_path) = parsed.to_file_path() {
path = Some(parsed_path);
break;
}
}
}
root_path = path;
};
// if workspace folders weren't set or came up empty, we check the root_uri
if root_path.is_none() {
if let Some(root_uri) = &params.root_uri {
if let Ok(path) = root_uri.to_file_path() {
root_path = Some(path)
}
}
};
// if we have a properly configured root path, check for the config file
if let Some(mut path) = root_path {
path.push(".asm-lsp.toml");
if let Ok(config) = std::fs::read_to_string(path.clone()) {
let path_s = path.display();
match toml::from_str::<TargetConfig>(&config) {
Ok(config) => {
info!("Parsing asm-lsp config from file -> {path_s}\n");
return config;
}
Err(e) => {
error!("Failed to parse config file {path_s} - Error: {e}\n");
} // if there's an error we fall through to the default
}
}
}
// default is to turn everything on
TargetConfig::default()
}
pub fn filter_targets(instr: &Instruction, config: &TargetConfig) -> Instruction {
let mut instr = instr.clone();
let forms = instr
.forms
.iter()
.filter(|form| {
(form.gas_name.is_some() && config.assemblers.gas)
|| (form.go_name.is_some() && config.assemblers.go)
})
.map(|form| {
let mut filtered = form.clone();
// handle cases where gas and go both have names on the same form
if !config.assemblers.gas {
filtered.gas_name = None;
}
if !config.assemblers.go {
filtered.go_name = None;
}
filtered
})
.collect();
instr.forms = forms;
instr
}

View file

@ -1,3 +1,4 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use strum_macros::{AsRefStr, EnumString};
@ -178,6 +179,53 @@ pub enum Arch {
X86_64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Assemblers {
pub gas: bool,
pub go: bool,
}
impl Default for Assemblers {
fn default() -> Self {
Assemblers {
gas: true,
go: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstructionSets {
pub x86: bool,
pub x86_64: bool,
}
impl Default for InstructionSets {
fn default() -> Self {
InstructionSets {
x86: true,
x86_64: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TargetConfig {
pub version: String,
pub assemblers: Assemblers,
pub instruction_sets: InstructionSets,
}
impl Default for TargetConfig {
fn default() -> Self {
TargetConfig {
version: String::from("0.1"),
assemblers: Assemblers::default(),
instruction_sets: InstructionSets::default(),
}
}
}
// Instruction Set Architecture -------------------------------------------------------------------
#[derive(Debug, Clone, EnumString, AsRefStr)]
pub enum ISA {