From b5021411a84822cb3f1e3aeffad9550dd15bdeb6 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sun, 16 Sep 2018 12:54:24 +0300 Subject: rename all things --- crates/ra_lsp_server/Cargo.toml | 31 ++ crates/ra_lsp_server/src/caps.rs | 49 +++ crates/ra_lsp_server/src/conv.rs | 296 ++++++++++++++ crates/ra_lsp_server/src/lib.rs | 37 ++ crates/ra_lsp_server/src/main.rs | 52 +++ crates/ra_lsp_server/src/main_loop/handlers.rs | 436 +++++++++++++++++++++ crates/ra_lsp_server/src/main_loop/mod.rs | 419 ++++++++++++++++++++ .../ra_lsp_server/src/main_loop/subscriptions.rs | 21 + crates/ra_lsp_server/src/path_map.rs | 110 ++++++ crates/ra_lsp_server/src/project_model.rs | 175 +++++++++ crates/ra_lsp_server/src/req.rs | 176 +++++++++ crates/ra_lsp_server/src/server_world.rs | 167 ++++++++ crates/ra_lsp_server/src/thread_watcher.rs | 70 ++++ crates/ra_lsp_server/src/vfs.rs | 71 ++++ crates/ra_lsp_server/tests/heavy_tests/main.rs | 99 +++++ crates/ra_lsp_server/tests/heavy_tests/support.rs | 217 ++++++++++ 16 files changed, 2426 insertions(+) create mode 100644 crates/ra_lsp_server/Cargo.toml create mode 100644 crates/ra_lsp_server/src/caps.rs create mode 100644 crates/ra_lsp_server/src/conv.rs create mode 100644 crates/ra_lsp_server/src/lib.rs create mode 100644 crates/ra_lsp_server/src/main.rs create mode 100644 crates/ra_lsp_server/src/main_loop/handlers.rs create mode 100644 crates/ra_lsp_server/src/main_loop/mod.rs create mode 100644 crates/ra_lsp_server/src/main_loop/subscriptions.rs create mode 100644 crates/ra_lsp_server/src/path_map.rs create mode 100644 crates/ra_lsp_server/src/project_model.rs create mode 100644 crates/ra_lsp_server/src/req.rs create mode 100644 crates/ra_lsp_server/src/server_world.rs create mode 100644 crates/ra_lsp_server/src/thread_watcher.rs create mode 100644 crates/ra_lsp_server/src/vfs.rs create mode 100644 crates/ra_lsp_server/tests/heavy_tests/main.rs create mode 100644 crates/ra_lsp_server/tests/heavy_tests/support.rs (limited to 'crates/ra_lsp_server') diff --git a/crates/ra_lsp_server/Cargo.toml b/crates/ra_lsp_server/Cargo.toml new file mode 100644 index 000000000..b851f70e1 --- /dev/null +++ b/crates/ra_lsp_server/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "ra_lsp_server" +version = "0.1.0" +authors = ["Aleksey Kladov "] + +[dependencies] +rayon = "1.0.2" +relative-path = "0.3.7" +failure = "0.1.2" +serde_json = "1.0.24" +serde = "1.0.71" +serde_derive = "1.0.71" +drop_bomb = "0.1.0" +crossbeam-channel = "0.2.4" +flexi_logger = "0.9.1" +log = "0.4.3" +url_serde = "0.2.0" +languageserver-types = "0.50.0" +walkdir = "2.2.0" +im = "12.0.0" +cargo_metadata = "0.6.0" +text_unit = { version = "0.1.2", features = ["serde"] } +smol_str = { version = "0.1.5", features = ["serde"] } + +ra_syntax = { path = "../ra_syntax" } +ra_editor = { path = "../ra_editor" } +ra_analysis = { path = "../ra_analysis" } +gen_lsp_server = { path = "../gen_lsp_server" } + +[dev-dependencies] +tempdir = "0.3.7" diff --git a/crates/ra_lsp_server/src/caps.rs b/crates/ra_lsp_server/src/caps.rs new file mode 100644 index 000000000..7456aea8a --- /dev/null +++ b/crates/ra_lsp_server/src/caps.rs @@ -0,0 +1,49 @@ +use languageserver_types::{ + ServerCapabilities, + TextDocumentSyncCapability, + TextDocumentSyncOptions, + TextDocumentSyncKind, + ExecuteCommandOptions, + CompletionOptions, + DocumentOnTypeFormattingOptions, +}; + +pub fn server_capabilities() -> ServerCapabilities { + ServerCapabilities { + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::Full), + will_save: None, + will_save_wait_until: None, + save: None, + } + )), + hover_provider: None, + completion_provider: Some(CompletionOptions { + resolve_provider: None, + trigger_characters: None, + }), + signature_help_provider: None, + definition_provider: Some(true), + type_definition_provider: None, + implementation_provider: None, + references_provider: None, + document_highlight_provider: None, + document_symbol_provider: Some(true), + workspace_symbol_provider: Some(true), + code_action_provider: Some(true), + code_lens_provider: None, + document_formatting_provider: None, + document_range_formatting_provider: None, + document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions { + first_trigger_character: "=".to_string(), + more_trigger_character: None, + }), + rename_provider: None, + color_provider: None, + execute_command_provider: Some(ExecuteCommandOptions { + commands: vec!["apply_code_action".to_string()], + }), + } +} diff --git a/crates/ra_lsp_server/src/conv.rs b/crates/ra_lsp_server/src/conv.rs new file mode 100644 index 000000000..759e5e914 --- /dev/null +++ b/crates/ra_lsp_server/src/conv.rs @@ -0,0 +1,296 @@ +use languageserver_types::{ + Range, SymbolKind, Position, TextEdit, Location, Url, + TextDocumentIdentifier, VersionedTextDocumentIdentifier, TextDocumentItem, + TextDocumentPositionParams, TextDocumentEdit, +}; +use ra_editor::{LineIndex, LineCol, Edit, AtomEdit}; +use ra_syntax::{SyntaxKind, TextUnit, TextRange}; +use ra_analysis::{FileId, SourceChange, SourceFileEdit, FileSystemEdit}; + +use { + Result, + server_world::ServerWorld, + req, +}; + +pub trait Conv { + type Output; + fn conv(self) -> Self::Output; +} + +pub trait ConvWith { + type Ctx; + type Output; + fn conv_with(self, ctx: &Self::Ctx) -> Self::Output; +} + +pub trait TryConvWith { + type Ctx; + type Output; + fn try_conv_with(self, ctx: &Self::Ctx) -> Result; +} + +impl Conv for SyntaxKind { + type Output = SymbolKind; + + fn conv(self) -> ::Output { + match self { + SyntaxKind::FN_DEF => SymbolKind::Function, + SyntaxKind::STRUCT_DEF => SymbolKind::Struct, + SyntaxKind::ENUM_DEF => SymbolKind::Enum, + SyntaxKind::TRAIT_DEF => SymbolKind::Interface, + SyntaxKind::MODULE => SymbolKind::Module, + SyntaxKind::TYPE_DEF => SymbolKind::TypeParameter, + SyntaxKind::STATIC_DEF => SymbolKind::Constant, + SyntaxKind::CONST_DEF => SymbolKind::Constant, + SyntaxKind::IMPL_ITEM => SymbolKind::Object, + _ => SymbolKind::Variable, + } + } +} + +impl ConvWith for Position { + type Ctx = LineIndex; + type Output = TextUnit; + + fn conv_with(self, line_index: &LineIndex) -> TextUnit { + // TODO: UTF-16 + let line_col = LineCol { + line: self.line as u32, + col: (self.character as u32).into(), + }; + line_index.offset(line_col) + } +} + +impl ConvWith for TextUnit { + type Ctx = LineIndex; + type Output = Position; + + fn conv_with(self, line_index: &LineIndex) -> Position { + let line_col = line_index.line_col(self); + // TODO: UTF-16 + Position::new(line_col.line as u64, u32::from(line_col.col) as u64) + } +} + +impl ConvWith for TextRange { + type Ctx = LineIndex; + type Output = Range; + + fn conv_with(self, line_index: &LineIndex) -> Range { + Range::new( + self.start().conv_with(line_index), + self.end().conv_with(line_index), + ) + } +} + +impl ConvWith for Range { + type Ctx = LineIndex; + type Output = TextRange; + + fn conv_with(self, line_index: &LineIndex) -> TextRange { + TextRange::from_to( + self.start.conv_with(line_index), + self.end.conv_with(line_index), + ) + } +} + +impl ConvWith for Edit { + type Ctx = LineIndex; + type Output = Vec; + + fn conv_with(self, line_index: &LineIndex) -> Vec { + self.into_atoms() + .into_iter() + .map_conv_with(line_index) + .collect() + } +} + +impl ConvWith for AtomEdit { + type Ctx = LineIndex; + type Output = TextEdit; + + fn conv_with(self, line_index: &LineIndex) -> TextEdit { + TextEdit { + range: self.delete.conv_with(line_index), + new_text: self.insert, + } + } +} + +impl ConvWith for Option { + type Ctx = ::Ctx; + type Output = Option<::Output>; + fn conv_with(self, ctx: &Self::Ctx) -> Self::Output { + self.map(|x| ConvWith::conv_with(x, ctx)) + } +} + +impl<'a> TryConvWith for &'a Url { + type Ctx = ServerWorld; + type Output = FileId; + fn try_conv_with(self, world: &ServerWorld) -> Result { + world.uri_to_file_id(self) + } +} + +impl TryConvWith for FileId { + type Ctx = ServerWorld; + type Output = Url; + fn try_conv_with(self, world: &ServerWorld) -> Result { + world.file_id_to_uri(self) + } +} + +impl<'a> TryConvWith for &'a TextDocumentItem { + type Ctx = ServerWorld; + type Output = FileId; + fn try_conv_with(self, world: &ServerWorld) -> Result { + self.uri.try_conv_with(world) + } +} + +impl<'a> TryConvWith for &'a VersionedTextDocumentIdentifier { + type Ctx = ServerWorld; + type Output = FileId; + fn try_conv_with(self, world: &ServerWorld) -> Result { + self.uri.try_conv_with(world) + } +} + +impl<'a> TryConvWith for &'a TextDocumentIdentifier { + type Ctx = ServerWorld; + type Output = FileId; + fn try_conv_with(self, world: &ServerWorld) -> Result { + world.uri_to_file_id(&self.uri) + } +} + +impl TryConvWith for Vec { + type Ctx = ::Ctx; + type Output = Vec<::Output>; + fn try_conv_with(self, ctx: &Self::Ctx) -> Result { + let mut res = Vec::with_capacity(self.len()); + for item in self { + res.push(item.try_conv_with(ctx)?); + } + Ok(res) + } +} + +impl TryConvWith for SourceChange { + type Ctx = ServerWorld; + type Output = req::SourceChange; + fn try_conv_with(self, world: &ServerWorld) -> Result { + let cursor_position = match self.cursor_position { + None => None, + Some(pos) => { + let line_index = world.analysis().file_line_index(pos.file_id); + Some(TextDocumentPositionParams { + text_document: TextDocumentIdentifier::new(pos.file_id.try_conv_with(world)?), + position: pos.offset.conv_with(&line_index), + }) + } + }; + let source_file_edits = self.source_file_edits.try_conv_with(world)?; + let file_system_edits = self.file_system_edits.try_conv_with(world)?; + Ok(req::SourceChange { + label: self.label, + source_file_edits, + file_system_edits, + cursor_position, + }) + } +} + +impl TryConvWith for SourceFileEdit { + type Ctx = ServerWorld; + type Output = TextDocumentEdit; + fn try_conv_with(self, world: &ServerWorld) -> Result { + let text_document = VersionedTextDocumentIdentifier { + uri: self.file_id.try_conv_with(world)?, + version: None, + }; + let line_index = world.analysis().file_line_index(self.file_id); + let edits = self.edits + .into_iter() + .map_conv_with(&line_index) + .collect(); + Ok(TextDocumentEdit { text_document, edits }) + } +} + +impl TryConvWith for FileSystemEdit { + type Ctx = ServerWorld; + type Output = req::FileSystemEdit; + fn try_conv_with(self, world: &ServerWorld) -> Result { + let res = match self { + FileSystemEdit::CreateFile { anchor, path } => { + let uri = world.file_id_to_uri(anchor)?; + let path = &path.as_str()[3..]; // strip `../` b/c url is weird + let uri = uri.join(path)?; + req::FileSystemEdit::CreateFile { uri } + }, + FileSystemEdit::MoveFile { file, path } => { + let src = world.file_id_to_uri(file)?; + let path = &path.as_str()[3..]; // strip `../` b/c url is weird + let dst = src.join(path)?; + req::FileSystemEdit::MoveFile { src, dst } + }, + }; + Ok(res) + } +} + +pub fn to_location( + file_id: FileId, + range: TextRange, + world: &ServerWorld, + line_index: &LineIndex, +) -> Result { + let url = file_id.try_conv_with(world)?; + let loc = Location::new( + url, + range.conv_with(line_index), + ); + Ok(loc) +} + +pub trait MapConvWith<'a>: Sized { + type Ctx; + type Output; + + fn map_conv_with(self, ctx: &'a Self::Ctx) -> ConvWithIter<'a, Self, Self::Ctx> { + ConvWithIter { iter: self, ctx } + } +} + +impl<'a, I> MapConvWith<'a> for I + where I: Iterator, + I::Item: ConvWith +{ + type Ctx = ::Ctx; + type Output = ::Output; +} + +pub struct ConvWithIter<'a, I, Ctx: 'a> { + iter: I, + ctx: &'a Ctx, +} + +impl<'a, I, Ctx> Iterator for ConvWithIter<'a, I, Ctx> + where + I: Iterator, + I::Item: ConvWith, +{ + type Item = ::Output; + + fn next(&mut self) -> Option { + self.iter.next().map(|item| item.conv_with(self.ctx)) + } +} + diff --git a/crates/ra_lsp_server/src/lib.rs b/crates/ra_lsp_server/src/lib.rs new file mode 100644 index 000000000..d2f76972f --- /dev/null +++ b/crates/ra_lsp_server/src/lib.rs @@ -0,0 +1,37 @@ +#[macro_use] +extern crate failure; +#[macro_use] +extern crate serde_derive; +extern crate serde; +extern crate serde_json; +extern crate languageserver_types; +#[macro_use] +extern crate crossbeam_channel; +extern crate rayon; +#[macro_use] +extern crate log; +extern crate drop_bomb; +extern crate url_serde; +extern crate walkdir; +extern crate im; +extern crate relative_path; +extern crate cargo_metadata; + +extern crate gen_lsp_server; +extern crate ra_editor; +extern crate ra_analysis; +extern crate ra_syntax; + +mod caps; +pub mod req; +mod conv; +mod main_loop; +mod vfs; +mod path_map; +mod server_world; +mod project_model; +pub mod thread_watcher; + +pub type Result = ::std::result::Result; +pub use caps::server_capabilities; +pub use main_loop::main_loop; diff --git a/crates/ra_lsp_server/src/main.rs b/crates/ra_lsp_server/src/main.rs new file mode 100644 index 000000000..c547764f6 --- /dev/null +++ b/crates/ra_lsp_server/src/main.rs @@ -0,0 +1,52 @@ +#[macro_use] +extern crate log; +#[macro_use] +extern crate failure; +extern crate flexi_logger; +extern crate gen_lsp_server; +extern crate ra_lsp_server; + +use flexi_logger::{Logger, Duplicate}; +use gen_lsp_server::{run_server, stdio_transport}; +use ra_lsp_server::Result; + +fn main() -> Result<()> { + ::std::env::set_var("RUST_BACKTRACE", "short"); + Logger::with_env_or_str("error") + .duplicate_to_stderr(Duplicate::All) + .log_to_file() + .directory("log") + .start()?; + info!("lifecycle: server started"); + match ::std::panic::catch_unwind(|| main_inner()) { + Ok(res) => { + info!("lifecycle: terminating process with {:?}", res); + res + } + Err(_) => { + error!("server panicked"); + bail!("server panicked") + } + } +} + +fn main_inner() -> Result<()> { + let (receiver, sender, threads) = stdio_transport(); + let cwd = ::std::env::current_dir()?; + run_server( + ra_lsp_server::server_capabilities(), + |params, r, s| { + let root = params.root_uri + .and_then(|it| it.to_file_path().ok()) + .unwrap_or(cwd); + ra_lsp_server::main_loop(false, root, r, s) + }, + receiver, + sender, + )?; + info!("shutting down IO..."); + threads.join()?; + info!("... IO is down"); + Ok(()) +} + diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs new file mode 100644 index 000000000..568f5344c --- /dev/null +++ b/crates/ra_lsp_server/src/main_loop/handlers.rs @@ -0,0 +1,436 @@ +use std::collections::HashMap; + +use languageserver_types::{ + Diagnostic, DiagnosticSeverity, DocumentSymbol, + Command, TextDocumentIdentifier, + SymbolInformation, Position, Location, TextEdit, + CompletionItem, InsertTextFormat, CompletionItemKind, +}; +use serde_json::to_value; +use ra_analysis::{Query, FileId, RunnableKind, JobToken}; +use ra_syntax::{ + text_utils::contains_offset_nonstrict, +}; + +use ::{ + req::{self, Decoration}, Result, + conv::{Conv, ConvWith, TryConvWith, MapConvWith, to_location}, + server_world::ServerWorld, + project_model::TargetKind, +}; + +pub fn handle_syntax_tree( + world: ServerWorld, + params: req::SyntaxTreeParams, + _token: JobToken, +) -> Result { + let id = params.text_document.try_conv_with(&world)?; + let res = world.analysis().syntax_tree(id); + Ok(res) +} + +pub fn handle_extend_selection( + world: ServerWorld, + params: req::ExtendSelectionParams, + _token: JobToken, +) -> Result { + let file_id = params.text_document.try_conv_with(&world)?; + let file = world.analysis().file_syntax(file_id); + let line_index = world.analysis().file_line_index(file_id); + let selections = params.selections.into_iter() + .map_conv_with(&line_index) + .map(|r| world.analysis().extend_selection(&file, r)) + .map_conv_with(&line_index) + .collect(); + Ok(req::ExtendSelectionResult { selections }) +} + +pub fn handle_find_matching_brace( + world: ServerWorld, + params: req::FindMatchingBraceParams, + _token: JobToken, +) -> Result> { + let file_id = params.text_document.try_conv_with(&world)?; + let file = world.analysis().file_syntax(file_id); + let line_index = world.analysis().file_line_index(file_id); + let res = params.offsets + .into_iter() + .map_conv_with(&line_index) + .map(|offset| { + world.analysis().matching_brace(&file, offset).unwrap_or(offset) + }) + .map_conv_with(&line_index) + .collect(); + Ok(res) +} + +pub fn handle_join_lines( + world: ServerWorld, + params: req::JoinLinesParams, + _token: JobToken, +) -> Result { + let file_id = params.text_document.try_conv_with(&world)?; + let line_index = world.analysis().file_line_index(file_id); + let range = params.range.conv_with(&line_index); + world.analysis().join_lines(file_id, range) + .try_conv_with(&world) +} + +pub fn handle_on_type_formatting( + world: ServerWorld, + params: req::DocumentOnTypeFormattingParams, + _token: JobToken, +) -> Result>> { + if params.ch != "=" { + return Ok(None); + } + + let file_id = params.text_document.try_conv_with(&world)?; + let line_index = world.analysis().file_line_index(file_id); + let offset = params.position.conv_with(&line_index); + let edits = match world.analysis().on_eq_typed(file_id, offset) { + None => return Ok(None), + Some(mut action) => action.source_file_edits.pop().unwrap().edits, + }; + let edits = edits.into_iter().map_conv_with(&line_index).collect(); + Ok(Some(edits)) +} + +pub fn handle_document_symbol( + world: ServerWorld, + params: req::DocumentSymbolParams, + _token: JobToken, +) -> Result> { + let file_id = params.text_document.try_conv_with(&world)?; + let line_index = world.analysis().file_line_index(file_id); + + let mut parents: Vec<(DocumentSymbol, Option)> = Vec::new(); + + for symbol in world.analysis().file_structure(file_id) { + let doc_symbol = DocumentSymbol { + name: symbol.label, + detail: Some("".to_string()), + kind: symbol.kind.conv(), + deprecated: None, + range: symbol.node_range.conv_with(&line_index), + selection_range: symbol.navigation_range.conv_with(&line_index), + children: None, + }; + parents.push((doc_symbol, symbol.parent)); + } + let mut res = Vec::new(); + while let Some((node, parent)) = parents.pop() { + match parent { + None => res.push(node), + Some(i) => { + let children = &mut parents[i].0.children; + if children.is_none() { + *children = Some(Vec::new()); + } + children.as_mut().unwrap().push(node); + } + } + } + + Ok(Some(req::DocumentSymbolResponse::Nested(res))) +} + +pub fn handle_workspace_symbol( + world: ServerWorld, + params: req::WorkspaceSymbolParams, + token: JobToken, +) -> Result>> { + let all_symbols = params.query.contains("#"); + let libs = params.query.contains("*"); + let query = { + let query: String = params.query.chars() + .filter(|&c| c != '#' && c != '*') + .collect(); + let mut q = Query::new(query); + if !all_symbols { + q.only_types(); + } + if libs { + q.libs(); + } + q.limit(128); + q + }; + let mut res = exec_query(&world, query, &token)?; + if res.is_empty() && !all_symbols { + let mut query = Query::new(params.query); + query.limit(128); + res = exec_query(&world, query, &token)?; + } + + return Ok(Some(res)); + + fn exec_query(world: &ServerWorld, query: Query, token: &JobToken) -> Result> { + let mut res = Vec::new(); + for (file_id, symbol) in world.analysis().symbol_search(query, token) { + let line_index = world.analysis().file_line_index(file_id); + let info = SymbolInformation { + name: symbol.name.to_string(), + kind: symbol.kind.conv(), + location: to_location( + file_id, symbol.node_range, + world, &line_index + )?, + container_name: None, + }; + res.push(info); + }; + Ok(res) + } +} + +pub fn handle_goto_definition( + world: ServerWorld, + params: req::TextDocumentPositionParams, + token: JobToken, +) -> Result> { + let file_id = params.text_document.try_conv_with(&world)?; + let line_index = world.analysis().file_line_index(file_id); + let offset = params.position.conv_with(&line_index); + let mut res = Vec::new(); + for (file_id, symbol) in world.analysis().approximately_resolve_symbol(file_id, offset, &token) { + let line_index = world.analysis().file_line_index(file_id); + let location = to_location( + file_id, symbol.node_range, + &world, &line_index, + )?; + res.push(location) + } + Ok(Some(req::GotoDefinitionResponse::Array(res))) +} + +pub fn handle_parent_module( + world: ServerWorld, + params: TextDocumentIdentifier, + _token: JobToken, +) -> Result> { + let file_id = params.try_conv_with(&world)?; + let mut res = Vec::new(); + for (file_id, symbol) in world.analysis().parent_module(file_id) { + let line_index = world.analysis().file_line_index(file_id); + let location = to_location( + file_id, symbol.node_range, + &world, &line_index + )?; + res.push(location); + } + Ok(res) +} + +pub fn handle_runnables( + world: ServerWorld, + params: req::RunnablesParams, + _token: JobToken, +) -> Result> { + let file_id = params.text_document.try_conv_with(&world)?; + let line_index = world.analysis().file_line_index(file_id); + let offset = params.position.map(|it| it.conv_with(&line_index)); + let mut res = Vec::new(); + for runnable in world.analysis().runnables(file_id) { + if let Some(offset) = offset { + if !contains_offset_nonstrict(runnable.range, offset) { + continue; + } + } + + let args = runnable_args(&world, file_id, &runnable.kind); + + let r = req::Runnable { + range: runnable.range.conv_with(&line_index), + label: match &runnable.kind { + RunnableKind::Test { name } => + format!("test {}", name), + RunnableKind::Bin => + "run binary".to_string(), + }, + bin: "cargo".to_string(), + args, + env: { + let mut m = HashMap::new(); + m.insert( + "RUST_BACKTRACE".to_string(), + "short".to_string(), + ); + m + } + }; + res.push(r); + } + return Ok(res); + + fn runnable_args(world: &ServerWorld, file_id: FileId, kind: &RunnableKind) -> Vec { + let spec = if let Some(&crate_id) = world.analysis().crate_for(file_id).first() { + let file_id = world.analysis().crate_root(crate_id); + let path = world.path_map.get_path(file_id); + world.workspaces.iter() + .filter_map(|ws| { + let tgt = ws.target_by_root(path)?; + Some((tgt.package(ws).name(ws).clone(), tgt.name(ws).clone(), tgt.kind(ws))) + }) + .next() + } else { + None + }; + let mut res = Vec::new(); + match kind { + RunnableKind::Test { name } => { + res.push("test".to_string()); + if let Some((pkg_name, tgt_name, tgt_kind)) = spec { + spec_args(pkg_name, tgt_name, tgt_kind, &mut res); + } + res.push("--".to_string()); + res.push(name.to_string()); + res.push("--nocapture".to_string()); + } + RunnableKind::Bin => { + res.push("run".to_string()); + if let Some((pkg_name, tgt_name, tgt_kind)) = spec { + spec_args(pkg_name, tgt_name, tgt_kind, &mut res); + } + } + } + res + } + + fn spec_args(pkg_name: &str, tgt_name: &str, tgt_kind: TargetKind, buf: &mut Vec) { + buf.push("--package".to_string()); + buf.push(pkg_name.to_string()); + match tgt_kind { + TargetKind::Bin => { + buf.push("--bin".to_string()); + buf.push(tgt_name.to_string()); + } + TargetKind::Test => { + buf.push("--test".to_string()); + buf.push(tgt_name.to_string()); + } + TargetKind::Bench => { + buf.push("--bench".to_string()); + buf.push(tgt_name.to_string()); + } + TargetKind::Example => { + buf.push("--example".to_string()); + buf.push(tgt_name.to_string()); + } + TargetKind::Lib => { + buf.push("--lib".to_string()); + } + TargetKind::Other => (), + } + } +} + +pub fn handle_decorations( + world: ServerWorld, + params: TextDocumentIdentifier, + _token: JobToken, +) -> Result> { + let file_id = params.try_conv_with(&world)?; + Ok(highlight(&world, file_id)) +} + +pub fn handle_completion( + world: ServerWorld, + params: req::CompletionParams, + _token: JobToken, +) -> Result> { + let file_id = params.text_document.try_conv_with(&world)?; + let line_index = world.analysis().file_line_index(file_id); + let offset = params.position.conv_with(&line_index); + let items = match world.analysis().completions(file_id, offset) { + None => return Ok(None), + Some(items) => items, + }; + let items = items.into_iter() + .map(|item| { + let mut res = CompletionItem { + label: item.label, + filter_text: item.lookup, + .. Default::default() + }; + if let Some(snip) = item.snippet { + res.insert_text = Some(snip); + res.insert_text_format = Some(InsertTextFormat::Snippet); + res.kind = Some(CompletionItemKind::Keyword); + }; + res + }) + .collect(); + + Ok(Some(req::CompletionResponse::Array(items))) +} + +pub fn handle_code_action( + world: ServerWorld, + params: req::CodeActionParams, + _token: JobToken, +) -> Result>> { + let file_id = params.text_document.try_conv_with(&world)?; + let line_index = world.analysis().file_line_index(file_id); + let range = params.range.conv_with(&line_index); + + let assists = world.analysis().assists(file_id, range).into_iter(); + let fixes = world.analysis().diagnostics(file_id).into_iter() + .filter_map(|d| Some((d.range, d.fix?))) + .filter(|(range, _fix)| contains_offset_nonstrict(*range, range.start())) + .map(|(_range, fix)| fix); + + let mut res = Vec::new(); + for source_edit in assists.chain(fixes) { + let title = source_edit.label.clone(); + let edit = source_edit.try_conv_with(&world)?; + let cmd = Command { + title, + command: "libsyntax-rust.applySourceChange".to_string(), + arguments: Some(vec![to_value(edit).unwrap()]), + }; + res.push(cmd); + } + + Ok(Some(res)) +} + +pub fn publish_diagnostics( + world: ServerWorld, + file_id: FileId, +) -> Result { + let uri = world.file_id_to_uri(file_id)?; + let line_index = world.analysis().file_line_index(file_id); + let diagnostics = world.analysis().diagnostics(file_id) + .into_iter() + .map(|d| Diagnostic { + range: d.range.conv_with(&line_index), + severity: Some(DiagnosticSeverity::Error), + code: None, + source: Some("libsyntax2".to_string()), + message: d.message, + related_information: None, + }).collect(); + Ok(req::PublishDiagnosticsParams { uri, diagnostics }) +} + +pub fn publish_decorations( + world: ServerWorld, + file_id: FileId, +) -> Result { + let uri = world.file_id_to_uri(file_id)?; + Ok(req::PublishDecorationsParams { + uri, + decorations: highlight(&world, file_id), + }) +} + +fn highlight(world: &ServerWorld, file_id: FileId) -> Vec { + let line_index = world.analysis().file_line_index(file_id); + world.analysis().highlight(file_id) + .into_iter() + .map(|h| Decoration { + range: h.range.conv_with(&line_index), + tag: h.tag, + }).collect() +} diff --git a/crates/ra_lsp_server/src/main_loop/mod.rs b/crates/ra_lsp_server/src/main_loop/mod.rs new file mode 100644 index 000000000..2b2279e97 --- /dev/null +++ b/crates/ra_lsp_server/src/main_loop/mod.rs @@ -0,0 +1,419 @@ +mod handlers; +mod subscriptions; + +use std::{ + path::PathBuf, + collections::{HashMap}, +}; + +use serde::{Serialize, de::DeserializeOwned}; +use crossbeam_channel::{unbounded, Sender, Receiver}; +use rayon::{self, ThreadPool}; +use languageserver_types::{NumberOrString}; +use ra_analysis::{FileId, JobHandle, JobToken, LibraryData}; +use gen_lsp_server::{ + RawRequest, RawNotification, RawMessage, RawResponse, ErrorCode, + handle_shutdown, +}; + +use { + req, + Result, + vfs::{self, FileEvent}, + server_world::{ServerWorldState, ServerWorld}, + main_loop::subscriptions::{Subscriptions}, + project_model::{CargoWorkspace, workspace_loader}, + thread_watcher::Worker, +}; + +#[derive(Debug)] +enum Task { + Respond(RawResponse), + Notify(RawNotification), +} + +pub fn main_loop( + internal_mode: bool, + root: PathBuf, + msg_receriver: &mut Receiver, + msg_sender: &mut Sender, +) -> Result<()> { + let pool = rayon::ThreadPoolBuilder::new() + .num_threads(4) + .panic_handler(|_| error!("thread panicked :(")) + .build() + .unwrap(); + let (task_sender, task_receiver) = unbounded::(); + let (fs_worker, fs_watcher) = vfs::roots_loader(); + let (ws_worker, ws_watcher) = workspace_loader(); + + info!("server initialized, serving requests"); + let mut state = ServerWorldState::new(); + + let mut pending_requests = HashMap::new(); + let mut subs = Subscriptions::new(); + let main_res = main_loop_inner( + internal_mode, + root, + &pool, + msg_sender, + msg_receriver, + task_sender, + task_receiver.clone(), + fs_worker, + ws_worker, + &mut state, + &mut pending_requests, + &mut subs, + ); + + info!("waiting for tasks to finish..."); + task_receiver.for_each(|task| on_task(task, msg_sender, &mut pending_requests)); + info!("...tasks have finished"); + info!("joining threadpool..."); + drop(pool); + info!("...threadpool has finished"); + + let fs_res = fs_watcher.stop(); + let ws_res = ws_watcher.stop(); + + main_res?; + fs_res?; + ws_res?; + + Ok(()) +} + +fn main_loop_inner( + internal_mode: bool, + ws_root: PathBuf, + pool: &ThreadPool, + msg_sender: &mut Sender, + msg_receiver: &mut Receiver, + task_sender: Sender, + task_receiver: Receiver, + fs_worker: Worker)>, + ws_worker: Worker>, + state: &mut ServerWorldState, + pending_requests: &mut HashMap, + subs: &mut Subscriptions, +) -> Result<()> { + let (libdata_sender, libdata_receiver) = unbounded(); + ws_worker.send(ws_root.clone()); + fs_worker.send(ws_root.clone()); + loop { + #[derive(Debug)] + enum Event { + Msg(RawMessage), + Task(Task), + Fs(PathBuf, Vec), + Ws(Result), + Lib(LibraryData), + } + trace!("selecting"); + let event = select! { + recv(msg_receiver, msg) => match msg { + Some(msg) => Event::Msg(msg), + None => bail!("client exited without shutdown"), + }, + recv(task_receiver, task) => Event::Task(task.unwrap()), + recv(fs_worker.out, events) => match events { + None => bail!("roots watcher died"), + Some((pb, events)) => Event::Fs(pb, events), + } + recv(ws_worker.out, ws) => match ws { + None => bail!("workspace watcher died"), + Some(ws) => Event::Ws(ws), + } + recv(libdata_receiver, data) => Event::Lib(data.unwrap()) + }; + let mut state_changed = false; + match event { + Event::Task(task) => on_task(task, msg_sender, pending_requests), + Event::Fs(root, events) => { + info!("fs change, {}, {} events", root.display(), events.len()); + if root == ws_root { + state.apply_fs_changes(events); + } else { + let (files, resolver) = state.events_to_files(events); + let sender = libdata_sender.clone(); + pool.spawn(move || { + let start = ::std::time::Instant::now(); + info!("indexing {} ... ", root.display()); + let data = LibraryData::prepare(files, resolver); + info!("indexed {:?} {}", start.elapsed(), root.display()); + sender.send(data); + }); + } + state_changed = true; + } + Event::Ws(ws) => { + match ws { + Ok(ws) => { + let workspaces = vec![ws]; + feedback(internal_mode, "workspace loaded", msg_sender); + for ws in workspaces.iter() { + for pkg in ws.packages().filter(|pkg| !pkg.is_member(ws)) { + debug!("sending root, {}", pkg.root(ws).to_path_buf().display()); + fs_worker.send(pkg.root(ws).to_path_buf()); + } + } + state.set_workspaces(workspaces); + state_changed = true; + } + Err(e) => warn!("loading workspace failed: {}", e), + } + } + Event::Lib(lib) => { + feedback(internal_mode, "library loaded", msg_sender); + state.add_lib(lib); + } + Event::Msg(msg) => { + match msg { + RawMessage::Request(req) => { + let req = match handle_shutdown(req, msg_sender) { + Some(req) => req, + None => return Ok(()), + }; + match on_request(state, pending_requests, pool, &task_sender, req)? { + None => (), + Some(req) => { + error!("unknown request: {:?}", req); + let resp = RawResponse::err( + req.id, + ErrorCode::MethodNotFound as i32, + "unknown request".to_string(), + ); + msg_sender.send(RawMessage::Response(resp)) + } + } + } + RawMessage::Notification(not) => { + on_notification(msg_sender, state, pending_requests, subs, not)?; + state_changed = true; + } + RawMessage::Response(resp) => { + error!("unexpected response: {:?}", resp) + } + } + } + }; + + if state_changed { + update_file_notifications_on_threadpool( + pool, + state.snapshot(), + task_sender.clone(), + subs.subscriptions(), + ) + } + } +} + +fn on_task( + task: Task, + msg_sender: &mut Sender, + pending_requests: &mut HashMap, +) { + match task { + Task::Respond(response) => { + if let Some(handle) = pending_requests.remove(&response.id) { + assert!(handle.has_completed()); + } + msg_sender.send(RawMessage::Response(response)) + } + Task::Notify(n) => + msg_sender.send(RawMessage::Notification(n)), + } +} + +fn on_request( + world: &mut ServerWorldState, + pending_requests: &mut HashMap, + pool: &ThreadPool, + sender: &Sender, + req: RawRequest, +) -> Result> { + let mut pool_dispatcher = PoolDispatcher { + req: Some(req), + res: None, + pool, world, sender + }; + let req = pool_dispatcher + .on::(handlers::handle_syntax_tree)? + .on::(handlers::handle_extend_selection)? + .on::(handlers::handle_find_matching_brace)? + .on::(handlers::handle_join_lines)? + .on::(handlers::handle_on_type_formatting)? + .on::(handlers::handle_document_symbol)? + .on::(handlers::handle_workspace_symbol)? + .on::(handlers::handle_goto_definition)? + .on::(handlers::handle_parent_module)? + .on::(handlers::handle_runnables)? + .on::(handlers::handle_decorations)? + .on::(handlers::handle_completion)? + .on::(handlers::handle_code_action)? + .finish(); + match req { + Ok((id, handle)) => { + let inserted = pending_requests.insert(id, handle).is_none(); + assert!(inserted, "duplicate request: {}", id); + Ok(None) + }, + Err(req) => Ok(Some(req)), + } +} + +fn on_notification( + msg_sender: &mut Sender, + state: &mut ServerWorldState, + pending_requests: &mut HashMap, + subs: &mut Subscriptions, + not: RawNotification, +) -> Result<()> { + let not = match not.cast::() { + Ok(params) => { + let id = match params.id { + NumberOrString::Number(id) => id, + NumberOrString::String(id) => { + panic!("string id's not supported: {:?}", id); + } + }; + if let Some(handle) = pending_requests.remove(&id) { + handle.cancel(); + } + return Ok(()) + } + Err(not) => not, + }; + let not = match not.cast::() { + Ok(params) => { + let uri = params.text_document.uri; + let path = uri.to_file_path() + .map_err(|()| format_err!("invalid uri: {}", uri))?; + let file_id = state.add_mem_file(path, params.text_document.text); + subs.add_sub(file_id); + return Ok(()) + } + Err(not) => not, + }; + let not = match not.cast::() { + Ok(mut params) => { + let uri = params.text_document.uri; + let path = uri.to_file_path() + .map_err(|()| format_err!("invalid uri: {}", uri))?; + let text = params.content_changes.pop() + .ok_or_else(|| format_err!("empty changes"))? + .text; + state.change_mem_file(path.as_path(), text)?; + return Ok(()) + } + Err(not) => not, + }; + let not = match not.cast::() { + Ok(params) => { + let uri = params.text_document.uri; + let path = uri.to_file_path() + .map_err(|()| format_err!("invalid uri: {}", uri))?; + let file_id = state.remove_mem_file(path.as_path())?; + subs.remove_sub(file_id); + let params = req::PublishDiagnosticsParams { uri, diagnostics: Vec::new() }; + let not = RawNotification::new::(¶ms); + msg_sender.send(RawMessage::Notification(not)); + return Ok(()) + } + Err(not) => not, + }; + error!("unhandled notification: {:?}", not); + Ok(()) +} + +struct PoolDispatcher<'a> { + req: Option, + res: Option<(u64, JobHandle)>, + pool: &'a ThreadPool, + world: &'a ServerWorldState, + sender: &'a Sender, +} + +impl<'a> PoolDispatcher<'a> { + fn on<'b, R>( + &'b mut self, + f: fn(ServerWorld, R::Params, JobToken) -> Result + ) -> Result<&'b mut Self> + where R: req::Request, + R::Params: DeserializeOwned + Send + 'static, + R::Result: Serialize + 'static, + { + let req = match self.req.take() { + None => return Ok(self), + Some(req) => req, + }; + match req.cast::() { + Ok((id, params)) => { + let (handle, token) = JobHandle::new(); + let world = self.world.snapshot(); + let sender = self.sender.clone(); + self.pool.spawn(move || { + let resp = match f(world, params, token) { + Ok(resp) => RawResponse::ok::(id, &resp), + Err(e) => RawResponse::err(id, ErrorCode::InternalError as i32, e.to_string()), + }; + let task = Task::Respond(resp); + sender.send(task); + }); + self.res = Some((id, handle)); + } + Err(req) => { + self.req = Some(req) + } + } + Ok(self) + } + + fn finish(&mut self) -> ::std::result::Result<(u64, JobHandle), RawRequest> { + match (self.res.take(), self.req.take()) { + (Some(res), None) => Ok(res), + (None, Some(req)) => Err(req), + _ => unreachable!(), + } + } +} + +fn update_file_notifications_on_threadpool( + pool: &ThreadPool, + world: ServerWorld, + sender: Sender, + subscriptions: Vec, +) { + pool.spawn(move || { + for file_id in subscriptions { + match handlers::publish_diagnostics(world.clone(), file_id) { + Err(e) => { + error!("failed to compute diagnostics: {:?}", e) + } + Ok(params) => { + let not = RawNotification::new::(¶ms); + sender.send(Task::Notify(not)); + } + } + match handlers::publish_decorations(world.clone(), file_id) { + Err(e) => { + error!("failed to compute decorations: {:?}", e) + } + Ok(params) => { + let not = RawNotification::new::(¶ms); + sender.send(Task::Notify(not)) + } + } + } + }); +} + +fn feedback(intrnal_mode: bool, msg: &str, sender: &Sender) { + if !intrnal_mode { + return; + } + let not = RawNotification::new::(&msg.to_string()); + sender.send(RawMessage::Notification(not)); +} diff --git a/crates/ra_lsp_server/src/main_loop/subscriptions.rs b/crates/ra_lsp_server/src/main_loop/subscriptions.rs new file mode 100644 index 000000000..27f92cc9a --- /dev/null +++ b/crates/ra_lsp_server/src/main_loop/subscriptions.rs @@ -0,0 +1,21 @@ +use std::collections::HashSet; +use ra_analysis::FileId; + +pub struct Subscriptions { + subs: HashSet, +} + +impl Subscriptions { + pub fn new() -> Subscriptions { + Subscriptions { subs: HashSet::new() } + } + pub fn add_sub(&mut self, file_id: FileId) { + self.subs.insert(file_id); + } + pub fn remove_sub(&mut self, file_id: FileId) { + self.subs.remove(&file_id); + } + pub fn subscriptions(&self) -> Vec { + self.subs.iter().cloned().collect() + } +} diff --git a/crates/ra_lsp_server/src/path_map.rs b/crates/ra_lsp_server/src/path_map.rs new file mode 100644 index 000000000..19c3b1d3b --- /dev/null +++ b/crates/ra_lsp_server/src/path_map.rs @@ -0,0 +1,110 @@ +use std::path::{PathBuf, Path, Component}; +use im; +use relative_path::RelativePath; +use ra_analysis::{FileId, FileResolver}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Root { + Workspace, Lib +} + +#[derive(Debug, Default, Clone)] +pub struct PathMap { + next_id: u32, + path2id: im::HashMap, + id2path: im::HashMap, + id2root: im::HashMap, +} + +impl PathMap { + pub fn new() -> PathMap { + Default::default() + } + pub fn get_or_insert(&mut self, path: PathBuf, root: Root) -> FileId { + self.path2id.get(path.as_path()) + .map(|&id| id) + .unwrap_or_else(|| { + let id = self.new_file_id(); + self.insert(path, id, root); + id + }) + } + pub fn get_id(&self, path: &Path) -> Option { + self.path2id.get(path).map(|&id| id) + } + pub fn get_path(&self, file_id: FileId) -> &Path { + self.id2path.get(&file_id) + .unwrap() + .as_path() + } + pub fn get_root(&self, file_id: FileId) -> Root { + self.id2root[&file_id] + } + fn insert(&mut self, path: PathBuf, file_id: FileId, root: Root) { + self.path2id.insert(path.clone(), file_id); + self.id2path.insert(file_id, path.clone()); + self.id2root.insert(file_id, root); + } + + fn new_file_id(&mut self) -> FileId { + let id = FileId(self.next_id); + self.next_id += 1; + id + } +} + +impl FileResolver for PathMap { + fn file_stem(&self, file_id: FileId) -> String { + self.get_path(file_id).file_stem().unwrap().to_str().unwrap().to_string() + } + + fn resolve(&self, file_id: FileId, path: &RelativePath) -> Option { + let path = path.to_path(&self.get_path(file_id)); + let path = normalize(&path); + self.get_id(&path) + } +} + +fn normalize(path: &Path) -> PathBuf { + let mut components = path.components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + ret.push(c); + } + } + } + ret +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_resolve() { + let mut m = PathMap::new(); + let id1 = m.get_or_insert(PathBuf::from("/foo"), Root::Workspace); + let id2 = m.get_or_insert(PathBuf::from("/foo/bar.rs"), Root::Workspace); + assert_eq!( + m.resolve(id1, &RelativePath::new("bar.rs")), + Some(id2), + ) + } +} + diff --git a/crates/ra_lsp_server/src/project_model.rs b/crates/ra_lsp_server/src/project_model.rs new file mode 100644 index 000000000..5db34e3e5 --- /dev/null +++ b/crates/ra_lsp_server/src/project_model.rs @@ -0,0 +1,175 @@ +use std::{ + collections::{HashMap, HashSet}, + path::{Path, PathBuf}, +}; +use cargo_metadata::{metadata_run, CargoOpt}; +use ra_syntax::SmolStr; + +use { + Result, + thread_watcher::{Worker, ThreadWatcher}, +}; + +#[derive(Debug, Clone)] +pub struct CargoWorkspace { + packages: Vec, + targets: Vec, +} + +#[derive(Clone, Copy, Debug, Serialize)] +pub struct Package(usize); +#[derive(Clone, Copy, Debug, Serialize)] +pub struct Target(usize); + +#[derive(Debug, Clone)] +struct PackageData { + name: SmolStr, + manifest: PathBuf, + targets: Vec, + is_member: bool, +} + +#[derive(Debug, Clone)] +struct TargetData { + pkg: Package, + name: SmolStr, + root: PathBuf, + kind: TargetKind, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TargetKind { + Bin, Lib, Example, Test, Bench, Other, +} + +impl Package { + pub fn name(self, ws: &CargoWorkspace) -> &str { + ws.pkg(self).name.as_str() + } + pub fn root(self, ws: &CargoWorkspace) -> &Path { + ws.pkg(self).manifest.parent().unwrap() + } + pub fn targets<'a>(self, ws: &'a CargoWorkspace) -> impl Iterator + 'a { + ws.pkg(self).targets.iter().cloned() + } + pub fn is_member(self, ws: &CargoWorkspace) -> bool { + ws.pkg(self).is_member + } +} + +impl Target { + pub fn package(self, ws: &CargoWorkspace) -> Package { + ws.tgt(self).pkg + } + pub fn name(self, ws: &CargoWorkspace) -> &str { + ws.tgt(self).name.as_str() + } + pub fn root(self, ws: &CargoWorkspace) -> &Path { + ws.tgt(self).root.as_path() + } + pub fn kind(self, ws: &CargoWorkspace) -> TargetKind { + ws.tgt(self).kind + } +} + +impl CargoWorkspace { + pub fn from_cargo_metadata(path: &Path) -> Result { + let cargo_toml = find_cargo_toml(path)?; + let meta = metadata_run( + Some(cargo_toml.as_path()), + true, + Some(CargoOpt::AllFeatures) + ).map_err(|e| format_err!("cargo metadata failed: {}", e))?; + let mut pkg_by_id = HashMap::new(); + let mut packages = Vec::new(); + let mut targets = Vec::new(); + + let ws_members: HashSet = meta.workspace_members + .into_iter() + .map(|it| it.raw) + .collect(); + + for meta_pkg in meta.packages { + let pkg = Package(packages.len()); + let is_member = ws_members.contains(&meta_pkg.id); + pkg_by_id.insert(meta_pkg.id.clone(), pkg); + let mut pkg_data = PackageData { + name: meta_pkg.name.into(), + manifest: PathBuf::from(meta_pkg.manifest_path), + targets: Vec::new(), + is_member, + }; + for meta_tgt in meta_pkg.targets { + let tgt = Target(targets.len()); + targets.push(TargetData { + pkg, + name: meta_tgt.name.into(), + root: PathBuf::from(meta_tgt.src_path), + kind: TargetKind::new(meta_tgt.kind.as_slice()), + }); + pkg_data.targets.push(tgt); + } + packages.push(pkg_data) + } + + Ok(CargoWorkspace { packages, targets }) + } + pub fn packages<'a>(&'a self) -> impl Iterator + 'a { + (0..self.packages.len()).map(Package) + } + pub fn target_by_root(&self, root: &Path) -> Option { + self.packages() + .filter_map(|pkg| pkg.targets(self).find(|it| it.root(self) == root)) + .next() + } + fn pkg(&self, pkg: Package) -> &PackageData { + &self.packages[pkg.0] + } + fn tgt(&self, tgt: Target) -> &TargetData { + &self.targets[tgt.0] + } +} + +fn find_cargo_toml(path: &Path) -> Result { + if path.ends_with("Cargo.toml") { + return Ok(path.to_path_buf()); + } + let mut curr = Some(path); + while let Some(path) = curr { + let candidate = path.join("Cargo.toml"); + if candidate.exists() { + return Ok(candidate); + } + curr = path.parent(); + } + bail!("can't find Cargo.toml at {}", path.display()) +} + +impl TargetKind { + fn new(kinds: &[String]) -> TargetKind { + for kind in kinds { + return match kind.as_str() { + "bin" => TargetKind::Bin, + "test" => TargetKind::Test, + "bench" => TargetKind::Bench, + "example" => TargetKind::Example, + _ if kind.contains("lib") => TargetKind::Lib, + _ => continue, + } + } + TargetKind::Other + } +} + +pub fn workspace_loader() -> (Worker>, ThreadWatcher) { + Worker::>::spawn( + "workspace loader", + 1, + |input_receiver, output_sender| { + input_receiver + .into_iter() + .map(|path| CargoWorkspace::from_cargo_metadata(path.as_path())) + .for_each(|it| output_sender.send(it)) + } + ) +} diff --git a/crates/ra_lsp_server/src/req.rs b/crates/ra_lsp_server/src/req.rs new file mode 100644 index 000000000..4af61dbbd --- /dev/null +++ b/crates/ra_lsp_server/src/req.rs @@ -0,0 +1,176 @@ +use std::collections::HashMap; +use languageserver_types::{TextDocumentIdentifier, Range, Url, Position, Location}; +use url_serde; + +pub use languageserver_types::{ + request::*, notification::*, + InitializeResult, PublishDiagnosticsParams, + DocumentSymbolParams, DocumentSymbolResponse, + CodeActionParams, ApplyWorkspaceEditParams, + ExecuteCommandParams, + WorkspaceSymbolParams, + TextDocumentPositionParams, + TextEdit, + CompletionParams, CompletionResponse, + DocumentOnTypeFormattingParams, + TextDocumentEdit, +}; + +pub enum SyntaxTree {} + +impl Request for SyntaxTree { + type Params = SyntaxTreeParams; + type Result = String; + const METHOD: &'static str = "m/syntaxTree"; +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct SyntaxTreeParams { + pub text_document: TextDocumentIdentifier +} + +pub enum ExtendSelection {} + +impl Request for ExtendSelection { + type Params = ExtendSelectionParams; + type Result = ExtendSelectionResult; + const METHOD: &'static str = "m/extendSelection"; +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ExtendSelectionParams { + pub text_document: TextDocumentIdentifier, + pub selections: Vec, +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ExtendSelectionResult { + pub selections: Vec, +} + +pub enum FindMatchingBrace {} + +impl Request for FindMatchingBrace { + type Params = FindMatchingBraceParams; + type Result = Vec; + const METHOD: &'static str = "m/findMatchingBrace"; +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct FindMatchingBraceParams { + pub text_document: TextDocumentIdentifier, + pub offsets: Vec, +} + +pub enum DecorationsRequest {} + +impl Request for DecorationsRequest { + type Params = TextDocumentIdentifier; + type Result = Vec; + const METHOD: &'static str = "m/decorationsRequest"; +} + +pub enum PublishDecorations {} + +impl Notification for PublishDecorations { + type Params = PublishDecorationsParams; + const METHOD: &'static str = "m/publishDecorations"; +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PublishDecorationsParams { + #[serde(with = "url_serde")] + pub uri: Url, + pub decorations: Vec, +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Decoration { + pub range: Range, + pub tag: &'static str +} + +pub enum ParentModule {} + +impl Request for ParentModule { + type Params = TextDocumentIdentifier; + type Result = Vec; + const METHOD: &'static str = "m/parentModule"; +} + +pub enum JoinLines {} + +impl Request for JoinLines { + type Params = JoinLinesParams; + type Result = SourceChange; + const METHOD: &'static str = "m/joinLines"; +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct JoinLinesParams { + pub text_document: TextDocumentIdentifier, + pub range: Range, +} + +pub enum Runnables {} + +impl Request for Runnables { + type Params = RunnablesParams; + type Result = Vec; + const METHOD: &'static str = "m/runnables"; +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RunnablesParams { + pub text_document: TextDocumentIdentifier, + pub position: Option, +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Runnable { + pub range: Range, + pub label: String, + pub bin: String, + pub args: Vec, + pub env: HashMap, +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct SourceChange { + pub label: String, + pub source_file_edits: Vec, + pub file_system_edits: Vec, + pub cursor_position: Option, +} + +#[derive(Serialize, Debug)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum FileSystemEdit { + CreateFile { + #[serde(with = "url_serde")] + uri: Url + }, + MoveFile { + #[serde(with = "url_serde")] + src: Url, + #[serde(with = "url_serde")] + dst: Url, + } +} + +pub enum InternalFeedback {} + +impl Notification for InternalFeedback { + const METHOD: &'static str = "internalFeedback"; + type Params = String; +} diff --git a/crates/ra_lsp_server/src/server_world.rs b/crates/ra_lsp_server/src/server_world.rs new file mode 100644 index 000000000..865f7c491 --- /dev/null +++ b/crates/ra_lsp_server/src/server_world.rs @@ -0,0 +1,167 @@ +use std::{ + fs, + path::{PathBuf, Path}, + collections::HashMap, + sync::Arc, +}; + +use languageserver_types::Url; +use ra_analysis::{FileId, AnalysisHost, Analysis, CrateGraph, CrateId, LibraryData, FileResolver}; + +use { + Result, + path_map::{PathMap, Root}, + vfs::{FileEvent, FileEventKind}, + project_model::CargoWorkspace, +}; + +#[derive(Debug)] +pub struct ServerWorldState { + pub workspaces: Arc>, + pub analysis_host: AnalysisHost, + pub path_map: PathMap, + pub mem_map: HashMap>, +} + +#[derive(Clone)] +pub struct ServerWorld { + pub workspaces: Arc>, + pub analysis: Analysis, + pub path_map: PathMap, +} + +impl ServerWorldState { + pub fn new() -> ServerWorldState { + ServerWorldState { + workspaces: Arc::new(Vec::new()), + analysis_host: AnalysisHost::new(), + path_map: PathMap::new(), + mem_map: HashMap::new(), + } + } + pub fn apply_fs_changes(&mut self, events: Vec) { + { + let pm = &mut self.path_map; + let mm = &mut self.mem_map; + let changes = events.into_iter() + .map(|event| { + let text = match event.kind { + FileEventKind::Add(text) => Some(text), + }; + (event.path, text) + }) + .map(|(path, text)| { + (pm.get_or_insert(path, Root::Workspace), text) + }) + .filter_map(|(id, text)| { + if mm.contains_key(&id) { + mm.insert(id, text); + None + } else { + Some((id, text)) + } + }); + self.analysis_host.change_files(changes); + } + self.analysis_host.set_file_resolver(Arc::new(self.path_map.clone())); + } + pub fn events_to_files(&mut self, events: Vec) -> (Vec<(FileId, String)>, Arc) { + let files = { + let pm = &mut self.path_map; + events.into_iter() + .map(|event| { + let text = match event.kind { + FileEventKind::Add(text) => text, + }; + (event.path, text) + }) + .map(|(path, text)| (pm.get_or_insert(path, Root::Lib), text)) + .collect() + }; + let resolver = Arc::new(self.path_map.clone()); + (files, resolver) + } + pub fn add_lib(&mut self, data: LibraryData) { + self.analysis_host.add_library(data); + } + + pub fn add_mem_file(&mut self, path: PathBuf, text: String) -> FileId { + let file_id = self.path_map.get_or_insert(path, Root::Workspace); + self.analysis_host.set_file_resolver(Arc::new(self.path_map.clone())); + self.mem_map.insert(file_id, None); + if self.path_map.get_root(file_id) != Root::Lib { + self.analysis_host.change_file(file_id, Some(text)); + } + file_id + } + + pub fn change_mem_file(&mut self, path: &Path, text: String) -> Result<()> { + let file_id = self.path_map.get_id(path).ok_or_else(|| { + format_err!("change to unknown file: {}", path.display()) + })?; + if self.path_map.get_root(file_id) != Root::Lib { + self.analysis_host.change_file(file_id, Some(text)); + } + Ok(()) + } + + pub fn remove_mem_file(&mut self, path: &Path) -> Result { + let file_id = self.path_map.get_id(path).ok_or_else(|| { + format_err!("change to unknown file: {}", path.display()) + })?; + match self.mem_map.remove(&file_id) { + Some(_) => (), + None => bail!("unmatched close notification"), + }; + // Do this via file watcher ideally. + let text = fs::read_to_string(path).ok(); + if self.path_map.get_root(file_id) != Root::Lib { + self.analysis_host.change_file(file_id, text); + } + Ok(file_id) + } + pub fn set_workspaces(&mut self, ws: Vec) { + let mut crate_roots = HashMap::new(); + ws.iter() + .flat_map(|ws| { + ws.packages() + .flat_map(move |pkg| pkg.targets(ws)) + .map(move |tgt| tgt.root(ws)) + }) + .for_each(|root| { + if let Some(file_id) = self.path_map.get_id(root) { + let crate_id = CrateId(crate_roots.len() as u32); + crate_roots.insert(crate_id, file_id); + } + }); + let crate_graph = CrateGraph { crate_roots }; + self.workspaces = Arc::new(ws); + self.analysis_host.set_crate_graph(crate_graph); + } + pub fn snapshot(&self) -> ServerWorld { + ServerWorld { + workspaces: Arc::clone(&self.workspaces), + analysis: self.analysis_host.analysis(), + path_map: self.path_map.clone() + } + } +} + +impl ServerWorld { + pub fn analysis(&self) -> &Analysis { + &self.analysis + } + + pub fn uri_to_file_id(&self, uri: &Url) -> Result { + let path = uri.to_file_path() + .map_err(|()| format_err!("invalid uri: {}", uri))?; + self.path_map.get_id(&path).ok_or_else(|| format_err!("unknown file: {}", path.display())) + } + + pub fn file_id_to_uri(&self, id: FileId) -> Result { + let path = self.path_map.get_path(id); + let url = Url::from_file_path(path) + .map_err(|()| format_err!("can't convert path to url: {}", path.display()))?; + Ok(url) + } +} diff --git a/crates/ra_lsp_server/src/thread_watcher.rs b/crates/ra_lsp_server/src/thread_watcher.rs new file mode 100644 index 000000000..86a3a91e0 --- /dev/null +++ b/crates/ra_lsp_server/src/thread_watcher.rs @@ -0,0 +1,70 @@ +use std::thread; +use crossbeam_channel::{bounded, unbounded, Sender, Receiver}; +use drop_bomb::DropBomb; +use Result; + +pub struct Worker { + pub inp: Sender, + pub out: Receiver, +} + +impl Worker { + pub fn spawn(name: &'static str, buf: usize, f: F) -> (Self, ThreadWatcher) + where + F: FnOnce(Receiver, Sender) + Send + 'static, + I: Send + 'static, + O: Send + 'static, + { + let ((inp, out), inp_r, out_s) = worker_chan(buf); + let worker = Worker { inp, out }; + let watcher = ThreadWatcher::spawn(name, move || f(inp_r, out_s)); + (worker, watcher) + } + + pub fn stop(self) -> Receiver { + self.out + } + + pub fn send(&self, item: I) { + self.inp.send(item) + } +} + +pub struct ThreadWatcher { + name: &'static str, + thread: thread::JoinHandle<()>, + bomb: DropBomb, +} + +impl ThreadWatcher { + fn spawn(name: &'static str, f: impl FnOnce() + Send + 'static) -> ThreadWatcher { + let thread = thread::spawn(f); + ThreadWatcher { + name, + thread, + bomb: DropBomb::new(format!("ThreadWatcher {} was not stopped", name)), + } + } + + pub fn stop(mut self) -> Result<()> { + info!("waiting for {} to finish ...", self.name); + let name = self.name; + self.bomb.defuse(); + let res = self.thread.join() + .map_err(|_| format_err!("ThreadWatcher {} died", name)); + match &res { + Ok(()) => info!("... {} terminated with ok", name), + Err(_) => error!("... {} terminated with err", name) + } + res + } +} + +/// Sets up worker channels in a deadlock-avoind way. +/// If one sets both input and output buffers to a fixed size, +/// a worker might get stuck. +fn worker_chan(buf: usize) -> ((Sender, Receiver), Receiver, Sender) { + let (input_sender, input_receiver) = bounded::(buf); + let (output_sender, output_receiver) = unbounded::(); + ((input_sender, output_receiver), input_receiver, output_sender) +} diff --git a/crates/ra_lsp_server/src/vfs.rs b/crates/ra_lsp_server/src/vfs.rs new file mode 100644 index 000000000..a1c1783f2 --- /dev/null +++ b/crates/ra_lsp_server/src/vfs.rs @@ -0,0 +1,71 @@ +use std::{ + path::{PathBuf, Path}, + fs, +}; + +use walkdir::WalkDir; + +use { + thread_watcher::{Worker, ThreadWatcher}, +}; + + +#[derive(Debug)] +pub struct FileEvent { + pub path: PathBuf, + pub kind: FileEventKind, +} + +#[derive(Debug)] +pub enum FileEventKind { + Add(String), +} + +pub fn roots_loader() -> (Worker)>, ThreadWatcher) { + Worker::)>::spawn( + "roots loader", + 128, |input_receiver, output_sender| { + input_receiver + .into_iter() + .map(|path| { + debug!("loading {} ...", path.as_path().display()); + let events = load_root(path.as_path()); + debug!("... loaded {}", path.as_path().display()); + (path, events) + }) + .for_each(|it| output_sender.send(it)) + } + ) +} + +fn load_root(path: &Path) -> Vec { + let mut res = Vec::new(); + for entry in WalkDir::new(path) { + let entry = match entry { + Ok(entry) => entry, + Err(e) => { + warn!("watcher error: {}", e); + continue; + } + }; + if !entry.file_type().is_file() { + continue; + } + let path = entry.path(); + if path.extension().and_then(|os| os.to_str()) != Some("rs") { + continue; + } + let text = match fs::read_to_string(path) { + Ok(text) => text, + Err(e) => { + warn!("watcher error: {}", e); + continue; + } + }; + res.push(FileEvent { + path: path.to_owned(), + kind: FileEventKind::Add(text), + }) + } + res +} diff --git a/crates/ra_lsp_server/tests/heavy_tests/main.rs b/crates/ra_lsp_server/tests/heavy_tests/main.rs new file mode 100644 index 000000000..dced45f55 --- /dev/null +++ b/crates/ra_lsp_server/tests/heavy_tests/main.rs @@ -0,0 +1,99 @@ +#[macro_use] +extern crate crossbeam_channel; +extern crate tempdir; +extern crate languageserver_types; +extern crate serde; +extern crate serde_json; +extern crate gen_lsp_server; +extern crate flexi_logger; +extern crate ra_lsp_server; + +mod support; + +use ra_lsp_server::req::{Runnables, RunnablesParams}; + +use support::project; + + +const LOG: &'static str = ""; + +#[test] +fn test_runnables_no_project() { + let server = project(r" +//- lib.rs +#[test] +fn foo() { +} +"); + server.request::( + RunnablesParams { + text_document: server.doc_id("lib.rs"), + position: None, + }, + r#"[ + { + "args": [ "test", "--", "foo", "--nocapture" ], + "bin": "cargo", + "env": { "RUST_BACKTRACE": "short" }, + "label": "test foo", + "range": { + "end": { "character": 1, "line": 2 }, + "start": { "character": 0, "line": 0 } + } + } + ]"# + ); +} + +#[test] +fn test_runnables_project() { + let server = project(r#" +//- Cargo.toml +[package] +name = "foo" +version = "0.0.0" + +//- src/lib.rs +pub fn foo() {} + +//- tests/spam.rs +#[test] +fn test_eggs() {} +"#); + server.wait_for_feedback("workspace loaded"); + server.request::( + RunnablesParams { + text_document: server.doc_id("tests/spam.rs"), + position: None, + }, + r#"[ + { + "args": [ "test", "--package", "foo", "--test", "spam", "--", "test_eggs", "--nocapture" ], + "bin": "cargo", + "env": { "RUST_BACKTRACE": "short" }, + "label": "test test_eggs", + "range": { + "end": { "character": 17, "line": 1 }, + "start": { "character": 0, "line": 0 } + } + } + ]"# + ); +} + +// #[test] +// fn test_deps() { +// let server = project(r#" +// //- Cargo.toml +// [package] +// name = "foo" +// version = "0.0.0" +// [dependencies] +// regex = "=1.0.4" + +// //- src/lib.rs +// extern crate regex; +// "#); +// server.wait_for_feedback("workspace loaded"); +// server.wait_for_feedback_n("library loaded", 9); +// } diff --git a/crates/ra_lsp_server/tests/heavy_tests/support.rs b/crates/ra_lsp_server/tests/heavy_tests/support.rs new file mode 100644 index 000000000..8fe2aa816 --- /dev/null +++ b/crates/ra_lsp_server/tests/heavy_tests/support.rs @@ -0,0 +1,217 @@ +use std::{ + fs, + cell::{Cell, RefCell}, + path::PathBuf, + time::Duration, + sync::Once, +}; + +use tempdir::TempDir; +use crossbeam_channel::{after, Receiver}; +use flexi_logger::Logger; +use languageserver_types::{ + Url, + TextDocumentIdentifier, + request::{Request, Shutdown}, + notification::DidOpenTextDocument, + DidOpenTextDocumentParams, + TextDocumentItem, +}; +use serde::Serialize; +use serde_json::{Value, from_str, to_string_pretty}; +use gen_lsp_server::{RawMessage, RawRequest, RawNotification}; + +use ra_lsp_server::{main_loop, req, thread_watcher::{ThreadWatcher, Worker}}; + +pub fn project(fixture: &str) -> Server { + static INIT: Once = Once::new(); + INIT.call_once(|| Logger::with_env_or_str(::LOG).start().unwrap()); + + let tmp_dir = TempDir::new("test-project") + .unwrap(); + let mut buf = String::new(); + let mut file_name = None; + let mut paths = vec![]; + macro_rules! flush { + () => { + if let Some(file_name) = file_name { + let path = tmp_dir.path().join(file_name); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path.as_path(), buf.as_bytes()).unwrap(); + paths.push((path, buf.clone())); + } + } + }; + for line in fixture.lines() { + if line.starts_with("//-") { + flush!(); + buf.clear(); + file_name = Some(line["//-".len()..].trim()); + continue; + } + buf.push_str(line); + buf.push('\n'); + } + flush!(); + Server::new(tmp_dir, paths) +} + +pub struct Server { + req_id: Cell, + messages: RefCell>, + dir: TempDir, + worker: Option>, + watcher: Option, +} + +impl Server { + fn new(dir: TempDir, files: Vec<(PathBuf, String)>) -> Server { + let path = dir.path().to_path_buf(); + let (worker, watcher) = Worker::::spawn( + "test server", + 128, + move |mut msg_receiver, mut msg_sender| { + main_loop(true, path, &mut msg_receiver, &mut msg_sender) + .unwrap() + } + ); + let res = Server { + req_id: Cell::new(1), + dir, + messages: Default::default(), + worker: Some(worker), + watcher: Some(watcher), + }; + + for (path, text) in files { + res.send_notification(RawNotification::new::( + &DidOpenTextDocumentParams { + text_document: TextDocumentItem { + uri: Url::from_file_path(path).unwrap(), + language_id: "rust".to_string(), + version: 0, + text, + } + } + )) + } + res + } + + pub fn doc_id(&self, rel_path: &str) -> TextDocumentIdentifier { + let path = self.dir.path().join(rel_path); + TextDocumentIdentifier { + uri: Url::from_file_path(path).unwrap(), + } + } + + pub fn request( + &self, + params: R::Params, + expected_resp: &str, + ) + where + R: Request, + R::Params: Serialize, + { + let id = self.req_id.get(); + self.req_id.set(id + 1); + let expected_resp: Value = from_str(expected_resp).unwrap(); + let actual = self.send_request::(id, params); + assert_eq!( + expected_resp, actual, + "Expected:\n{}\n\ + Actual:\n{}\n", + to_string_pretty(&expected_resp).unwrap(), + to_string_pretty(&actual).unwrap(), + ); + } + + fn send_request(&self, id: u64, params: R::Params) -> Value + where + R: Request, + R::Params: Serialize, + { + let r = RawRequest::new::(id, ¶ms); + self.send_request_(r) + } + fn send_request_(&self, r: RawRequest) -> Value + { + let id = r.id; + self.worker.as_ref() + .unwrap() + .send(RawMessage::Request(r)); + while let Some(msg) = self.recv() { + match msg { + RawMessage::Request(req) => panic!("unexpected request: {:?}", req), + RawMessage::Notification(_) => (), + RawMessage::Response(res) => { + assert_eq!(res.id, id); + if let Some(err) = res.error { + panic!("error response: {:#?}", err); + } + return res.result.unwrap(); + } + } + } + panic!("no response"); + } + pub fn wait_for_feedback(&self, feedback: &str) { + self.wait_for_feedback_n(feedback, 1) + } + pub fn wait_for_feedback_n(&self, feedback: &str, n: usize) { + let f = |msg: &RawMessage| match msg { + RawMessage::Notification(n) if n.method == "internalFeedback" => { + return n.clone().cast::() + .unwrap() == feedback + } + _ => false, + }; + let mut total = 0; + for msg in self.messages.borrow().iter() { + if f(msg) { + total += 1 + } + } + while total < n { + let msg = self.recv().expect("no response"); + if f(&msg) { + total += 1; + } + } + } + fn recv(&self) -> Option { + recv_timeout(&self.worker.as_ref().unwrap().out) + .map(|msg| { + self.messages.borrow_mut().push(msg.clone()); + msg + }) + } + fn send_notification(&self, not: RawNotification) { + self.worker.as_ref() + .unwrap() + .send(RawMessage::Notification(not)); + } +} + +impl Drop for Server { + fn drop(&mut self) { + self.send_request::(666, ()); + let receiver = self.worker.take().unwrap().stop(); + while let Some(msg) = recv_timeout(&receiver) { + drop(msg); + } + self.watcher.take() + .unwrap() + .stop() + .unwrap(); + } +} + +fn recv_timeout(receiver: &Receiver) -> Option { + let timeout = Duration::from_secs(5); + select! { + recv(receiver, msg) => msg, + recv(after(timeout)) => panic!("timed out"), + } +} -- cgit v1.2.3