Skip to content

Commit

Permalink
Feat autocomplete (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
WillLillis authored Nov 26, 2023
1 parent 39d26dc commit fb71a59
Show file tree
Hide file tree
Showing 7 changed files with 514 additions and 53 deletions.
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,8 @@ serde = "1.0.158"
toml = "0.8.1"
home = "0.5.5"
mockito = "1.2.0"
tree-sitter = "0.20.10"
tree-sitter-asm = "0.1.0"
once_cell = "1.18.0"

# [dev-dependencies]
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

## Goal

Provide hovering and (TODO) autocompletion for assembly files written in the
Provide hovering and autocompletion for assembly files written in the
GAS/NASM or GO assembly flavors. It supports assembly files for the x86 or
x86_64 instruction sets.

Expand Down Expand Up @@ -64,7 +64,7 @@ x86_64 = true

### Autocomplete

TODO
![](https://github.com/bergercookie/asm-lsp/blob/master/demo/autocomplete.gif)

## Acknowledgements

Expand Down
Binary file added demo/autocomplete.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
193 changes: 145 additions & 48 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use asm_lsp::*;

use log::{error, info};
use lsp_types::request::HoverRequest;
use lsp_types::notification::{DidChangeTextDocument, DidOpenTextDocument};
use lsp_types::request::{Completion, HoverRequest};
use lsp_types::*;

use crate::lsp::{get_target_config, instr_filter_targets};

use lsp_server::{Connection, Message, Request, RequestId, Response};
use lsp_server::{Connection, Message, Notification, Request, RequestId, Response};
use serde_json::json;

// main -------------------------------------------------------------------------------------------
Expand All @@ -24,8 +25,21 @@ pub fn main() -> anyhow::Result<()> {

// Run the server and wait for the two threads to end (typically by trigger LSP Exit event).
let hover_provider = Some(HoverProviderCapability::Simple(true));

let completion_provider = Some(CompletionOptions {
completion_item: Some(CompletionOptionsCompletionItem {
label_details_support: Some(true),
}),
trigger_characters: Some(vec![String::from("%")]),
..Default::default()
});

let text_document_sync = Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL));

let capabilities = ServerCapabilities {
hover_provider,
completion_provider,
text_document_sync,
..ServerCapabilities::default()
};
let server_capabilities = serde_json::to_value(capabilities).unwrap();
Expand All @@ -34,6 +48,26 @@ pub fn main() -> anyhow::Result<()> {
let params: InitializeParams = serde_json::from_value(initialization_params.clone()).unwrap();
let target_config = get_target_config(&params);

let x86_registers = if target_config.instruction_sets.x86 {
info!("Populating register set -> x86...");
let xml_conts_regs_x86 = include_str!("../../registers/x86.xml");
populate_registers(xml_conts_regs_x86)?
} else {
Vec::new()
};

let x86_64_registers = if target_config.instruction_sets.x86_64 {
info!("Populating register set -> x86_64...");
let xml_conts_regs_x86_64 = include_str!("../../registers/x86_64.xml");
populate_registers(xml_conts_regs_x86_64)?
} else {
Vec::new()
};

let mut names_to_registers = NameToRegisterMap::new();
populate_name_to_register_map(Arch::X86, &x86_registers, &mut names_to_registers);
populate_name_to_register_map(Arch::X86_64, &x86_64_registers, &mut names_to_registers);

// 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
Expand Down Expand Up @@ -118,11 +152,16 @@ pub fn main() -> anyhow::Result<()> {
populate_name_to_register_map(Arch::X86, &x86_registers, &mut names_to_registers);
populate_name_to_register_map(Arch::X86_64, &x86_64_registers, &mut names_to_registers);

let instr_comps = get_completes(&names_to_instructions, Some(CompletionItemKind::OPERATOR));
let reg_comps = get_completes(&names_to_registers, Some(CompletionItemKind::VARIABLE));

main_loop(
&connection,
initialization_params,
&names_to_instructions,
&names_to_registers,
&instr_comps,
&reg_comps,
)?;
io_threads.join()?;

Expand All @@ -136,73 +175,119 @@ fn main_loop(
params: serde_json::Value,
names_to_instructions: &NameToInstructionMap,
names_to_registers: &NameToRegisterMap,
instruction_completes: &[CompletionItem],
register_completes: &[CompletionItem],
) -> anyhow::Result<()> {
let _params: InitializeParams = serde_json::from_value(params).unwrap();

let mut doc_open_params;
let mut doc_change_params;
let curr_doc = String::new();
let mut curr_doc = &curr_doc;
let mut parser = tree_sitter::Parser::new();
parser.set_logger(Some(Box::new(tree_sitter_logger)));
parser.set_language(tree_sitter_asm::language())?;

info!("Starting LSP loop...");
for msg in &connection.receiver {
match msg {
Message::Request(req) => {
if connection.handle_shutdown(&req)? {
return Ok(());
}
match cast::<HoverRequest>(req) {
} else if let Ok((id, params)) = cast_req::<HoverRequest>(req.clone()) {
// HoverRequest ---------------------------------------------------------------
Ok((id, params)) => {
let res = Response {
id: id.clone(),
result: Some(json!("")),
error: None,
};

// get the word under the cursor
let word = get_word_from_file_params(&params.text_document_position_params);

// get documentation ------------------------------------------------------
// format response
match word {
Ok(word) => {
let hover_res = get_hover_resp(&word, names_to_instructions);
// If no instructions matched, check the registers
let hover_res = if hover_res.is_none() {
get_hover_resp(&word, names_to_registers)
} else {
hover_res
};
match hover_res {
Some(_) => {
let result = serde_json::to_value(&hover_res).unwrap();
let result = Response {
id: id.clone(),
result: Some(result),
error: None,
};
connection.sender.send(Message::Response(result))?;
}
None => {
// don't know of this word
connection.sender.send(Message::Response(res.clone()))?;
}
let res = Response {
id: id.clone(),
result: Some(json!("")),
error: None,
};

// get the word under the cursor
let word = get_word_from_file_params(&params.text_document_position_params);

// get documentation ------------------------------------------------------
// format response
match word {
Ok(word) => {
let hover_res = get_hover_resp(&word, names_to_instructions);
// If no instructions matched, check the registers
let hover_res = if hover_res.is_none() {
get_hover_resp(&word, names_to_registers)
} else {
hover_res
};
match hover_res {
Some(_) => {
let result = serde_json::to_value(&hover_res).unwrap();
let result = Response {
id: id.clone(),
result: Some(result),
error: None,
};
connection.sender.send(Message::Response(result))?;
}
None => {
// don't know of this word
connection.sender.send(Message::Response(res.clone()))?;
}
}
Err(_) => {
// given word is not valid
connection.sender.send(Message::Response(res))?;
}
}
Err(_) => {
// given word is not valid
connection.sender.send(Message::Response(res))?;
}
}
Err(req) => {
error!("Invalid request format -> {:#?}", req);
} else if let Ok((id, params)) = cast_req::<Completion>(req.clone()) {
// CompletionRequest ---------------------------------------------------------------
// get suggestions ------------------------------------------------------
let comp_res = get_comp_resp(
curr_doc,
&mut parser,
&params,
instruction_completes,
register_completes,
);
match comp_res {
Some(_) => {
let result = serde_json::to_value(&comp_res).unwrap();
let result = Response {
id: id.clone(),
result: Some(result),
error: None,
};
connection.sender.send(Message::Response(result))?;
}
None => {
// don't know what to suggest
let res = Response {
id: id.clone(),
result: Some(json!("")),
error: None,
};
connection.sender.send(Message::Response(res.clone()))?;
}
}
};
} else {
error!("Invalid request fromat -> {:#?}", req);
}
}
Message::Notification(notif) => {
if let Ok(params) = cast_notif::<DidOpenTextDocument>(notif.clone()) {
// move the notification's params into this variable so we can avoid cloning the document contents
doc_open_params = params;
curr_doc = &doc_open_params.text_document.text;
} else if let Ok(params) = cast_notif::<DidChangeTextDocument>(notif.clone()) {
doc_change_params = params;
curr_doc = &doc_change_params.content_changes.last().unwrap().text;
}
}
Message::Response(_resp) => {}
Message::Notification(_notification) => {}
}
}
Ok(())
}

fn cast<R>(req: Request) -> anyhow::Result<(RequestId, R::Params)>
fn cast_req<R>(req: Request) -> anyhow::Result<(RequestId, R::Params)>
where
R: lsp_types::request::Request,
R::Params: serde::de::DeserializeOwned,
Expand All @@ -213,3 +298,15 @@ where
Err(e) => Err(anyhow::anyhow!("Error: {e}")),
}
}

fn cast_notif<R>(notif: Notification) -> anyhow::Result<R::Params>
where
R: lsp_types::notification::Notification,
R::Params: serde::de::DeserializeOwned,
{
match notif.extract(R::METHOD) {
Ok(value) => Ok(value),
// Fixme please
Err(e) => Err(anyhow::anyhow!("Error: {e}")),
}
}
Loading

0 comments on commit fb71a59

Please sign in to comment.