diff --git a/.gitignore b/.gitignore index 76bc7bf577f8..68158e41cbd7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ crates/*/target .idea/* .vscode/* *.log +*.iml diff --git a/Cargo.lock b/Cargo.lock index 2fb9936895c8..8e962d35236b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -790,6 +790,7 @@ dependencies = [ "text_unit 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "thread_worker 0.1.0", "threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tools 0.1.0", "url_serde 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 2.2.7 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/crates/ra_analysis/src/imp.rs b/crates/ra_analysis/src/imp.rs index fcb4cd95740c..bff2e00c9b31 100644 --- a/crates/ra_analysis/src/imp.rs +++ b/crates/ra_analysis/src/imp.rs @@ -140,6 +140,9 @@ impl fmt::Debug for AnalysisImpl { } impl AnalysisImpl { + pub fn file_text(&self, file_id: FileId) -> Arc { + self.db.file_text(file_id) + } pub fn file_syntax(&self, file_id: FileId) -> SourceFileNode { self.db.source_file(file_id) } diff --git a/crates/ra_analysis/src/lib.rs b/crates/ra_analysis/src/lib.rs index 67b1c148241a..9f5e9f358b95 100644 --- a/crates/ra_analysis/src/lib.rs +++ b/crates/ra_analysis/src/lib.rs @@ -274,6 +274,9 @@ pub struct Analysis { } impl Analysis { + pub fn file_text(&self, file_id: FileId) -> Arc { + self.imp.file_text(file_id) + } pub fn file_syntax(&self, file_id: FileId) -> SourceFileNode { self.imp.file_syntax(file_id).clone() } diff --git a/crates/ra_lsp_server/Cargo.toml b/crates/ra_lsp_server/Cargo.toml index c53106a625ab..f8f91e5e7e4b 100644 --- a/crates/ra_lsp_server/Cargo.toml +++ b/crates/ra_lsp_server/Cargo.toml @@ -33,6 +33,7 @@ ra_text_edit = { path = "../ra_text_edit" } ra_analysis = { path = "../ra_analysis" } gen_lsp_server = { path = "../gen_lsp_server" } ra_vfs = { path = "../ra_vfs" } +tools = { path = "../tools" } [dev-dependencies] tempdir = "0.3.7" diff --git a/crates/ra_lsp_server/src/caps.rs b/crates/ra_lsp_server/src/caps.rs index 5f7038f63119..8d508a3ba531 100644 --- a/crates/ra_lsp_server/src/caps.rs +++ b/crates/ra_lsp_server/src/caps.rs @@ -33,7 +33,7 @@ pub fn server_capabilities() -> ServerCapabilities { workspace_symbol_provider: Some(true), code_action_provider: Some(CodeActionProviderCapability::Simple(true)), code_lens_provider: None, - document_formatting_provider: None, + document_formatting_provider: Some(true), document_range_formatting_provider: None, document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions { first_trigger_character: "=".to_string(), diff --git a/crates/ra_lsp_server/src/main_loop.rs b/crates/ra_lsp_server/src/main_loop.rs index 1edb9fae4956..97c1be7788fe 100644 --- a/crates/ra_lsp_server/src/main_loop.rs +++ b/crates/ra_lsp_server/src/main_loop.rs @@ -295,6 +295,7 @@ fn on_request( .on::(handlers::handle_prepare_rename)? .on::(handlers::handle_rename)? .on::(handlers::handle_references)? + .on::(handlers::handle_formatting)? .finish(); match req { Ok(id) => { diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs index d6f3dbe28da2..a2c12a4c194c 100644 --- a/crates/ra_lsp_server/src/main_loop/handlers.rs +++ b/crates/ra_lsp_server/src/main_loop/handlers.rs @@ -6,13 +6,16 @@ use languageserver_types::{ DiagnosticSeverity, DocumentSymbol, Documentation, FoldingRange, FoldingRangeKind, FoldingRangeParams, Location, MarkupContent, MarkupKind, MarkedString, Position, PrepareRenameResponse, RenameParams, SymbolInformation, TextDocumentIdentifier, TextEdit, + Range, WorkspaceEdit, ParameterInformation, ParameterLabel, SignatureInformation, Hover, HoverContents, + DocumentFormattingParams, }; use ra_analysis::{FileId, FoldKind, Query, RunnableKind, FileRange, FilePosition, Severity}; use ra_syntax::{TextUnit, text_utils::intersect}; use ra_text_edit::text_utils::contains_offset_nonstrict; use rustc_hash::FxHashMap; use serde_json::to_value; +use std::io::Write; use crate::{ conv::{to_location, Conv, ConvWith, MapConvWith, TryConvWith}, @@ -601,6 +604,40 @@ pub fn handle_references( )) } +pub fn handle_formatting( + world: ServerWorld, + params: DocumentFormattingParams, +) -> Result>> { + let file_id = params.text_document.try_conv_with(&world)?; + let file = world.analysis().file_text(file_id); + + let file_line_index = world.analysis().file_line_index(file_id); + let end_position = TextUnit::of_str(&file).conv_with(&file_line_index); + + use std::process; + let mut rustfmt = process::Command::new("rustfmt") + .stdin(process::Stdio::piped()) + .stdout(process::Stdio::piped()) + .spawn()?; + + rustfmt.stdin.as_mut().unwrap().write_all(file.as_bytes())?; + + let output = rustfmt.wait_with_output()?; + let captured_stdout = String::from_utf8(output.stdout)?; + if !output.status.success() { + failure::bail!( + "rustfmt exited with error code {}: {}.", + output.status, + captured_stdout, + ); + } + + Ok(Some(vec![TextEdit { + range: Range::new(Position::new(0, 0), end_position), + new_text: captured_stdout, + }])) +} + pub fn handle_code_action( world: ServerWorld, params: req::CodeActionParams, diff --git a/crates/ra_lsp_server/tests/heavy_tests/main.rs b/crates/ra_lsp_server/tests/heavy_tests/main.rs index 1f5cc5e8bb48..b0e1e65b6298 100644 --- a/crates/ra_lsp_server/tests/heavy_tests/main.rs +++ b/crates/ra_lsp_server/tests/heavy_tests/main.rs @@ -1,8 +1,8 @@ mod support; use serde_json::json; -use ra_lsp_server::req::{Runnables, RunnablesParams, CodeActionRequest, CodeActionParams}; -use languageserver_types::{Position, Range, CodeActionContext}; +use ra_lsp_server::req::{Runnables, RunnablesParams, CodeActionRequest, CodeActionParams, Formatting}; +use languageserver_types::{Position, Range, CodeActionContext, DocumentFormattingParams, FormattingOptions}; use crate::support::project; @@ -118,6 +118,60 @@ fn test_eggs() {} ); } +use std::collections::HashMap; +#[test] +fn test_format_document() { + tools::install_rustfmt().unwrap(); + + let server = project( + r#" +[package] +name = "foo" +version = "0.0.0" + +//- src/lib.rs +mod bar; + +fn main() { +} + +pub use std::collections::HashMap; +"#, + ); + server.wait_for_feedback("workspace loaded"); + + server.request::( + DocumentFormattingParams { + text_document: server.doc_id("src/lib.rs"), + options: FormattingOptions { + tab_size: 4, + insert_spaces: false, + properties: HashMap::new(), + }, + }, + json!([ + { + "newText": r#"mod bar; + +fn main() {} + +pub use std::collections::HashMap; +"#, + "range": { + "end": { + "character": 0, + "line": 6 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + ]), + ); +} + #[test] fn test_missing_module_code_action() { let server = project( diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs index 6f96b8120519..e5b32c25ce44 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -117,7 +117,7 @@ pub fn run_rustfmt(mode: Mode) -> Result<()> { Ok(()) } -fn install_rustfmt() -> Result<()> { +pub fn install_rustfmt() -> Result<()> { run(&format!("rustup install {}", TOOLCHAIN), ".")?; run( &format!("rustup component add rustfmt --toolchain {}", TOOLCHAIN), diff --git a/editors/README.md b/editors/README.md index ad2ce16954f7..a63ced72559c 100644 --- a/editors/README.md +++ b/editors/README.md @@ -45,6 +45,8 @@ It's better to remove existing Rust plugins to avoid interference. `#[test]`, this action runs this specific test. If the cursor is outside of the test function, this re-runs the last test. Do bind this to a shortcut! + - **Format document**. Formats the current file with rustfmt. + Rustfmt must be installed separately with `rustup component add rustfmt`. * Typing assists - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression.