diff --git a/src/bin/main.rs b/src/bin/main.rs index cba84957..dc3231a1 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -5,7 +5,7 @@ use asm_lsp::*; use log::{error, info}; use lsp_types::notification::{DidChangeTextDocument, DidOpenTextDocument}; use lsp_types::request::{ - Completion, DocumentSymbolRequest, GotoDefinition, HoverRequest, References, + Completion, DocumentSymbolRequest, Formatting, GotoDefinition, HoverRequest, References, SignatureHelpRequest, }; use lsp_types::*; @@ -59,6 +59,8 @@ pub fn main() -> anyhow::Result<()> { let references_provider = Some(OneOf::Left(true)); + let document_formatting_provider = Some(OneOf::Left(true)); + let capabilities = ServerCapabilities { position_encoding, hover_provider, @@ -68,6 +70,7 @@ pub fn main() -> anyhow::Result<()> { text_document_sync, document_symbol_provider: Some(OneOf::Left(true)), references_provider, + document_formatting_provider, ..ServerCapabilities::default() }; let server_capabilities = serde_json::to_value(capabilities).unwrap(); @@ -219,13 +222,13 @@ fn main_loop( get_word_from_pos_params( doc, ¶ms.text_document_position_params, - ".", + "", ), // treat the word under the cursor as a filename and grab it as well get_word_from_pos_params( doc, ¶ms.text_document_position_params, - "", + ".", ), ) } else { @@ -401,6 +404,26 @@ fn main_loop( connection.sender.send(Message::Response(res.clone()))?; } } + } else if let Ok((id, params)) = cast_req::(req.clone()) { + // DocumentFormatRequest + if let Some(ref doc) = curr_doc { + let fmt_resp = get_doc_fmt_resp(doc, ¶ms); + + let result = serde_json::to_value(&fmt_resp).unwrap(); + let result = Response { + id: id.clone(), + result: Some(result), + error: None, + }; + connection.sender.send(Message::Response(result))?; + } else { + let res = Response { + id: id.clone(), + result: Some(json!("")), + error: None, + }; + connection.sender.send(Message::Response(res))?; + } } else { error!("Invalid request format -> {:#?}", req); } diff --git a/src/lsp.rs b/src/lsp.rs index b175e4bd..1db23ec0 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -5,6 +5,7 @@ use log::{error, info, log, log_enabled}; use lsp_textdocument::FullTextDocument; use lsp_types::*; use once_cell::sync::Lazy; +use regex::Regex; use std::collections::{HashMap, HashSet}; use std::fs::{create_dir_all, File}; use std::io::BufRead; @@ -814,6 +815,219 @@ fn search_for_hoverable<'a, T: Hoverable>( (x86_res, x86_64_res) } +/// Returns a series of non-overlapping edits from bottom to top +/// Labels are non-indented, non-labels are indented once, and +/// trailing newlines/ whitespace is trimmed according to params +pub fn get_doc_fmt_resp( + doc: &FullTextDocument, + params: &DocumentFormattingParams, +) -> Vec { + let mut edits: Vec = Vec::new(); + + if doc.line_count() == 0 { + return edits; + } + + let single_indent = if params.options.insert_spaces { + " ".repeat(params.options.tab_size as usize) + } else { + String::from("\t") + }; + const EMPTY_STR: &str = ""; + const NEW_LINE: &str = "\n"; + + let insert_final_newline = params.options.insert_final_newline.unwrap_or(false); + let trim_final_newlines = params.options.trim_final_newlines.unwrap_or(true); + let trim_trailing_whitespace = params.options.trim_trailing_whitespace.unwrap_or(true); + + // Find the lowest line (highest line number) that isn't a trailing newline + let mut lowest_nonempty_line = doc.line_count() - 1; + for line_num in (0..doc.line_count()).rev() { + lowest_nonempty_line = line_num; + if !doc + .get_content(Some(Range { + start: Position { + line: line_num, + character: 0, + }, + end: Position { + line: line_num, + character: u32::MAX, + }, + })) + .trim() + .is_empty() + { + break; + } + } + + // handle trimming of/ adding trailing newlines per params + match (trim_final_newlines, insert_final_newline) { + // replace all trailing newlines with a single one + (true, true) => edits.push(TextEdit { + range: Range { + start: Position { + line: lowest_nonempty_line, + character: u32::MAX, + }, + end: Position { + line: doc.line_count() - 1, + character: u32::MAX, + }, + }, + new_text: NEW_LINE.to_string(), + }), + // take out all trailing newlines, no additional lines added + (true, false) => edits.push(TextEdit { + range: Range { + start: Position { + line: lowest_nonempty_line, + character: u32::MAX, + }, + end: Position { + line: doc.line_count() - 1, + character: u32::MAX, + }, + }, + new_text: EMPTY_STR.to_string(), + }), + // Add another trailing newline on top of 0 or more existing trailing newlines + (false, true) => { + if lowest_nonempty_line < doc.line_count() - 1 { + edits.push(TextEdit { + range: Range { + start: Position { + line: doc.line_count() - 1, + character: u32::MAX, + }, + end: Position { + line: doc.line_count() - 1, + character: u32::MAX, + }, + }, + new_text: NEW_LINE.to_string(), + }) + } + } + (false, false) => {} // do nothing + } + + // Match leading white space if there is any, the label text, ':' literally, + // white space before a comment, an optional comment, and trailing whitespace + static LABEL_REGEX: Lazy = Lazy::new(|| { + Regex::new(r"(?P^\s*)(?P