aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_lsp_server
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_lsp_server')
-rw-r--r--crates/ra_lsp_server/Cargo.toml53
-rw-r--r--crates/ra_lsp_server/build.rs15
-rw-r--r--crates/ra_lsp_server/src/args.rs242
-rw-r--r--crates/ra_lsp_server/src/caps.rs62
-rw-r--r--crates/ra_lsp_server/src/cargo_target_spec.rs113
-rw-r--r--crates/ra_lsp_server/src/cli.rs75
-rw-r--r--crates/ra_lsp_server/src/cli/analysis_bench.rs158
-rw-r--r--crates/ra_lsp_server/src/cli/analysis_stats.rs259
-rw-r--r--crates/ra_lsp_server/src/cli/load_cargo.rs153
-rw-r--r--crates/ra_lsp_server/src/cli/progress_report.rs120
-rw-r--r--crates/ra_lsp_server/src/config.rs105
-rw-r--r--crates/ra_lsp_server/src/conv.rs629
-rw-r--r--crates/ra_lsp_server/src/diagnostics.rs85
-rw-r--r--crates/ra_lsp_server/src/lib.rs51
-rw-r--r--crates/ra_lsp_server/src/main.rs96
-rw-r--r--crates/ra_lsp_server/src/main_loop.rs889
-rw-r--r--crates/ra_lsp_server/src/main_loop/handlers.rs1068
-rw-r--r--crates/ra_lsp_server/src/main_loop/pending_requests.rs75
-rw-r--r--crates/ra_lsp_server/src/main_loop/subscriptions.rs21
-rw-r--r--crates/ra_lsp_server/src/markdown.rs75
-rw-r--r--crates/ra_lsp_server/src/req.rs221
-rw-r--r--crates/ra_lsp_server/src/vfs_glob.rs94
-rw-r--r--crates/ra_lsp_server/src/world.rs314
-rw-r--r--crates/ra_lsp_server/tests/heavy_tests/main.rs582
-rw-r--r--crates/ra_lsp_server/tests/heavy_tests/support.rs254
25 files changed, 0 insertions, 5809 deletions
diff --git a/crates/ra_lsp_server/Cargo.toml b/crates/ra_lsp_server/Cargo.toml
deleted file mode 100644
index da523ba8a..000000000
--- a/crates/ra_lsp_server/Cargo.toml
+++ /dev/null
@@ -1,53 +0,0 @@
1[package]
2edition = "2018"
3name = "ra_lsp_server"
4version = "0.1.0"
5authors = ["rust-analyzer developers"]
6
7[lib]
8doctest = false
9
10[dependencies]
11anyhow = "1.0"
12crossbeam-channel = "0.4"
13either = "1.5"
14env_logger = { version = "0.7.1", default-features = false }
15globset = "0.4.4"
16itertools = "0.8.0"
17jod-thread = "0.1.0"
18log = "0.4.3"
19lsp-types = { version = "0.70.0", features = ["proposed"] }
20parking_lot = "0.10.0"
21pico-args = "0.3.0"
22rand = { version = "0.7.0", features = ["small_rng"] }
23relative-path = "1.0.0"
24rustc-hash = "1.0"
25serde = { version = "1.0.83", features = ["derive"] }
26serde_json = "1.0.34"
27threadpool = "1.7.1"
28
29lsp-server = "0.3.0"
30ra_cargo_watch = { path = "../ra_cargo_watch" }
31ra_ide = { path = "../ra_ide" }
32ra_prof = { path = "../ra_prof" }
33ra_project_model = { path = "../ra_project_model" }
34ra_syntax = { path = "../ra_syntax" }
35ra_text_edit = { path = "../ra_text_edit" }
36ra_vfs = "0.5.0"
37
38# This should only be used in CLI
39ra_db = { path = "../ra_db" }
40hir = { path = "../ra_hir", package = "ra_hir" }
41hir_def = { path = "../ra_hir_def", package = "ra_hir_def" }
42hir_ty = { path = "../ra_hir_ty", package = "ra_hir_ty" }
43
44
45[target.'cfg(windows)'.dependencies]
46winapi = "0.3"
47
48[dev-dependencies]
49tempfile = "3"
50test_utils = { path = "../test_utils" }
51
52[features]
53jemalloc = [ "ra_prof/jemalloc" ]
diff --git a/crates/ra_lsp_server/build.rs b/crates/ra_lsp_server/build.rs
deleted file mode 100644
index 05f9772c0..000000000
--- a/crates/ra_lsp_server/build.rs
+++ /dev/null
@@ -1,15 +0,0 @@
1//! Just embed git-hash to `--version`
2
3use std::process::Command;
4
5fn main() {
6 let rev = rev().unwrap_or_else(|| "???????".to_string());
7 println!("cargo:rustc-env=REV={}", rev)
8}
9
10fn rev() -> Option<String> {
11 let output = Command::new("git").args(&["rev-parse", "HEAD"]).output().ok()?;
12 let stdout = String::from_utf8(output.stdout).ok()?;
13 let short_hash = stdout.get(0..7)?;
14 Some(short_hash.to_owned())
15}
diff --git a/crates/ra_lsp_server/src/args.rs b/crates/ra_lsp_server/src/args.rs
deleted file mode 100644
index 3890fe13a..000000000
--- a/crates/ra_lsp_server/src/args.rs
+++ /dev/null
@@ -1,242 +0,0 @@
1//! Command like parsing for rust-analyzer.
2//!
3//! If run started args, we run the LSP server loop. With a subcommand, we do a
4//! one-time batch processing.
5
6use anyhow::{bail, Result};
7use pico_args::Arguments;
8use ra_lsp_server::cli::{BenchWhat, Position, Verbosity};
9
10use std::{fmt::Write, path::PathBuf};
11
12pub(crate) struct Args {
13 pub(crate) verbosity: Verbosity,
14 pub(crate) command: Command,
15}
16
17pub(crate) enum Command {
18 Parse {
19 no_dump: bool,
20 },
21 Symbols,
22 Highlight {
23 rainbow: bool,
24 },
25 Stats {
26 randomize: bool,
27 memory_usage: bool,
28 only: Option<String>,
29 with_deps: bool,
30 path: PathBuf,
31 },
32 Bench {
33 path: PathBuf,
34 what: BenchWhat,
35 },
36 RunServer,
37 Version,
38}
39
40impl Args {
41 pub(crate) fn parse() -> Result<Result<Args, HelpPrinted>> {
42 let mut matches = Arguments::from_env();
43
44 if matches.contains("--version") {
45 matches.finish().or_else(handle_extra_flags)?;
46 return Ok(Ok(Args { verbosity: Verbosity::Normal, command: Command::Version }));
47 }
48
49 let verbosity = match (
50 matches.contains(["-vv", "--spammy"]),
51 matches.contains(["-v", "--verbose"]),
52 matches.contains(["-q", "--quiet"]),
53 ) {
54 (true, _, true) => bail!("Invalid flags: -q conflicts with -vv"),
55 (true, _, false) => Verbosity::Spammy,
56 (false, false, false) => Verbosity::Normal,
57 (false, false, true) => Verbosity::Quiet,
58 (false, true, false) => Verbosity::Verbose,
59 (false, true, true) => bail!("Invalid flags: -q conflicts with -v"),
60 };
61
62 let subcommand = match matches.subcommand()? {
63 Some(it) => it,
64 None => {
65 matches.finish().or_else(handle_extra_flags)?;
66 return Ok(Ok(Args { verbosity, command: Command::RunServer }));
67 }
68 };
69 let command = match subcommand.as_str() {
70 "parse" => {
71 if matches.contains(["-h", "--help"]) {
72 eprintln!(
73 "\
74ra-cli-parse
75
76USAGE:
77 ra_lsp_server parse [FLAGS]
78
79FLAGS:
80 -h, --help Prints help inforamtion
81 --no-dump"
82 );
83 return Ok(Err(HelpPrinted));
84 }
85
86 let no_dump = matches.contains("--no-dump");
87 matches.finish().or_else(handle_extra_flags)?;
88 Command::Parse { no_dump }
89 }
90 "symbols" => {
91 if matches.contains(["-h", "--help"]) {
92 eprintln!(
93 "\
94ra-cli-symbols
95
96USAGE:
97 ra_lsp_server highlight [FLAGS]
98
99FLAGS:
100 -h, --help Prints help inforamtion"
101 );
102 return Ok(Err(HelpPrinted));
103 }
104
105 matches.finish().or_else(handle_extra_flags)?;
106
107 Command::Symbols
108 }
109 "highlight" => {
110 if matches.contains(["-h", "--help"]) {
111 eprintln!(
112 "\
113ra-cli-highlight
114
115USAGE:
116 ra_lsp_server highlight [FLAGS]
117
118FLAGS:
119 -h, --help Prints help information
120 -r, --rainbow"
121 );
122 return Ok(Err(HelpPrinted));
123 }
124
125 let rainbow = matches.contains(["-r", "--rainbow"]);
126 matches.finish().or_else(handle_extra_flags)?;
127 Command::Highlight { rainbow }
128 }
129 "analysis-stats" => {
130 if matches.contains(["-h", "--help"]) {
131 eprintln!(
132 "\
133ra-cli-analysis-stats
134
135USAGE:
136 ra_lsp_server analysis-stats [FLAGS] [OPTIONS] [PATH]
137
138FLAGS:
139 -h, --help Prints help information
140 --memory-usage
141 -v, --verbose
142 -q, --quiet
143
144OPTIONS:
145 -o <ONLY>
146
147ARGS:
148 <PATH>"
149 );
150 return Ok(Err(HelpPrinted));
151 }
152
153 let randomize = matches.contains("--randomize");
154 let memory_usage = matches.contains("--memory-usage");
155 let only: Option<String> = matches.opt_value_from_str(["-o", "--only"])?;
156 let with_deps: bool = matches.contains("--with-deps");
157 let path = {
158 let mut trailing = matches.free()?;
159 if trailing.len() != 1 {
160 bail!("Invalid flags");
161 }
162 trailing.pop().unwrap().into()
163 };
164
165 Command::Stats { randomize, memory_usage, only, with_deps, path }
166 }
167 "analysis-bench" => {
168 if matches.contains(["-h", "--help"]) {
169 eprintln!(
170 "\
171ra_lsp_server-analysis-bench
172
173USAGE:
174 ra_lsp_server analysis-bench [FLAGS] [OPTIONS] [PATH]
175
176FLAGS:
177 -h, --help Prints help information
178 -v, --verbose
179
180OPTIONS:
181 --complete <PATH:LINE:COLUMN> Compute completions at this location
182 --highlight <PATH> Hightlight this file
183
184ARGS:
185 <PATH> Project to analyse"
186 );
187 return Ok(Err(HelpPrinted));
188 }
189
190 let path: PathBuf = matches.opt_value_from_str("--path")?.unwrap_or_default();
191 let highlight_path: Option<String> = matches.opt_value_from_str("--highlight")?;
192 let complete_path: Option<Position> = matches.opt_value_from_str("--complete")?;
193 let goto_def_path: Option<Position> = matches.opt_value_from_str("--goto-def")?;
194 let what = match (highlight_path, complete_path, goto_def_path) {
195 (Some(path), None, None) => BenchWhat::Highlight { path: path.into() },
196 (None, Some(position), None) => BenchWhat::Complete(position),
197 (None, None, Some(position)) => BenchWhat::GotoDef(position),
198 _ => panic!(
199 "exactly one of `--highlight`, `--complete` or `--goto-def` must be set"
200 ),
201 };
202 Command::Bench { path, what }
203 }
204 _ => {
205 eprintln!(
206 "\
207ra-cli
208
209USAGE:
210 ra_lsp_server <SUBCOMMAND>
211
212FLAGS:
213 -h, --help Prints help information
214
215SUBCOMMANDS:
216 analysis-bench
217 analysis-stats
218 highlight
219 parse
220 symbols"
221 );
222 return Ok(Err(HelpPrinted));
223 }
224 };
225 Ok(Ok(Args { verbosity, command }))
226 }
227}
228
229pub(crate) struct HelpPrinted;
230
231fn handle_extra_flags(e: pico_args::Error) -> Result<()> {
232 if let pico_args::Error::UnusedArgsLeft(flags) = e {
233 let mut invalid_flags = String::new();
234 for flag in flags {
235 write!(&mut invalid_flags, "{}, ", flag)?;
236 }
237 let (invalid_flags, _) = invalid_flags.split_at(invalid_flags.len() - 2);
238 bail!("Invalid flags: {}", invalid_flags);
239 } else {
240 bail!(e);
241 }
242}
diff --git a/crates/ra_lsp_server/src/caps.rs b/crates/ra_lsp_server/src/caps.rs
deleted file mode 100644
index c4711076c..000000000
--- a/crates/ra_lsp_server/src/caps.rs
+++ /dev/null
@@ -1,62 +0,0 @@
1//! Advertizes the capabilities of the LSP Server.
2
3use lsp_types::{
4 CallHierarchyServerCapability, CodeActionProviderCapability, CodeLensOptions,
5 CompletionOptions, DocumentOnTypeFormattingOptions, FoldingRangeProviderCapability,
6 ImplementationProviderCapability, RenameOptions, RenameProviderCapability, SaveOptions,
7 SelectionRangeProviderCapability, ServerCapabilities, SignatureHelpOptions,
8 TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
9 TypeDefinitionProviderCapability, WorkDoneProgressOptions,
10};
11
12pub fn server_capabilities() -> ServerCapabilities {
13 ServerCapabilities {
14 text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
15 open_close: Some(true),
16 change: Some(TextDocumentSyncKind::Full),
17 will_save: None,
18 will_save_wait_until: None,
19 save: Some(SaveOptions::default()),
20 })),
21 hover_provider: Some(true),
22 completion_provider: Some(CompletionOptions {
23 resolve_provider: None,
24 trigger_characters: Some(vec![":".to_string(), ".".to_string()]),
25 work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None },
26 }),
27 signature_help_provider: Some(SignatureHelpOptions {
28 trigger_characters: Some(vec!["(".to_string(), ",".to_string()]),
29 retrigger_characters: None,
30 work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None },
31 }),
32 declaration_provider: None,
33 definition_provider: Some(true),
34 type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
35 implementation_provider: Some(ImplementationProviderCapability::Simple(true)),
36 references_provider: Some(true),
37 document_highlight_provider: Some(true),
38 document_symbol_provider: Some(true),
39 workspace_symbol_provider: Some(true),
40 code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
41 code_lens_provider: Some(CodeLensOptions { resolve_provider: Some(true) }),
42 document_formatting_provider: Some(true),
43 document_range_formatting_provider: None,
44 document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions {
45 first_trigger_character: "=".to_string(),
46 more_trigger_character: Some(vec![".".to_string(), ">".to_string()]),
47 }),
48 selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)),
49 semantic_highlighting: None,
50 folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
51 rename_provider: Some(RenameProviderCapability::Options(RenameOptions {
52 prepare_provider: Some(true),
53 work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None },
54 })),
55 document_link_provider: None,
56 color_provider: None,
57 execute_command_provider: None,
58 workspace: None,
59 call_hierarchy_provider: Some(CallHierarchyServerCapability::Simple(true)),
60 experimental: Default::default(),
61 }
62}
diff --git a/crates/ra_lsp_server/src/cargo_target_spec.rs b/crates/ra_lsp_server/src/cargo_target_spec.rs
deleted file mode 100644
index 5fd1e7b6b..000000000
--- a/crates/ra_lsp_server/src/cargo_target_spec.rs
+++ /dev/null
@@ -1,113 +0,0 @@
1//! FIXME: write short doc here
2
3use ra_ide::{FileId, RunnableKind, TestId};
4use ra_project_model::{self, ProjectWorkspace, TargetKind};
5
6use crate::{world::WorldSnapshot, Result};
7
8pub(crate) fn runnable_args(
9 world: &WorldSnapshot,
10 file_id: FileId,
11 kind: &RunnableKind,
12) -> Result<Vec<String>> {
13 let spec = CargoTargetSpec::for_file(world, file_id)?;
14 let mut res = Vec::new();
15 match kind {
16 RunnableKind::Test { test_id } => {
17 res.push("test".to_string());
18 if let Some(spec) = spec {
19 spec.push_to(&mut res);
20 }
21 res.push("--".to_string());
22 res.push(test_id.to_string());
23 if let TestId::Path(_) = test_id {
24 res.push("--exact".to_string());
25 }
26 res.push("--nocapture".to_string());
27 }
28 RunnableKind::TestMod { path } => {
29 res.push("test".to_string());
30 if let Some(spec) = spec {
31 spec.push_to(&mut res);
32 }
33 res.push("--".to_string());
34 res.push(path.to_string());
35 res.push("--nocapture".to_string());
36 }
37 RunnableKind::Bench { test_id } => {
38 res.push("bench".to_string());
39 if let Some(spec) = spec {
40 spec.push_to(&mut res);
41 }
42 res.push("--".to_string());
43 res.push(test_id.to_string());
44 if let TestId::Path(_) = test_id {
45 res.push("--exact".to_string());
46 }
47 res.push("--nocapture".to_string());
48 }
49 RunnableKind::Bin => {
50 res.push("run".to_string());
51 if let Some(spec) = spec {
52 spec.push_to(&mut res);
53 }
54 }
55 }
56 Ok(res)
57}
58
59pub struct CargoTargetSpec {
60 pub package: String,
61 pub target: String,
62 pub target_kind: TargetKind,
63}
64
65impl CargoTargetSpec {
66 pub fn for_file(world: &WorldSnapshot, file_id: FileId) -> Result<Option<CargoTargetSpec>> {
67 let &crate_id = match world.analysis().crate_for(file_id)?.first() {
68 Some(crate_id) => crate_id,
69 None => return Ok(None),
70 };
71 let file_id = world.analysis().crate_root(crate_id)?;
72 let path = world.file_id_to_path(file_id);
73 let res = world.workspaces.iter().find_map(|ws| match ws {
74 ProjectWorkspace::Cargo { cargo, .. } => {
75 let tgt = cargo.target_by_root(&path)?;
76 Some(CargoTargetSpec {
77 package: tgt.package(&cargo).name(&cargo).to_string(),
78 target: tgt.name(&cargo).to_string(),
79 target_kind: tgt.kind(&cargo),
80 })
81 }
82 ProjectWorkspace::Json { .. } => None,
83 });
84 Ok(res)
85 }
86
87 pub fn push_to(self, buf: &mut Vec<String>) {
88 buf.push("--package".to_string());
89 buf.push(self.package);
90 match self.target_kind {
91 TargetKind::Bin => {
92 buf.push("--bin".to_string());
93 buf.push(self.target);
94 }
95 TargetKind::Test => {
96 buf.push("--test".to_string());
97 buf.push(self.target);
98 }
99 TargetKind::Bench => {
100 buf.push("--bench".to_string());
101 buf.push(self.target);
102 }
103 TargetKind::Example => {
104 buf.push("--example".to_string());
105 buf.push(self.target);
106 }
107 TargetKind::Lib => {
108 buf.push("--lib".to_string());
109 }
110 TargetKind::Other => (),
111 }
112 }
113}
diff --git a/crates/ra_lsp_server/src/cli.rs b/crates/ra_lsp_server/src/cli.rs
deleted file mode 100644
index 3c7b8e250..000000000
--- a/crates/ra_lsp_server/src/cli.rs
+++ /dev/null
@@ -1,75 +0,0 @@
1//! FIXME: write short doc here
2
3mod load_cargo;
4mod analysis_stats;
5mod analysis_bench;
6mod progress_report;
7
8use std::io::Read;
9
10use anyhow::Result;
11use ra_ide::{file_structure, Analysis};
12use ra_prof::profile;
13use ra_syntax::{AstNode, SourceFile};
14
15#[derive(Clone, Copy)]
16pub enum Verbosity {
17 Spammy,
18 Verbose,
19 Normal,
20 Quiet,
21}
22
23impl Verbosity {
24 pub fn is_verbose(self) -> bool {
25 match self {
26 Verbosity::Verbose | Verbosity::Spammy => true,
27 _ => false,
28 }
29 }
30 pub fn is_spammy(self) -> bool {
31 match self {
32 Verbosity::Spammy => true,
33 _ => false,
34 }
35 }
36}
37
38pub fn parse(no_dump: bool) -> Result<()> {
39 let _p = profile("parsing");
40 let file = file()?;
41 if !no_dump {
42 println!("{:#?}", file.syntax());
43 }
44 std::mem::forget(file);
45 Ok(())
46}
47
48pub fn symbols() -> Result<()> {
49 let file = file()?;
50 for s in file_structure(&file) {
51 println!("{:?}", s);
52 }
53 Ok(())
54}
55
56pub fn highlight(rainbow: bool) -> Result<()> {
57 let (analysis, file_id) = Analysis::from_single_file(read_stdin()?);
58 let html = analysis.highlight_as_html(file_id, rainbow).unwrap();
59 println!("{}", html);
60 Ok(())
61}
62
63pub use analysis_bench::{analysis_bench, BenchWhat, Position};
64pub use analysis_stats::analysis_stats;
65
66fn file() -> Result<SourceFile> {
67 let text = read_stdin()?;
68 Ok(SourceFile::parse(&text).tree())
69}
70
71fn read_stdin() -> Result<String> {
72 let mut buff = String::new();
73 std::io::stdin().read_to_string(&mut buff)?;
74 Ok(buff)
75}
diff --git a/crates/ra_lsp_server/src/cli/analysis_bench.rs b/crates/ra_lsp_server/src/cli/analysis_bench.rs
deleted file mode 100644
index e00f81073..000000000
--- a/crates/ra_lsp_server/src/cli/analysis_bench.rs
+++ /dev/null
@@ -1,158 +0,0 @@
1//! FIXME: write short doc here
2
3use std::{
4 path::{Path, PathBuf},
5 str::FromStr,
6 sync::Arc,
7 time::Instant,
8};
9
10use anyhow::{format_err, Result};
11use ra_db::{
12 salsa::{Database, Durability},
13 FileId, SourceDatabaseExt,
14};
15use ra_ide::{Analysis, AnalysisChange, AnalysisHost, FilePosition, LineCol};
16
17use crate::cli::{load_cargo::load_cargo, Verbosity};
18
19pub enum BenchWhat {
20 Highlight { path: PathBuf },
21 Complete(Position),
22 GotoDef(Position),
23}
24
25pub struct Position {
26 pub path: PathBuf,
27 pub line: u32,
28 pub column: u32,
29}
30
31impl FromStr for Position {
32 type Err = anyhow::Error;
33 fn from_str(s: &str) -> Result<Self> {
34 let (path_line, column) = rsplit_at_char(s, ':')?;
35 let (path, line) = rsplit_at_char(path_line, ':')?;
36 Ok(Position { path: path.into(), line: line.parse()?, column: column.parse()? })
37 }
38}
39
40fn rsplit_at_char(s: &str, c: char) -> Result<(&str, &str)> {
41 let idx = s.rfind(c).ok_or_else(|| format_err!("no `{}` in {}", c, s))?;
42 Ok((&s[..idx], &s[idx + 1..]))
43}
44
45pub fn analysis_bench(verbosity: Verbosity, path: &Path, what: BenchWhat) -> Result<()> {
46 ra_prof::init();
47
48 let start = Instant::now();
49 eprint!("loading: ");
50 let (mut host, roots) = load_cargo(path)?;
51 let db = host.raw_database();
52 eprintln!("{:?}\n", start.elapsed());
53
54 let file_id = {
55 let path = match &what {
56 BenchWhat::Highlight { path } => path,
57 BenchWhat::Complete(pos) | BenchWhat::GotoDef(pos) => &pos.path,
58 };
59 let path = std::env::current_dir()?.join(path).canonicalize()?;
60 roots
61 .iter()
62 .find_map(|(source_root_id, project_root)| {
63 if project_root.is_member() {
64 for file_id in db.source_root(*source_root_id).walk() {
65 let rel_path = db.file_relative_path(file_id);
66 let abs_path = rel_path.to_path(project_root.path());
67 if abs_path == path {
68 return Some(file_id);
69 }
70 }
71 }
72 None
73 })
74 .ok_or_else(|| format_err!("Can't find {}", path.display()))?
75 };
76
77 match &what {
78 BenchWhat::Highlight { .. } => {
79 let res = do_work(&mut host, file_id, |analysis| {
80 analysis.diagnostics(file_id).unwrap();
81 analysis.highlight_as_html(file_id, false).unwrap()
82 });
83 if verbosity.is_verbose() {
84 println!("\n{}", res);
85 }
86 }
87 BenchWhat::Complete(pos) | BenchWhat::GotoDef(pos) => {
88 let is_completion = match what {
89 BenchWhat::Complete(..) => true,
90 _ => false,
91 };
92
93 let offset = host
94 .analysis()
95 .file_line_index(file_id)?
96 .offset(LineCol { line: pos.line - 1, col_utf16: pos.column });
97 let file_postion = FilePosition { file_id, offset };
98
99 if is_completion {
100 let res =
101 do_work(&mut host, file_id, |analysis| analysis.completions(file_postion));
102 if verbosity.is_verbose() {
103 println!("\n{:#?}", res);
104 }
105 } else {
106 let res =
107 do_work(&mut host, file_id, |analysis| analysis.goto_definition(file_postion));
108 if verbosity.is_verbose() {
109 println!("\n{:#?}", res);
110 }
111 }
112 }
113 }
114 Ok(())
115}
116
117fn do_work<F: Fn(&Analysis) -> T, T>(host: &mut AnalysisHost, file_id: FileId, work: F) -> T {
118 {
119 let start = Instant::now();
120 eprint!("from scratch: ");
121 work(&host.analysis());
122 eprintln!("{:?}", start.elapsed());
123 }
124 {
125 let start = Instant::now();
126 eprint!("no change: ");
127 work(&host.analysis());
128 eprintln!("{:?}", start.elapsed());
129 }
130 {
131 let start = Instant::now();
132 eprint!("trivial change: ");
133 host.raw_database_mut().salsa_runtime_mut().synthetic_write(Durability::LOW);
134 work(&host.analysis());
135 eprintln!("{:?}", start.elapsed());
136 }
137 {
138 let start = Instant::now();
139 eprint!("comment change: ");
140 {
141 let mut text = host.analysis().file_text(file_id).unwrap().to_string();
142 text.push_str("\n/* Hello world */\n");
143 let mut change = AnalysisChange::new();
144 change.change_file(file_id, Arc::new(text));
145 host.apply_change(change);
146 }
147 work(&host.analysis());
148 eprintln!("{:?}", start.elapsed());
149 }
150 {
151 let start = Instant::now();
152 eprint!("const change: ");
153 host.raw_database_mut().salsa_runtime_mut().synthetic_write(Durability::HIGH);
154 let res = work(&host.analysis());
155 eprintln!("{:?}", start.elapsed());
156 res
157 }
158}
diff --git a/crates/ra_lsp_server/src/cli/analysis_stats.rs b/crates/ra_lsp_server/src/cli/analysis_stats.rs
deleted file mode 100644
index c27fabe3c..000000000
--- a/crates/ra_lsp_server/src/cli/analysis_stats.rs
+++ /dev/null
@@ -1,259 +0,0 @@
1//! FIXME: write short doc here
2
3use std::{collections::HashSet, fmt::Write, path::Path, time::Instant};
4
5use hir::{
6 db::{DefDatabase, HirDatabase},
7 AssocItem, Crate, HasSource, HirDisplay, ModuleDef,
8};
9use hir_def::FunctionId;
10use hir_ty::{Ty, TypeWalk};
11use itertools::Itertools;
12use ra_db::SourceDatabaseExt;
13use ra_syntax::AstNode;
14use rand::{seq::SliceRandom, thread_rng};
15
16use crate::cli::{load_cargo::load_cargo, progress_report::ProgressReport, Result, Verbosity};
17
18pub fn analysis_stats(
19 verbosity: Verbosity,
20 memory_usage: bool,
21 path: &Path,
22 only: Option<&str>,
23 with_deps: bool,
24 randomize: bool,
25) -> Result<()> {
26 let db_load_time = Instant::now();
27 let (mut host, roots) = load_cargo(path)?;
28 let db = host.raw_database();
29 println!("Database loaded, {} roots, {:?}", roots.len(), db_load_time.elapsed());
30 let analysis_time = Instant::now();
31 let mut num_crates = 0;
32 let mut visited_modules = HashSet::new();
33 let mut visit_queue = Vec::new();
34
35 let members =
36 roots
37 .into_iter()
38 .filter_map(|(source_root_id, project_root)| {
39 if with_deps || project_root.is_member() {
40 Some(source_root_id)
41 } else {
42 None
43 }
44 })
45 .collect::<HashSet<_>>();
46
47 let mut krates = Crate::all(db);
48 if randomize {
49 krates.shuffle(&mut thread_rng());
50 }
51 for krate in krates {
52 let module = krate.root_module(db).expect("crate without root module");
53 let file_id = module.definition_source(db).file_id;
54 if members.contains(&db.file_source_root(file_id.original_file(db))) {
55 num_crates += 1;
56 visit_queue.push(module);
57 }
58 }
59
60 if randomize {
61 visit_queue.shuffle(&mut thread_rng());
62 }
63
64 println!("Crates in this dir: {}", num_crates);
65 let mut num_decls = 0;
66 let mut funcs = Vec::new();
67 while let Some(module) = visit_queue.pop() {
68 if visited_modules.insert(module) {
69 visit_queue.extend(module.children(db));
70
71 for decl in module.declarations(db) {
72 num_decls += 1;
73 if let ModuleDef::Function(f) = decl {
74 funcs.push(f);
75 }
76 }
77
78 for impl_block in module.impl_blocks(db) {
79 for item in impl_block.items(db) {
80 num_decls += 1;
81 if let AssocItem::Function(f) = item {
82 funcs.push(f);
83 }
84 }
85 }
86 }
87 }
88 println!("Total modules found: {}", visited_modules.len());
89 println!("Total declarations: {}", num_decls);
90 println!("Total functions: {}", funcs.len());
91 println!("Item Collection: {:?}, {}", analysis_time.elapsed(), ra_prof::memory_usage());
92
93 if randomize {
94 funcs.shuffle(&mut thread_rng());
95 }
96
97 let inference_time = Instant::now();
98 let mut bar = match verbosity {
99 Verbosity::Quiet | Verbosity::Spammy => ProgressReport::hidden(),
100 _ => ProgressReport::new(funcs.len() as u64),
101 };
102
103 bar.tick();
104 let mut num_exprs = 0;
105 let mut num_exprs_unknown = 0;
106 let mut num_exprs_partially_unknown = 0;
107 let mut num_type_mismatches = 0;
108 for f in funcs {
109 let name = f.name(db);
110 let full_name = f
111 .module(db)
112 .path_to_root(db)
113 .into_iter()
114 .rev()
115 .filter_map(|it| it.name(db))
116 .chain(Some(f.name(db)))
117 .join("::");
118 if let Some(only_name) = only {
119 if name.to_string() != only_name && full_name != only_name {
120 continue;
121 }
122 }
123 let mut msg = format!("processing: {}", full_name);
124 if verbosity.is_verbose() {
125 let src = f.source(db);
126 let original_file = src.file_id.original_file(db);
127 let path = db.file_relative_path(original_file);
128 let syntax_range = src.value.syntax().text_range();
129 write!(msg, " ({:?} {})", path, syntax_range).unwrap();
130 }
131 if verbosity.is_spammy() {
132 bar.println(format!("{}", msg));
133 }
134 bar.set_message(&msg);
135 let f_id = FunctionId::from(f);
136 let body = db.body(f_id.into());
137 let inference_result = db.infer(f_id.into());
138 let (previous_exprs, previous_unknown, previous_partially_unknown) =
139 (num_exprs, num_exprs_unknown, num_exprs_partially_unknown);
140 for (expr_id, _) in body.exprs.iter() {
141 let ty = &inference_result[expr_id];
142 num_exprs += 1;
143 if let Ty::Unknown = ty {
144 num_exprs_unknown += 1;
145 } else {
146 let mut is_partially_unknown = false;
147 ty.walk(&mut |ty| {
148 if let Ty::Unknown = ty {
149 is_partially_unknown = true;
150 }
151 });
152 if is_partially_unknown {
153 num_exprs_partially_unknown += 1;
154 }
155 }
156 if only.is_some() && verbosity.is_spammy() {
157 // in super-verbose mode for just one function, we print every single expression
158 let (_, sm) = db.body_with_source_map(f_id.into());
159 let src = sm.expr_syntax(expr_id);
160 if let Some(src) = src {
161 let original_file = src.file_id.original_file(db);
162 let line_index = host.analysis().file_line_index(original_file).unwrap();
163 let text_range = src.value.either(
164 |it| it.syntax_node_ptr().range(),
165 |it| it.syntax_node_ptr().range(),
166 );
167 let (start, end) = (
168 line_index.line_col(text_range.start()),
169 line_index.line_col(text_range.end()),
170 );
171 bar.println(format!(
172 "{}:{}-{}:{}: {}",
173 start.line + 1,
174 start.col_utf16,
175 end.line + 1,
176 end.col_utf16,
177 ty.display(db)
178 ));
179 } else {
180 bar.println(format!("unknown location: {}", ty.display(db)));
181 }
182 }
183 if let Some(mismatch) = inference_result.type_mismatch_for_expr(expr_id) {
184 num_type_mismatches += 1;
185 if verbosity.is_verbose() {
186 let (_, sm) = db.body_with_source_map(f_id.into());
187 let src = sm.expr_syntax(expr_id);
188 if let Some(src) = src {
189 // FIXME: it might be nice to have a function (on Analysis?) that goes from Source<T> -> (LineCol, LineCol) directly
190 let original_file = src.file_id.original_file(db);
191 let path = db.file_relative_path(original_file);
192 let line_index = host.analysis().file_line_index(original_file).unwrap();
193 let text_range = src.value.either(
194 |it| it.syntax_node_ptr().range(),
195 |it| it.syntax_node_ptr().range(),
196 );
197 let (start, end) = (
198 line_index.line_col(text_range.start()),
199 line_index.line_col(text_range.end()),
200 );
201 bar.println(format!(
202 "{} {}:{}-{}:{}: Expected {}, got {}",
203 path,
204 start.line + 1,
205 start.col_utf16,
206 end.line + 1,
207 end.col_utf16,
208 mismatch.expected.display(db),
209 mismatch.actual.display(db)
210 ));
211 } else {
212 bar.println(format!(
213 "{}: Expected {}, got {}",
214 name,
215 mismatch.expected.display(db),
216 mismatch.actual.display(db)
217 ));
218 }
219 }
220 }
221 }
222 if verbosity.is_spammy() {
223 bar.println(format!(
224 "In {}: {} exprs, {} unknown, {} partial",
225 full_name,
226 num_exprs - previous_exprs,
227 num_exprs_unknown - previous_unknown,
228 num_exprs_partially_unknown - previous_partially_unknown
229 ));
230 }
231 bar.inc(1);
232 }
233 bar.finish_and_clear();
234 println!("Total expressions: {}", num_exprs);
235 println!(
236 "Expressions of unknown type: {} ({}%)",
237 num_exprs_unknown,
238 if num_exprs > 0 { num_exprs_unknown * 100 / num_exprs } else { 100 }
239 );
240 println!(
241 "Expressions of partially unknown type: {} ({}%)",
242 num_exprs_partially_unknown,
243 if num_exprs > 0 { num_exprs_partially_unknown * 100 / num_exprs } else { 100 }
244 );
245 println!("Type mismatches: {}", num_type_mismatches);
246 println!("Inference: {:?}, {}", inference_time.elapsed(), ra_prof::memory_usage());
247 println!("Total: {:?}, {}", analysis_time.elapsed(), ra_prof::memory_usage());
248
249 if memory_usage {
250 for (name, bytes) in host.per_query_memory_usage() {
251 println!("{:>8} {}", bytes, name)
252 }
253 let before = ra_prof::memory_usage();
254 drop(host);
255 println!("leftover: {}", before.allocated - ra_prof::memory_usage().allocated)
256 }
257
258 Ok(())
259}
diff --git a/crates/ra_lsp_server/src/cli/load_cargo.rs b/crates/ra_lsp_server/src/cli/load_cargo.rs
deleted file mode 100644
index bb3e1513b..000000000
--- a/crates/ra_lsp_server/src/cli/load_cargo.rs
+++ /dev/null
@@ -1,153 +0,0 @@
1//! FIXME: write short doc here
2
3use std::{collections::HashSet, path::Path};
4
5use crossbeam_channel::{unbounded, Receiver};
6use ra_db::{CrateGraph, FileId, SourceRootId};
7use ra_ide::{AnalysisChange, AnalysisHost, FeatureFlags};
8use ra_project_model::{get_rustc_cfg_options, PackageRoot, ProjectWorkspace};
9use ra_vfs::{RootEntry, Vfs, VfsChange, VfsTask, Watch};
10use rustc_hash::FxHashMap;
11
12use crate::vfs_glob::RustPackageFilterBuilder;
13
14use anyhow::Result;
15
16fn vfs_file_to_id(f: ra_vfs::VfsFile) -> FileId {
17 FileId(f.0)
18}
19fn vfs_root_to_id(r: ra_vfs::VfsRoot) -> SourceRootId {
20 SourceRootId(r.0)
21}
22
23pub fn load_cargo(root: &Path) -> Result<(AnalysisHost, FxHashMap<SourceRootId, PackageRoot>)> {
24 let root = std::env::current_dir()?.join(root);
25 let ws = ProjectWorkspace::discover(root.as_ref(), &Default::default())?;
26 let project_roots = ws.to_roots();
27 let (sender, receiver) = unbounded();
28 let sender = Box::new(move |t| sender.send(t).unwrap());
29 let (mut vfs, roots) = Vfs::new(
30 project_roots
31 .iter()
32 .map(|pkg_root| {
33 RootEntry::new(
34 pkg_root.path().clone(),
35 RustPackageFilterBuilder::default()
36 .set_member(pkg_root.is_member())
37 .into_vfs_filter(),
38 )
39 })
40 .collect(),
41 sender,
42 Watch(false),
43 );
44
45 // FIXME: cfg options?
46 let default_cfg_options = {
47 let mut opts = get_rustc_cfg_options();
48 opts.insert_atom("test".into());
49 opts.insert_atom("debug_assertion".into());
50 opts
51 };
52
53 let (crate_graph, _crate_names) =
54 ws.to_crate_graph(&default_cfg_options, &mut |path: &Path| {
55 let vfs_file = vfs.load(path);
56 log::debug!("vfs file {:?} -> {:?}", path, vfs_file);
57 vfs_file.map(vfs_file_to_id)
58 });
59 log::debug!("crate graph: {:?}", crate_graph);
60
61 let source_roots = roots
62 .iter()
63 .map(|&vfs_root| {
64 let source_root_id = vfs_root_to_id(vfs_root);
65 let project_root = project_roots
66 .iter()
67 .find(|it| it.path() == &vfs.root2path(vfs_root))
68 .unwrap()
69 .clone();
70 (source_root_id, project_root)
71 })
72 .collect::<FxHashMap<_, _>>();
73 let host = load(&source_roots, crate_graph, &mut vfs, receiver);
74 Ok((host, source_roots))
75}
76
77pub fn load(
78 source_roots: &FxHashMap<SourceRootId, PackageRoot>,
79 crate_graph: CrateGraph,
80 vfs: &mut Vfs,
81 receiver: Receiver<VfsTask>,
82) -> AnalysisHost {
83 let lru_cap = std::env::var("RA_LRU_CAP").ok().and_then(|it| it.parse::<usize>().ok());
84 let mut host = AnalysisHost::new(lru_cap, FeatureFlags::default());
85 let mut analysis_change = AnalysisChange::new();
86 analysis_change.set_crate_graph(crate_graph);
87
88 // wait until Vfs has loaded all roots
89 let mut roots_loaded = HashSet::new();
90 for task in receiver {
91 vfs.handle_task(task);
92 let mut done = false;
93 for change in vfs.commit_changes() {
94 match change {
95 VfsChange::AddRoot { root, files } => {
96 let source_root_id = vfs_root_to_id(root);
97 let is_local = source_roots[&source_root_id].is_member();
98 log::debug!(
99 "loaded source root {:?} with path {:?}",
100 source_root_id,
101 vfs.root2path(root)
102 );
103 analysis_change.add_root(source_root_id, is_local);
104 analysis_change.set_debug_root_path(
105 source_root_id,
106 source_roots[&source_root_id].path().display().to_string(),
107 );
108
109 let mut file_map = FxHashMap::default();
110 for (vfs_file, path, text) in files {
111 let file_id = vfs_file_to_id(vfs_file);
112 analysis_change.add_file(source_root_id, file_id, path.clone(), text);
113 file_map.insert(path, file_id);
114 }
115 roots_loaded.insert(source_root_id);
116 if roots_loaded.len() == vfs.n_roots() {
117 done = true;
118 }
119 }
120 VfsChange::AddFile { root, file, path, text } => {
121 let source_root_id = vfs_root_to_id(root);
122 let file_id = vfs_file_to_id(file);
123 analysis_change.add_file(source_root_id, file_id, path, text);
124 }
125 VfsChange::RemoveFile { .. } | VfsChange::ChangeFile { .. } => {
126 // We just need the first scan, so just ignore these
127 }
128 }
129 }
130 if done {
131 break;
132 }
133 }
134
135 host.apply_change(analysis_change);
136 host
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 use hir::Crate;
144
145 #[test]
146 fn test_loading_rust_analyzer() {
147 let path = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().parent().unwrap();
148 let (host, _roots) = load_cargo(path).unwrap();
149 let n_crates = Crate::all(host.raw_database()).len();
150 // RA has quite a few crates, but the exact count doesn't matter
151 assert!(n_crates > 20);
152 }
153}
diff --git a/crates/ra_lsp_server/src/cli/progress_report.rs b/crates/ra_lsp_server/src/cli/progress_report.rs
deleted file mode 100644
index 31867a1e9..000000000
--- a/crates/ra_lsp_server/src/cli/progress_report.rs
+++ /dev/null
@@ -1,120 +0,0 @@
1//! A simple progress bar
2//!
3//! A single thread non-optimized progress bar
4use std::io::Write;
5
6/// A Simple ASCII Progress Bar
7pub struct ProgressReport {
8 curr: f32,
9 text: String,
10 hidden: bool,
11
12 len: u64,
13 pos: u64,
14 msg: String,
15}
16
17impl ProgressReport {
18 pub fn new(len: u64) -> ProgressReport {
19 ProgressReport {
20 curr: 0.0,
21 text: String::new(),
22 hidden: false,
23 len,
24 pos: 0,
25 msg: String::new(),
26 }
27 }
28
29 pub fn hidden() -> ProgressReport {
30 ProgressReport {
31 curr: 0.0,
32 text: String::new(),
33 hidden: true,
34 len: 0,
35 pos: 0,
36 msg: String::new(),
37 }
38 }
39
40 pub fn set_message(&mut self, msg: &str) {
41 self.msg = msg.to_string();
42 self.tick();
43 }
44
45 pub fn println<I: Into<String>>(&mut self, msg: I) {
46 self.clear();
47 println!("{}", msg.into());
48 self.tick();
49 }
50
51 pub fn inc(&mut self, delta: u64) {
52 self.pos += delta;
53 if self.len == 0 {
54 self.set_value(0.0)
55 } else {
56 self.set_value((self.pos as f32) / (self.len as f32))
57 }
58 self.tick();
59 }
60
61 pub fn finish_and_clear(&mut self) {
62 self.clear();
63 }
64
65 pub fn tick(&mut self) {
66 if self.hidden {
67 return;
68 }
69 let percent = (self.curr * 100.0) as u32;
70 let text = format!("{}/{} {:3>}% {}", self.pos, self.len, percent, self.msg);
71 self.update_text(&text);
72 }
73
74 fn update_text(&mut self, text: &str) {
75 // Get length of common portion
76 let mut common_prefix_length = 0;
77 let common_length = usize::min(self.text.len(), text.len());
78
79 while common_prefix_length < common_length
80 && text.chars().nth(common_prefix_length).unwrap()
81 == self.text.chars().nth(common_prefix_length).unwrap()
82 {
83 common_prefix_length += 1;
84 }
85
86 // Backtrack to the first differing character
87 let mut output = String::new();
88 output += &'\x08'.to_string().repeat(self.text.len() - common_prefix_length);
89 // Output new suffix
90 output += &text[common_prefix_length..text.len()];
91
92 // If the new text is shorter than the old one: delete overlapping characters
93 if let Some(overlap_count) = self.text.len().checked_sub(text.len()) {
94 if overlap_count > 0 {
95 output += &" ".repeat(overlap_count);
96 output += &"\x08".repeat(overlap_count);
97 }
98 }
99
100 let _ = std::io::stdout().write(output.as_bytes());
101 let _ = std::io::stdout().flush();
102 self.text = text.to_string();
103 }
104
105 fn set_value(&mut self, value: f32) {
106 self.curr = f32::max(0.0, f32::min(1.0, value));
107 }
108
109 fn clear(&mut self) {
110 if self.hidden {
111 return;
112 }
113
114 // Fill all last text to space and return the cursor
115 let spaces = " ".repeat(self.text.len());
116 let backspaces = "\x08".repeat(self.text.len());
117 print!("{}{}{}", backspaces, spaces, backspaces);
118 self.text = String::new();
119 }
120}
diff --git a/crates/ra_lsp_server/src/config.rs b/crates/ra_lsp_server/src/config.rs
deleted file mode 100644
index 3314269ec..000000000
--- a/crates/ra_lsp_server/src/config.rs
+++ /dev/null
@@ -1,105 +0,0 @@
1//! Config used by the language server.
2//!
3//! We currently get this config from `initialize` LSP request, which is not the
4//! best way to do it, but was the simplest thing we could implement.
5//!
6//! Of particular interest is the `feature_flags` hash map: while other fields
7//! configure the server itself, feature flags are passed into analysis, and
8//! tweak things like automatic insertion of `()` in completions.
9
10use rustc_hash::FxHashMap;
11
12use ra_project_model::CargoFeatures;
13use serde::{Deserialize, Deserializer};
14
15/// Client provided initialization options
16#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
17#[serde(rename_all = "camelCase", default)]
18pub struct ServerConfig {
19 /// Whether the client supports our custom highlighting publishing decorations.
20 /// This is different to the highlightingOn setting, which is whether the user
21 /// wants our custom highlighting to be used.
22 ///
23 /// Defaults to `false`
24 #[serde(deserialize_with = "nullable_bool_false")]
25 pub publish_decorations: bool,
26
27 pub exclude_globs: Vec<String>,
28 #[serde(deserialize_with = "nullable_bool_false")]
29 pub use_client_watching: bool,
30
31 pub lru_capacity: Option<usize>,
32
33 pub max_inlay_hint_length: Option<usize>,
34
35 pub cargo_watch_enable: bool,
36 pub cargo_watch_args: Vec<String>,
37 pub cargo_watch_command: String,
38 pub cargo_watch_all_targets: bool,
39
40 /// For internal usage to make integrated tests faster.
41 #[serde(deserialize_with = "nullable_bool_true")]
42 pub with_sysroot: bool,
43
44 /// Fine grained feature flags to disable specific features.
45 pub feature_flags: FxHashMap<String, bool>,
46
47 pub rustfmt_args: Vec<String>,
48
49 /// Cargo feature configurations.
50 pub cargo_features: CargoFeatures,
51}
52
53impl Default for ServerConfig {
54 fn default() -> ServerConfig {
55 ServerConfig {
56 publish_decorations: false,
57 exclude_globs: Vec::new(),
58 use_client_watching: false,
59 lru_capacity: None,
60 max_inlay_hint_length: None,
61 cargo_watch_enable: true,
62 cargo_watch_args: Vec::new(),
63 cargo_watch_command: "check".to_string(),
64 cargo_watch_all_targets: true,
65 with_sysroot: true,
66 feature_flags: FxHashMap::default(),
67 cargo_features: Default::default(),
68 rustfmt_args: Vec::new(),
69 }
70 }
71}
72
73/// Deserializes a null value to a bool false by default
74fn nullable_bool_false<'de, D>(deserializer: D) -> Result<bool, D::Error>
75where
76 D: Deserializer<'de>,
77{
78 let opt = Option::deserialize(deserializer)?;
79 Ok(opt.unwrap_or(false))
80}
81
82/// Deserializes a null value to a bool true by default
83fn nullable_bool_true<'de, D>(deserializer: D) -> Result<bool, D::Error>
84where
85 D: Deserializer<'de>,
86{
87 let opt = Option::deserialize(deserializer)?;
88 Ok(opt.unwrap_or(true))
89}
90
91#[cfg(test)]
92mod test {
93 use super::*;
94
95 #[test]
96 fn deserialize_init_options_defaults() {
97 // check that null == default for both fields
98 let default = ServerConfig::default();
99 assert_eq!(default, serde_json::from_str(r#"{}"#).unwrap());
100 assert_eq!(
101 default,
102 serde_json::from_str(r#"{"publishDecorations":null, "lruCapacity":null}"#).unwrap()
103 );
104 }
105}
diff --git a/crates/ra_lsp_server/src/conv.rs b/crates/ra_lsp_server/src/conv.rs
deleted file mode 100644
index 8af74b211..000000000
--- a/crates/ra_lsp_server/src/conv.rs
+++ /dev/null
@@ -1,629 +0,0 @@
1//! Convenience module responsible for translating between rust-analyzer's types and LSP types.
2
3use lsp_types::{
4 self, CreateFile, DiagnosticSeverity, DocumentChangeOperation, DocumentChanges, Documentation,
5 Location, LocationLink, MarkupContent, MarkupKind, Position, Range, RenameFile, ResourceOp,
6 SymbolKind, TextDocumentEdit, TextDocumentIdentifier, TextDocumentItem,
7 TextDocumentPositionParams, Url, VersionedTextDocumentIdentifier, WorkspaceEdit,
8};
9use ra_ide::{
10 translate_offset_with_edit, CompletionItem, CompletionItemKind, FileId, FilePosition,
11 FileRange, FileSystemEdit, Fold, FoldKind, InsertTextFormat, LineCol, LineIndex,
12 NavigationTarget, RangeInfo, ReferenceAccess, Severity, SourceChange, SourceFileEdit,
13};
14use ra_syntax::{SyntaxKind, TextRange, TextUnit};
15use ra_text_edit::{AtomTextEdit, TextEdit};
16use ra_vfs::LineEndings;
17
18use crate::{req, world::WorldSnapshot, Result};
19
20pub trait Conv {
21 type Output;
22 fn conv(self) -> Self::Output;
23}
24
25pub trait ConvWith<CTX> {
26 type Output;
27 fn conv_with(self, ctx: CTX) -> Self::Output;
28}
29
30pub trait TryConvWith<CTX> {
31 type Output;
32 fn try_conv_with(self, ctx: CTX) -> Result<Self::Output>;
33}
34
35impl Conv for SyntaxKind {
36 type Output = SymbolKind;
37
38 fn conv(self) -> <Self as Conv>::Output {
39 match self {
40 SyntaxKind::FN_DEF => SymbolKind::Function,
41 SyntaxKind::STRUCT_DEF => SymbolKind::Struct,
42 SyntaxKind::ENUM_DEF => SymbolKind::Enum,
43 SyntaxKind::ENUM_VARIANT => SymbolKind::EnumMember,
44 SyntaxKind::TRAIT_DEF => SymbolKind::Interface,
45 SyntaxKind::MACRO_CALL => SymbolKind::Function,
46 SyntaxKind::MODULE => SymbolKind::Module,
47 SyntaxKind::TYPE_ALIAS_DEF => SymbolKind::TypeParameter,
48 SyntaxKind::RECORD_FIELD_DEF => SymbolKind::Field,
49 SyntaxKind::STATIC_DEF => SymbolKind::Constant,
50 SyntaxKind::CONST_DEF => SymbolKind::Constant,
51 SyntaxKind::IMPL_BLOCK => SymbolKind::Object,
52 _ => SymbolKind::Variable,
53 }
54 }
55}
56
57impl Conv for ReferenceAccess {
58 type Output = ::lsp_types::DocumentHighlightKind;
59
60 fn conv(self) -> Self::Output {
61 use lsp_types::DocumentHighlightKind;
62 match self {
63 ReferenceAccess::Read => DocumentHighlightKind::Read,
64 ReferenceAccess::Write => DocumentHighlightKind::Write,
65 }
66 }
67}
68
69impl Conv for CompletionItemKind {
70 type Output = ::lsp_types::CompletionItemKind;
71
72 fn conv(self) -> <Self as Conv>::Output {
73 use lsp_types::CompletionItemKind::*;
74 match self {
75 CompletionItemKind::Keyword => Keyword,
76 CompletionItemKind::Snippet => Snippet,
77 CompletionItemKind::Module => Module,
78 CompletionItemKind::Function => Function,
79 CompletionItemKind::Struct => Struct,
80 CompletionItemKind::Enum => Enum,
81 CompletionItemKind::EnumVariant => EnumMember,
82 CompletionItemKind::BuiltinType => Struct,
83 CompletionItemKind::Binding => Variable,
84 CompletionItemKind::Field => Field,
85 CompletionItemKind::Trait => Interface,
86 CompletionItemKind::TypeAlias => Struct,
87 CompletionItemKind::Const => Constant,
88 CompletionItemKind::Static => Value,
89 CompletionItemKind::Method => Method,
90 CompletionItemKind::TypeParam => TypeParameter,
91 CompletionItemKind::Macro => Method,
92 }
93 }
94}
95
96impl Conv for Severity {
97 type Output = DiagnosticSeverity;
98 fn conv(self) -> DiagnosticSeverity {
99 match self {
100 Severity::Error => DiagnosticSeverity::Error,
101 Severity::WeakWarning => DiagnosticSeverity::Hint,
102 }
103 }
104}
105
106impl ConvWith<(&LineIndex, LineEndings)> for CompletionItem {
107 type Output = ::lsp_types::CompletionItem;
108
109 fn conv_with(self, ctx: (&LineIndex, LineEndings)) -> ::lsp_types::CompletionItem {
110 let mut additional_text_edits = Vec::new();
111 let mut text_edit = None;
112 // LSP does not allow arbitrary edits in completion, so we have to do a
113 // non-trivial mapping here.
114 for atom_edit in self.text_edit().as_atoms() {
115 if self.source_range().is_subrange(&atom_edit.delete) {
116 text_edit = Some(if atom_edit.delete == self.source_range() {
117 atom_edit.conv_with(ctx)
118 } else {
119 assert!(self.source_range().end() == atom_edit.delete.end());
120 let range1 =
121 TextRange::from_to(atom_edit.delete.start(), self.source_range().start());
122 let range2 = self.source_range();
123 let edit1 = AtomTextEdit::replace(range1, String::new());
124 let edit2 = AtomTextEdit::replace(range2, atom_edit.insert.clone());
125 additional_text_edits.push(edit1.conv_with(ctx));
126 edit2.conv_with(ctx)
127 })
128 } else {
129 assert!(self.source_range().intersection(&atom_edit.delete).is_none());
130 additional_text_edits.push(atom_edit.conv_with(ctx));
131 }
132 }
133 let text_edit = text_edit.unwrap();
134
135 let mut res = lsp_types::CompletionItem {
136 label: self.label().to_string(),
137 detail: self.detail().map(|it| it.to_string()),
138 filter_text: Some(self.lookup().to_string()),
139 kind: self.kind().map(|it| it.conv()),
140 text_edit: Some(text_edit),
141 additional_text_edits: Some(additional_text_edits),
142 documentation: self.documentation().map(|it| it.conv()),
143 deprecated: Some(self.deprecated()),
144 ..Default::default()
145 };
146
147 if self.deprecated() {
148 res.tags = Some(vec![lsp_types::CompletionItemTag::Deprecated])
149 }
150
151 res.insert_text_format = Some(match self.insert_text_format() {
152 InsertTextFormat::Snippet => lsp_types::InsertTextFormat::Snippet,
153 InsertTextFormat::PlainText => lsp_types::InsertTextFormat::PlainText,
154 });
155
156 res
157 }
158}
159
160impl ConvWith<&LineIndex> for Position {
161 type Output = TextUnit;
162
163 fn conv_with(self, line_index: &LineIndex) -> TextUnit {
164 let line_col = LineCol { line: self.line as u32, col_utf16: self.character as u32 };
165 line_index.offset(line_col)
166 }
167}
168
169impl ConvWith<&LineIndex> for TextUnit {
170 type Output = Position;
171
172 fn conv_with(self, line_index: &LineIndex) -> Position {
173 let line_col = line_index.line_col(self);
174 Position::new(u64::from(line_col.line), u64::from(line_col.col_utf16))
175 }
176}
177
178impl ConvWith<&LineIndex> for TextRange {
179 type Output = Range;
180
181 fn conv_with(self, line_index: &LineIndex) -> Range {
182 Range::new(self.start().conv_with(line_index), self.end().conv_with(line_index))
183 }
184}
185
186impl ConvWith<&LineIndex> for Range {
187 type Output = TextRange;
188
189 fn conv_with(self, line_index: &LineIndex) -> TextRange {
190 TextRange::from_to(self.start.conv_with(line_index), self.end.conv_with(line_index))
191 }
192}
193
194impl Conv for ra_ide::Documentation {
195 type Output = lsp_types::Documentation;
196 fn conv(self) -> Documentation {
197 Documentation::MarkupContent(MarkupContent {
198 kind: MarkupKind::Markdown,
199 value: crate::markdown::format_docs(self.as_str()),
200 })
201 }
202}
203
204impl Conv for ra_ide::FunctionSignature {
205 type Output = lsp_types::SignatureInformation;
206 fn conv(self) -> Self::Output {
207 use lsp_types::{ParameterInformation, ParameterLabel, SignatureInformation};
208
209 let label = self.to_string();
210
211 let documentation = self.doc.map(|it| it.conv());
212
213 let parameters: Vec<ParameterInformation> = self
214 .parameters
215 .into_iter()
216 .map(|param| ParameterInformation {
217 label: ParameterLabel::Simple(param),
218 documentation: None,
219 })
220 .collect();
221
222 SignatureInformation { label, documentation, parameters: Some(parameters) }
223 }
224}
225
226impl ConvWith<(&LineIndex, LineEndings)> for TextEdit {
227 type Output = Vec<lsp_types::TextEdit>;
228
229 fn conv_with(self, ctx: (&LineIndex, LineEndings)) -> Vec<lsp_types::TextEdit> {
230 self.as_atoms().iter().map_conv_with(ctx).collect()
231 }
232}
233
234impl ConvWith<(&LineIndex, LineEndings)> for &AtomTextEdit {
235 type Output = lsp_types::TextEdit;
236
237 fn conv_with(
238 self,
239 (line_index, line_endings): (&LineIndex, LineEndings),
240 ) -> lsp_types::TextEdit {
241 let mut new_text = self.insert.clone();
242 if line_endings == LineEndings::Dos {
243 new_text = new_text.replace('\n', "\r\n");
244 }
245 lsp_types::TextEdit { range: self.delete.conv_with(line_index), new_text }
246 }
247}
248
249pub(crate) struct FoldConvCtx<'a> {
250 pub(crate) text: &'a str,
251 pub(crate) line_index: &'a LineIndex,
252 pub(crate) line_folding_only: bool,
253}
254
255impl ConvWith<&FoldConvCtx<'_>> for Fold {
256 type Output = lsp_types::FoldingRange;
257
258 fn conv_with(self, ctx: &FoldConvCtx) -> lsp_types::FoldingRange {
259 let kind = match self.kind {
260 FoldKind::Comment => Some(lsp_types::FoldingRangeKind::Comment),
261 FoldKind::Imports => Some(lsp_types::FoldingRangeKind::Imports),
262 FoldKind::Mods => None,
263 FoldKind::Block => None,
264 };
265
266 let range = self.range.conv_with(&ctx.line_index);
267
268 if ctx.line_folding_only {
269 // Clients with line_folding_only == true (such as VSCode) will fold the whole end line
270 // even if it contains text not in the folding range. To prevent that we exclude
271 // range.end.line from the folding region if there is more text after range.end
272 // on the same line.
273 let has_more_text_on_end_line = ctx.text
274 [TextRange::from_to(self.range.end(), TextUnit::of_str(ctx.text))]
275 .chars()
276 .take_while(|it| *it != '\n')
277 .any(|it| !it.is_whitespace());
278
279 let end_line = if has_more_text_on_end_line {
280 range.end.line.saturating_sub(1)
281 } else {
282 range.end.line
283 };
284
285 lsp_types::FoldingRange {
286 start_line: range.start.line,
287 start_character: None,
288 end_line,
289 end_character: None,
290 kind,
291 }
292 } else {
293 lsp_types::FoldingRange {
294 start_line: range.start.line,
295 start_character: Some(range.start.character),
296 end_line: range.end.line,
297 end_character: Some(range.end.character),
298 kind,
299 }
300 }
301 }
302}
303
304impl<T: ConvWith<CTX>, CTX> ConvWith<CTX> for Option<T> {
305 type Output = Option<T::Output>;
306
307 fn conv_with(self, ctx: CTX) -> Self::Output {
308 self.map(|x| ConvWith::conv_with(x, ctx))
309 }
310}
311
312impl TryConvWith<&WorldSnapshot> for &Url {
313 type Output = FileId;
314 fn try_conv_with(self, world: &WorldSnapshot) -> Result<FileId> {
315 world.uri_to_file_id(self)
316 }
317}
318
319impl TryConvWith<&WorldSnapshot> for FileId {
320 type Output = Url;
321 fn try_conv_with(self, world: &WorldSnapshot) -> Result<Url> {
322 world.file_id_to_uri(self)
323 }
324}
325
326impl TryConvWith<&WorldSnapshot> for &TextDocumentItem {
327 type Output = FileId;
328 fn try_conv_with(self, world: &WorldSnapshot) -> Result<FileId> {
329 self.uri.try_conv_with(world)
330 }
331}
332
333impl TryConvWith<&WorldSnapshot> for &VersionedTextDocumentIdentifier {
334 type Output = FileId;
335 fn try_conv_with(self, world: &WorldSnapshot) -> Result<FileId> {
336 self.uri.try_conv_with(world)
337 }
338}
339
340impl TryConvWith<&WorldSnapshot> for &TextDocumentIdentifier {
341 type Output = FileId;
342 fn try_conv_with(self, world: &WorldSnapshot) -> Result<FileId> {
343 world.uri_to_file_id(&self.uri)
344 }
345}
346
347impl TryConvWith<&WorldSnapshot> for &TextDocumentPositionParams {
348 type Output = FilePosition;
349 fn try_conv_with(self, world: &WorldSnapshot) -> Result<FilePosition> {
350 let file_id = self.text_document.try_conv_with(world)?;
351 let line_index = world.analysis().file_line_index(file_id)?;
352 let offset = self.position.conv_with(&line_index);
353 Ok(FilePosition { file_id, offset })
354 }
355}
356
357impl TryConvWith<&WorldSnapshot> for (&TextDocumentIdentifier, Range) {
358 type Output = FileRange;
359 fn try_conv_with(self, world: &WorldSnapshot) -> Result<FileRange> {
360 let file_id = self.0.try_conv_with(world)?;
361 let line_index = world.analysis().file_line_index(file_id)?;
362 let range = self.1.conv_with(&line_index);
363 Ok(FileRange { file_id, range })
364 }
365}
366
367impl<T: TryConvWith<CTX>, CTX: Copy> TryConvWith<CTX> for Vec<T> {
368 type Output = Vec<<T as TryConvWith<CTX>>::Output>;
369 fn try_conv_with(self, ctx: CTX) -> Result<Self::Output> {
370 let mut res = Vec::with_capacity(self.len());
371 for item in self {
372 res.push(item.try_conv_with(ctx)?);
373 }
374 Ok(res)
375 }
376}
377
378impl TryConvWith<&WorldSnapshot> for SourceChange {
379 type Output = req::SourceChange;
380 fn try_conv_with(self, world: &WorldSnapshot) -> Result<req::SourceChange> {
381 let cursor_position = match self.cursor_position {
382 None => None,
383 Some(pos) => {
384 let line_index = world.analysis().file_line_index(pos.file_id)?;
385 let edit = self
386 .source_file_edits
387 .iter()
388 .find(|it| it.file_id == pos.file_id)
389 .map(|it| &it.edit);
390 let line_col = match edit {
391 Some(edit) => translate_offset_with_edit(&*line_index, pos.offset, edit),
392 None => line_index.line_col(pos.offset),
393 };
394 let position =
395 Position::new(u64::from(line_col.line), u64::from(line_col.col_utf16));
396 Some(TextDocumentPositionParams {
397 text_document: TextDocumentIdentifier::new(pos.file_id.try_conv_with(world)?),
398 position,
399 })
400 }
401 };
402 let mut document_changes: Vec<DocumentChangeOperation> = Vec::new();
403 for resource_op in self.file_system_edits.try_conv_with(world)? {
404 document_changes.push(DocumentChangeOperation::Op(resource_op));
405 }
406 for text_document_edit in self.source_file_edits.try_conv_with(world)? {
407 document_changes.push(DocumentChangeOperation::Edit(text_document_edit));
408 }
409 let workspace_edit = WorkspaceEdit {
410 changes: None,
411 document_changes: Some(DocumentChanges::Operations(document_changes)),
412 };
413 Ok(req::SourceChange { label: self.label, workspace_edit, cursor_position })
414 }
415}
416
417impl TryConvWith<&WorldSnapshot> for SourceFileEdit {
418 type Output = TextDocumentEdit;
419 fn try_conv_with(self, world: &WorldSnapshot) -> Result<TextDocumentEdit> {
420 let text_document = VersionedTextDocumentIdentifier {
421 uri: self.file_id.try_conv_with(world)?,
422 version: None,
423 };
424 let line_index = world.analysis().file_line_index(self.file_id)?;
425 let line_endings = world.file_line_endings(self.file_id);
426 let edits =
427 self.edit.as_atoms().iter().map_conv_with((&line_index, line_endings)).collect();
428 Ok(TextDocumentEdit { text_document, edits })
429 }
430}
431
432impl TryConvWith<&WorldSnapshot> for FileSystemEdit {
433 type Output = ResourceOp;
434 fn try_conv_with(self, world: &WorldSnapshot) -> Result<ResourceOp> {
435 let res = match self {
436 FileSystemEdit::CreateFile { source_root, path } => {
437 let uri = world.path_to_uri(source_root, &path)?;
438 ResourceOp::Create(CreateFile { uri, options: None })
439 }
440 FileSystemEdit::MoveFile { src, dst_source_root, dst_path } => {
441 let old_uri = world.file_id_to_uri(src)?;
442 let new_uri = world.path_to_uri(dst_source_root, &dst_path)?;
443 ResourceOp::Rename(RenameFile { old_uri, new_uri, options: None })
444 }
445 };
446 Ok(res)
447 }
448}
449
450impl TryConvWith<&WorldSnapshot> for &NavigationTarget {
451 type Output = Location;
452 fn try_conv_with(self, world: &WorldSnapshot) -> Result<Location> {
453 let line_index = world.analysis().file_line_index(self.file_id())?;
454 let range = self.range();
455 to_location(self.file_id(), range, &world, &line_index)
456 }
457}
458
459impl TryConvWith<&WorldSnapshot> for (FileId, RangeInfo<NavigationTarget>) {
460 type Output = LocationLink;
461 fn try_conv_with(self, world: &WorldSnapshot) -> Result<LocationLink> {
462 let (src_file_id, target) = self;
463
464 let target_uri = target.info.file_id().try_conv_with(world)?;
465 let src_line_index = world.analysis().file_line_index(src_file_id)?;
466 let tgt_line_index = world.analysis().file_line_index(target.info.file_id())?;
467
468 let target_range = target.info.full_range().conv_with(&tgt_line_index);
469
470 let target_selection_range = target
471 .info
472 .focus_range()
473 .map(|it| it.conv_with(&tgt_line_index))
474 .unwrap_or(target_range);
475
476 let res = LocationLink {
477 origin_selection_range: Some(target.range.conv_with(&src_line_index)),
478 target_uri,
479 target_range,
480 target_selection_range,
481 };
482 Ok(res)
483 }
484}
485
486impl TryConvWith<&WorldSnapshot> for (FileId, RangeInfo<Vec<NavigationTarget>>) {
487 type Output = req::GotoDefinitionResponse;
488 fn try_conv_with(self, world: &WorldSnapshot) -> Result<req::GotoTypeDefinitionResponse> {
489 let (file_id, RangeInfo { range, info: navs }) = self;
490 let links = navs
491 .into_iter()
492 .map(|nav| (file_id, RangeInfo::new(range, nav)))
493 .try_conv_with_to_vec(world)?;
494 if world.options.supports_location_link {
495 Ok(links.into())
496 } else {
497 let locations: Vec<Location> = links
498 .into_iter()
499 .map(|link| Location { uri: link.target_uri, range: link.target_selection_range })
500 .collect();
501 Ok(locations.into())
502 }
503 }
504}
505
506pub fn to_call_hierarchy_item(
507 file_id: FileId,
508 range: TextRange,
509 world: &WorldSnapshot,
510 line_index: &LineIndex,
511 nav: NavigationTarget,
512) -> Result<lsp_types::CallHierarchyItem> {
513 Ok(lsp_types::CallHierarchyItem {
514 name: nav.name().to_string(),
515 kind: nav.kind().conv(),
516 tags: None,
517 detail: nav.description().map(|it| it.to_string()),
518 uri: file_id.try_conv_with(&world)?,
519 range: nav.range().conv_with(&line_index),
520 selection_range: range.conv_with(&line_index),
521 })
522}
523
524pub fn to_location(
525 file_id: FileId,
526 range: TextRange,
527 world: &WorldSnapshot,
528 line_index: &LineIndex,
529) -> Result<Location> {
530 let url = file_id.try_conv_with(world)?;
531 let loc = Location::new(url, range.conv_with(line_index));
532 Ok(loc)
533}
534
535pub trait MapConvWith<CTX>: Sized {
536 type Output;
537
538 fn map_conv_with(self, ctx: CTX) -> ConvWithIter<Self, CTX> {
539 ConvWithIter { iter: self, ctx }
540 }
541}
542
543impl<CTX, I> MapConvWith<CTX> for I
544where
545 I: Iterator,
546 I::Item: ConvWith<CTX>,
547{
548 type Output = <I::Item as ConvWith<CTX>>::Output;
549}
550
551pub struct ConvWithIter<I, CTX> {
552 iter: I,
553 ctx: CTX,
554}
555
556impl<I, CTX> Iterator for ConvWithIter<I, CTX>
557where
558 I: Iterator,
559 I::Item: ConvWith<CTX>,
560 CTX: Copy,
561{
562 type Item = <I::Item as ConvWith<CTX>>::Output;
563
564 fn next(&mut self) -> Option<Self::Item> {
565 self.iter.next().map(|item| item.conv_with(self.ctx))
566 }
567}
568
569pub trait TryConvWithToVec<CTX>: Sized {
570 type Output;
571
572 fn try_conv_with_to_vec(self, ctx: CTX) -> Result<Vec<Self::Output>>;
573}
574
575impl<I, CTX> TryConvWithToVec<CTX> for I
576where
577 I: Iterator,
578 I::Item: TryConvWith<CTX>,
579 CTX: Copy,
580{
581 type Output = <I::Item as TryConvWith<CTX>>::Output;
582
583 fn try_conv_with_to_vec(self, ctx: CTX) -> Result<Vec<Self::Output>> {
584 self.map(|it| it.try_conv_with(ctx)).collect()
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591 use test_utils::extract_ranges;
592
593 #[test]
594 fn conv_fold_line_folding_only_fixup() {
595 let text = r#"<fold>mod a;
596mod b;
597mod c;</fold>
598
599fn main() <fold>{
600 if cond <fold>{
601 a::do_a();
602 }</fold> else <fold>{
603 b::do_b();
604 }</fold>
605}</fold>"#;
606
607 let (ranges, text) = extract_ranges(text, "fold");
608 assert_eq!(ranges.len(), 4);
609 let folds = vec![
610 Fold { range: ranges[0], kind: FoldKind::Mods },
611 Fold { range: ranges[1], kind: FoldKind::Block },
612 Fold { range: ranges[2], kind: FoldKind::Block },
613 Fold { range: ranges[3], kind: FoldKind::Block },
614 ];
615
616 let line_index = LineIndex::new(&text);
617 let ctx = FoldConvCtx { text: &text, line_index: &line_index, line_folding_only: true };
618 let converted: Vec<_> = folds.into_iter().map_conv_with(&ctx).collect();
619
620 let expected_lines = [(0, 2), (4, 10), (5, 6), (7, 9)];
621 assert_eq!(converted.len(), expected_lines.len());
622 for (folding_range, (start_line, end_line)) in converted.iter().zip(expected_lines.iter()) {
623 assert_eq!(folding_range.start_line, *start_line);
624 assert_eq!(folding_range.start_character, None);
625 assert_eq!(folding_range.end_line, *end_line);
626 assert_eq!(folding_range.end_character, None);
627 }
628 }
629}
diff --git a/crates/ra_lsp_server/src/diagnostics.rs b/crates/ra_lsp_server/src/diagnostics.rs
deleted file mode 100644
index ea08bce24..000000000
--- a/crates/ra_lsp_server/src/diagnostics.rs
+++ /dev/null
@@ -1,85 +0,0 @@
1//! Book keeping for keeping diagnostics easily in sync with the client.
2use lsp_types::{CodeActionOrCommand, Diagnostic, Range};
3use ra_ide::FileId;
4use std::{collections::HashMap, sync::Arc};
5
6pub type CheckFixes = Arc<HashMap<FileId, Vec<Fix>>>;
7
8#[derive(Debug, Default, Clone)]
9pub struct DiagnosticCollection {
10 pub native: HashMap<FileId, Vec<Diagnostic>>,
11 pub check: HashMap<FileId, Vec<Diagnostic>>,
12 pub check_fixes: CheckFixes,
13}
14
15#[derive(Debug, Clone)]
16pub struct Fix {
17 pub range: Range,
18 pub action: CodeActionOrCommand,
19}
20
21#[derive(Debug)]
22pub enum DiagnosticTask {
23 ClearCheck,
24 AddCheck(FileId, Diagnostic, Vec<CodeActionOrCommand>),
25 SetNative(FileId, Vec<Diagnostic>),
26}
27
28impl DiagnosticCollection {
29 pub fn clear_check(&mut self) -> Vec<FileId> {
30 Arc::make_mut(&mut self.check_fixes).clear();
31 self.check.drain().map(|(key, _value)| key).collect()
32 }
33
34 pub fn add_check_diagnostic(
35 &mut self,
36 file_id: FileId,
37 diagnostic: Diagnostic,
38 fixes: Vec<CodeActionOrCommand>,
39 ) {
40 let diagnostics = self.check.entry(file_id).or_default();
41 for existing_diagnostic in diagnostics.iter() {
42 if are_diagnostics_equal(&existing_diagnostic, &diagnostic) {
43 return;
44 }
45 }
46
47 let check_fixes = Arc::make_mut(&mut self.check_fixes);
48 check_fixes
49 .entry(file_id)
50 .or_default()
51 .extend(fixes.into_iter().map(|action| Fix { range: diagnostic.range, action }));
52 diagnostics.push(diagnostic);
53 }
54
55 pub fn set_native_diagnostics(&mut self, file_id: FileId, diagnostics: Vec<Diagnostic>) {
56 self.native.insert(file_id, diagnostics);
57 }
58
59 pub fn diagnostics_for(&self, file_id: FileId) -> impl Iterator<Item = &Diagnostic> {
60 let native = self.native.get(&file_id).into_iter().flatten();
61 let check = self.check.get(&file_id).into_iter().flatten();
62 native.chain(check)
63 }
64
65 pub fn handle_task(&mut self, task: DiagnosticTask) -> Vec<FileId> {
66 match task {
67 DiagnosticTask::ClearCheck => self.clear_check(),
68 DiagnosticTask::AddCheck(file_id, diagnostic, fixes) => {
69 self.add_check_diagnostic(file_id, diagnostic, fixes);
70 vec![file_id]
71 }
72 DiagnosticTask::SetNative(file_id, diagnostics) => {
73 self.set_native_diagnostics(file_id, diagnostics);
74 vec![file_id]
75 }
76 }
77 }
78}
79
80fn are_diagnostics_equal(left: &Diagnostic, right: &Diagnostic) -> bool {
81 left.source == right.source
82 && left.severity == right.severity
83 && left.range == right.range
84 && left.message == right.message
85}
diff --git a/crates/ra_lsp_server/src/lib.rs b/crates/ra_lsp_server/src/lib.rs
deleted file mode 100644
index 958c70fe5..000000000
--- a/crates/ra_lsp_server/src/lib.rs
+++ /dev/null
@@ -1,51 +0,0 @@
1//! Implementation of the LSP for rust-analyzer.
2//!
3//! This crate takes Rust-specific analysis results from ra_ide and
4//! translates into LSP types.
5//!
6//! It also is the root of all state. `world` module defines the bulk of the
7//! state, and `main_loop` module defines the rules for modifying it.
8#![recursion_limit = "512"]
9
10pub mod cli;
11
12#[allow(unused)]
13macro_rules! println {
14 ($($tt:tt)*) => {
15 compile_error!("stdout is locked, use eprintln")
16 };
17}
18
19#[allow(unused)]
20macro_rules! print {
21 ($($tt:tt)*) => {
22 compile_error!("stdout is locked, use eprint")
23 };
24}
25
26mod vfs_glob;
27mod caps;
28mod cargo_target_spec;
29mod conv;
30mod main_loop;
31mod markdown;
32pub mod req;
33mod config;
34mod world;
35mod diagnostics;
36
37use serde::de::DeserializeOwned;
38
39pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
40pub use crate::{
41 caps::server_capabilities,
42 config::ServerConfig,
43 main_loop::LspError,
44 main_loop::{main_loop, show_message},
45};
46
47pub fn from_json<T: DeserializeOwned>(what: &'static str, json: serde_json::Value) -> Result<T> {
48 let res = T::deserialize(&json)
49 .map_err(|e| format!("Failed to deserialize {}: {}; {}", what, e, json))?;
50 Ok(res)
51}
diff --git a/crates/ra_lsp_server/src/main.rs b/crates/ra_lsp_server/src/main.rs
deleted file mode 100644
index a549e5ff1..000000000
--- a/crates/ra_lsp_server/src/main.rs
+++ /dev/null
@@ -1,96 +0,0 @@
1//! `ra_lsp_server` binary
2mod args;
3
4use lsp_server::Connection;
5use ra_lsp_server::{cli, from_json, show_message, Result, ServerConfig};
6use ra_prof;
7
8use crate::args::HelpPrinted;
9
10fn main() -> Result<()> {
11 setup_logging()?;
12 let args = match args::Args::parse()? {
13 Ok(it) => it,
14 Err(HelpPrinted) => return Ok(()),
15 };
16 match args.command {
17 args::Command::Parse { no_dump } => cli::parse(no_dump)?,
18 args::Command::Symbols => cli::symbols()?,
19 args::Command::Highlight { rainbow } => cli::highlight(rainbow)?,
20 args::Command::Stats { randomize, memory_usage, only, with_deps, path } => {
21 cli::analysis_stats(
22 args.verbosity,
23 memory_usage,
24 path.as_ref(),
25 only.as_ref().map(String::as_ref),
26 with_deps,
27 randomize,
28 )?
29 }
30
31 args::Command::Bench { path, what } => {
32 cli::analysis_bench(args.verbosity, path.as_ref(), what)?
33 }
34
35 args::Command::RunServer => run_server()?,
36 args::Command::Version => println!("rust-analyzer {}", env!("REV")),
37 }
38 Ok(())
39}
40
41fn setup_logging() -> Result<()> {
42 std::env::set_var("RUST_BACKTRACE", "short");
43 env_logger::try_init()?;
44 ra_prof::init();
45 Ok(())
46}
47
48fn run_server() -> Result<()> {
49 log::info!("lifecycle: server started");
50
51 let (connection, io_threads) = Connection::stdio();
52 let server_capabilities = serde_json::to_value(ra_lsp_server::server_capabilities()).unwrap();
53
54 let initialize_params = connection.initialize(server_capabilities)?;
55 let initialize_params =
56 from_json::<lsp_types::InitializeParams>("InitializeParams", initialize_params)?;
57
58 if let Some(client_info) = initialize_params.client_info {
59 log::info!("Client '{}' {}", client_info.name, client_info.version.unwrap_or_default());
60 }
61
62 let cwd = std::env::current_dir()?;
63 let root = initialize_params.root_uri.and_then(|it| it.to_file_path().ok()).unwrap_or(cwd);
64
65 let workspace_roots = initialize_params
66 .workspace_folders
67 .map(|workspaces| {
68 workspaces.into_iter().filter_map(|it| it.uri.to_file_path().ok()).collect::<Vec<_>>()
69 })
70 .filter(|workspaces| !workspaces.is_empty())
71 .unwrap_or_else(|| vec![root]);
72
73 let server_config = initialize_params
74 .initialization_options
75 .and_then(|v| {
76 from_json::<ServerConfig>("config", v)
77 .map_err(|e| {
78 log::error!("{}", e);
79 show_message(lsp_types::MessageType::Error, e.to_string(), &connection.sender);
80 })
81 .ok()
82 })
83 .unwrap_or_default();
84
85 ra_lsp_server::main_loop(
86 workspace_roots,
87 initialize_params.capabilities,
88 server_config,
89 connection,
90 )?;
91
92 log::info!("shutting down IO...");
93 io_threads.join()?;
94 log::info!("... IO is down");
95 Ok(())
96}
diff --git a/crates/ra_lsp_server/src/main_loop.rs b/crates/ra_lsp_server/src/main_loop.rs
deleted file mode 100644
index 944074118..000000000
--- a/crates/ra_lsp_server/src/main_loop.rs
+++ /dev/null
@@ -1,889 +0,0 @@
1//! The main loop of `ra_lsp_server` responsible for dispatching LSP requests/replies and
2//! notifications back to the client.
3
4mod handlers;
5mod subscriptions;
6pub(crate) mod pending_requests;
7
8use std::{
9 env,
10 error::Error,
11 fmt, panic,
12 path::PathBuf,
13 sync::Arc,
14 time::{Duration, Instant},
15};
16
17use crossbeam_channel::{select, unbounded, RecvError, Sender};
18use lsp_server::{Connection, ErrorCode, Message, Notification, Request, RequestId, Response};
19use lsp_types::{ClientCapabilities, NumberOrString};
20use ra_cargo_watch::{url_from_path_with_drive_lowercasing, CheckOptions, CheckTask};
21use ra_ide::{Canceled, FeatureFlags, FileId, LibraryData, SourceRootId};
22use ra_prof::profile;
23use ra_vfs::{VfsFile, VfsTask, Watch};
24use relative_path::RelativePathBuf;
25use rustc_hash::FxHashSet;
26use serde::{de::DeserializeOwned, Serialize};
27use threadpool::ThreadPool;
28
29use crate::{
30 diagnostics::DiagnosticTask,
31 main_loop::{
32 pending_requests::{PendingRequest, PendingRequests},
33 subscriptions::Subscriptions,
34 },
35 req,
36 world::{Options, WorldSnapshot, WorldState},
37 Result, ServerConfig,
38};
39
40#[derive(Debug)]
41pub struct LspError {
42 pub code: i32,
43 pub message: String,
44}
45
46impl LspError {
47 pub fn new(code: i32, message: String) -> LspError {
48 LspError { code, message }
49 }
50}
51
52impl fmt::Display for LspError {
53 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
54 write!(f, "Language Server request failed with {}. ({})", self.code, self.message)
55 }
56}
57
58impl Error for LspError {}
59
60pub fn main_loop(
61 ws_roots: Vec<PathBuf>,
62 client_caps: ClientCapabilities,
63 config: ServerConfig,
64 connection: Connection,
65) -> Result<()> {
66 log::info!("server_config: {:#?}", config);
67
68 // Windows scheduler implements priority boosts: if thread waits for an
69 // event (like a condvar), and event fires, priority of the thread is
70 // temporary bumped. This optimization backfires in our case: each time the
71 // `main_loop` schedules a task to run on a threadpool, the worker threads
72 // gets a higher priority, and (on a machine with fewer cores) displaces the
73 // main loop! We work-around this by marking the main loop as a
74 // higher-priority thread.
75 //
76 // https://docs.microsoft.com/en-us/windows/win32/procthread/scheduling-priorities
77 // https://docs.microsoft.com/en-us/windows/win32/procthread/priority-boosts
78 // https://github.com/rust-analyzer/rust-analyzer/issues/2835
79 #[cfg(windows)]
80 unsafe {
81 use winapi::um::processthreadsapi::*;
82 let thread = GetCurrentThread();
83 let thread_priority_above_normal = 1;
84 SetThreadPriority(thread, thread_priority_above_normal);
85 }
86
87 let mut loop_state = LoopState::default();
88 let mut world_state = {
89 let feature_flags = {
90 let mut ff = FeatureFlags::default();
91 for (flag, value) in config.feature_flags {
92 if ff.set(flag.as_str(), value).is_err() {
93 log::error!("unknown feature flag: {:?}", flag);
94 show_message(
95 req::MessageType::Error,
96 format!("unknown feature flag: {:?}", flag),
97 &connection.sender,
98 );
99 }
100 }
101 ff
102 };
103 log::info!("feature_flags: {:#?}", feature_flags);
104
105 // FIXME: support dynamic workspace loading.
106 let workspaces = {
107 let mut loaded_workspaces = Vec::new();
108 for ws_root in &ws_roots {
109 let workspace = ra_project_model::ProjectWorkspace::discover_with_sysroot(
110 ws_root.as_path(),
111 config.with_sysroot,
112 &config.cargo_features,
113 );
114 match workspace {
115 Ok(workspace) => loaded_workspaces.push(workspace),
116 Err(e) => {
117 log::error!("loading workspace failed: {}", e);
118 if let Some(ra_project_model::CargoTomlNotFoundError(_)) = e.downcast_ref()
119 {
120 if !feature_flags.get("notifications.cargo-toml-not-found") {
121 continue;
122 }
123 }
124 show_message(
125 req::MessageType::Error,
126 format!("rust-analyzer failed to load workspace: {}", e),
127 &connection.sender,
128 );
129 }
130 }
131 }
132 loaded_workspaces
133 };
134
135 let globs = config
136 .exclude_globs
137 .iter()
138 .map(|glob| crate::vfs_glob::Glob::new(glob))
139 .collect::<std::result::Result<Vec<_>, _>>()?;
140
141 if config.use_client_watching {
142 let registration_options = req::DidChangeWatchedFilesRegistrationOptions {
143 watchers: workspaces
144 .iter()
145 .flat_map(|ws| ws.to_roots())
146 .filter(|root| root.is_member())
147 .map(|root| format!("{}/**/*.rs", root.path().display()))
148 .map(|glob_pattern| req::FileSystemWatcher { glob_pattern, kind: None })
149 .collect(),
150 };
151 let registration = req::Registration {
152 id: "file-watcher".to_string(),
153 method: "workspace/didChangeWatchedFiles".to_string(),
154 register_options: Some(serde_json::to_value(registration_options).unwrap()),
155 };
156 let params = req::RegistrationParams { registrations: vec![registration] };
157 let request =
158 request_new::<req::RegisterCapability>(loop_state.next_request_id(), params);
159 connection.sender.send(request.into()).unwrap();
160 }
161
162 let options = {
163 let text_document_caps = client_caps.text_document.as_ref();
164 Options {
165 publish_decorations: config.publish_decorations,
166 supports_location_link: text_document_caps
167 .and_then(|it| it.definition)
168 .and_then(|it| it.link_support)
169 .unwrap_or(false),
170 line_folding_only: text_document_caps
171 .and_then(|it| it.folding_range.as_ref())
172 .and_then(|it| it.line_folding_only)
173 .unwrap_or(false),
174 max_inlay_hint_length: config.max_inlay_hint_length,
175 cargo_watch: CheckOptions {
176 enable: config.cargo_watch_enable,
177 args: config.cargo_watch_args,
178 command: config.cargo_watch_command,
179 all_targets: config.cargo_watch_all_targets,
180 },
181 rustfmt_args: config.rustfmt_args,
182 }
183 };
184
185 WorldState::new(
186 ws_roots,
187 workspaces,
188 config.lru_capacity,
189 &globs,
190 Watch(!config.use_client_watching),
191 options,
192 feature_flags,
193 )
194 };
195
196 let pool = ThreadPool::default();
197 let (task_sender, task_receiver) = unbounded::<Task>();
198 let (libdata_sender, libdata_receiver) = unbounded::<LibraryData>();
199
200 log::info!("server initialized, serving requests");
201 {
202 let task_sender = task_sender;
203 let libdata_sender = libdata_sender;
204 loop {
205 log::trace!("selecting");
206 let event = select! {
207 recv(&connection.receiver) -> msg => match msg {
208 Ok(msg) => Event::Msg(msg),
209 Err(RecvError) => Err("client exited without shutdown")?,
210 },
211 recv(task_receiver) -> task => Event::Task(task.unwrap()),
212 recv(world_state.task_receiver) -> task => match task {
213 Ok(task) => Event::Vfs(task),
214 Err(RecvError) => Err("vfs died")?,
215 },
216 recv(libdata_receiver) -> data => Event::Lib(data.unwrap()),
217 recv(world_state.check_watcher.task_recv) -> task => match task {
218 Ok(task) => Event::CheckWatcher(task),
219 Err(RecvError) => Err("check watcher died")?,
220 }
221 };
222 if let Event::Msg(Message::Request(req)) = &event {
223 if connection.handle_shutdown(&req)? {
224 break;
225 };
226 }
227 loop_turn(
228 &pool,
229 &task_sender,
230 &libdata_sender,
231 &connection,
232 &mut world_state,
233 &mut loop_state,
234 event,
235 )?;
236 }
237 }
238 world_state.analysis_host.request_cancellation();
239 log::info!("waiting for tasks to finish...");
240 task_receiver.into_iter().for_each(|task| {
241 on_task(task, &connection.sender, &mut loop_state.pending_requests, &mut world_state)
242 });
243 libdata_receiver.into_iter().for_each(drop);
244 log::info!("...tasks have finished");
245 log::info!("joining threadpool...");
246 drop(pool);
247 log::info!("...threadpool has finished");
248
249 let vfs = Arc::try_unwrap(world_state.vfs).expect("all snapshots should be dead");
250 drop(vfs);
251
252 Ok(())
253}
254
255#[derive(Debug)]
256enum Task {
257 Respond(Response),
258 Notify(Notification),
259 Diagnostic(DiagnosticTask),
260}
261
262enum Event {
263 Msg(Message),
264 Task(Task),
265 Vfs(VfsTask),
266 Lib(LibraryData),
267 CheckWatcher(CheckTask),
268}
269
270impl fmt::Debug for Event {
271 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
272 let debug_verbose_not = |not: &Notification, f: &mut fmt::Formatter| {
273 f.debug_struct("Notification").field("method", &not.method).finish()
274 };
275
276 match self {
277 Event::Msg(Message::Notification(not)) => {
278 if notification_is::<req::DidOpenTextDocument>(not)
279 || notification_is::<req::DidChangeTextDocument>(not)
280 {
281 return debug_verbose_not(not, f);
282 }
283 }
284 Event::Task(Task::Notify(not)) => {
285 if notification_is::<req::PublishDecorations>(not)
286 || notification_is::<req::PublishDiagnostics>(not)
287 {
288 return debug_verbose_not(not, f);
289 }
290 }
291 Event::Task(Task::Respond(resp)) => {
292 return f
293 .debug_struct("Response")
294 .field("id", &resp.id)
295 .field("error", &resp.error)
296 .finish();
297 }
298 _ => (),
299 }
300 match self {
301 Event::Msg(it) => fmt::Debug::fmt(it, f),
302 Event::Task(it) => fmt::Debug::fmt(it, f),
303 Event::Vfs(it) => fmt::Debug::fmt(it, f),
304 Event::Lib(it) => fmt::Debug::fmt(it, f),
305 Event::CheckWatcher(it) => fmt::Debug::fmt(it, f),
306 }
307 }
308}
309
310#[derive(Debug, Default)]
311struct LoopState {
312 next_request_id: u64,
313 pending_responses: FxHashSet<RequestId>,
314 pending_requests: PendingRequests,
315 subscriptions: Subscriptions,
316 // We try not to index more than MAX_IN_FLIGHT_LIBS libraries at the same
317 // time to always have a thread ready to react to input.
318 in_flight_libraries: usize,
319 pending_libraries: Vec<(SourceRootId, Vec<(FileId, RelativePathBuf, Arc<String>)>)>,
320 workspace_loaded: bool,
321}
322
323impl LoopState {
324 fn next_request_id(&mut self) -> RequestId {
325 self.next_request_id += 1;
326 let res: RequestId = self.next_request_id.into();
327 let inserted = self.pending_responses.insert(res.clone());
328 assert!(inserted);
329 res
330 }
331}
332
333fn loop_turn(
334 pool: &ThreadPool,
335 task_sender: &Sender<Task>,
336 libdata_sender: &Sender<LibraryData>,
337 connection: &Connection,
338 world_state: &mut WorldState,
339 loop_state: &mut LoopState,
340 event: Event,
341) -> Result<()> {
342 let loop_start = Instant::now();
343
344 // NOTE: don't count blocking select! call as a loop-turn time
345 let _p = profile("main_loop_inner/loop-turn");
346 log::info!("loop turn = {:?}", event);
347 let queue_count = pool.queued_count();
348 if queue_count > 0 {
349 log::info!("queued count = {}", queue_count);
350 }
351
352 match event {
353 Event::Task(task) => {
354 on_task(task, &connection.sender, &mut loop_state.pending_requests, world_state);
355 world_state.maybe_collect_garbage();
356 }
357 Event::Vfs(task) => {
358 world_state.vfs.write().handle_task(task);
359 }
360 Event::Lib(lib) => {
361 world_state.add_lib(lib);
362 world_state.maybe_collect_garbage();
363 loop_state.in_flight_libraries -= 1;
364 }
365 Event::CheckWatcher(task) => on_check_task(task, world_state, task_sender)?,
366 Event::Msg(msg) => match msg {
367 Message::Request(req) => on_request(
368 world_state,
369 &mut loop_state.pending_requests,
370 pool,
371 task_sender,
372 &connection.sender,
373 loop_start,
374 req,
375 )?,
376 Message::Notification(not) => {
377 on_notification(
378 &connection.sender,
379 world_state,
380 &mut loop_state.pending_requests,
381 &mut loop_state.subscriptions,
382 not,
383 )?;
384 }
385 Message::Response(resp) => {
386 let removed = loop_state.pending_responses.remove(&resp.id);
387 if !removed {
388 log::error!("unexpected response: {:?}", resp)
389 }
390 }
391 },
392 };
393
394 let mut state_changed = false;
395 if let Some(changes) = world_state.process_changes() {
396 state_changed = true;
397 loop_state.pending_libraries.extend(changes);
398 }
399
400 let max_in_flight_libs = pool.max_count().saturating_sub(2).max(1);
401 while loop_state.in_flight_libraries < max_in_flight_libs
402 && !loop_state.pending_libraries.is_empty()
403 {
404 let (root, files) = loop_state.pending_libraries.pop().unwrap();
405 loop_state.in_flight_libraries += 1;
406 let sender = libdata_sender.clone();
407 pool.execute(move || {
408 log::info!("indexing {:?} ... ", root);
409 let data = LibraryData::prepare(root, files);
410 sender.send(data).unwrap();
411 });
412 }
413
414 if !loop_state.workspace_loaded
415 && world_state.roots_to_scan == 0
416 && loop_state.pending_libraries.is_empty()
417 && loop_state.in_flight_libraries == 0
418 {
419 loop_state.workspace_loaded = true;
420 let n_packages: usize = world_state.workspaces.iter().map(|it| it.n_packages()).sum();
421 if world_state.feature_flags().get("notifications.workspace-loaded") {
422 let msg = format!("workspace loaded, {} rust packages", n_packages);
423 show_message(req::MessageType::Info, msg, &connection.sender);
424 }
425 world_state.check_watcher.update();
426 }
427
428 if state_changed {
429 update_file_notifications_on_threadpool(
430 pool,
431 world_state.snapshot(),
432 world_state.options.publish_decorations,
433 task_sender.clone(),
434 loop_state.subscriptions.subscriptions(),
435 )
436 }
437
438 let loop_duration = loop_start.elapsed();
439 if loop_duration > Duration::from_millis(100) {
440 log::error!("overly long loop turn: {:?}", loop_duration);
441 if env::var("RA_PROFILE").is_ok() {
442 show_message(
443 req::MessageType::Error,
444 format!("overly long loop turn: {:?}", loop_duration),
445 &connection.sender,
446 );
447 }
448 }
449
450 Ok(())
451}
452
453fn on_task(
454 task: Task,
455 msg_sender: &Sender<Message>,
456 pending_requests: &mut PendingRequests,
457 state: &mut WorldState,
458) {
459 match task {
460 Task::Respond(response) => {
461 if let Some(completed) = pending_requests.finish(&response.id) {
462 log::info!("handled req#{} in {:?}", completed.id, completed.duration);
463 state.complete_request(completed);
464 msg_sender.send(response.into()).unwrap();
465 }
466 }
467 Task::Notify(n) => {
468 msg_sender.send(n.into()).unwrap();
469 }
470 Task::Diagnostic(task) => on_diagnostic_task(task, msg_sender, state),
471 }
472}
473
474fn on_request(
475 world: &mut WorldState,
476 pending_requests: &mut PendingRequests,
477 pool: &ThreadPool,
478 task_sender: &Sender<Task>,
479 msg_sender: &Sender<Message>,
480 request_received: Instant,
481 req: Request,
482) -> Result<()> {
483 let mut pool_dispatcher = PoolDispatcher {
484 req: Some(req),
485 pool,
486 world,
487 task_sender,
488 msg_sender,
489 pending_requests,
490 request_received,
491 };
492 pool_dispatcher
493 .on_sync::<req::CollectGarbage>(|s, ()| Ok(s.collect_garbage()))?
494 .on_sync::<req::JoinLines>(|s, p| handlers::handle_join_lines(s.snapshot(), p))?
495 .on_sync::<req::OnEnter>(|s, p| handlers::handle_on_enter(s.snapshot(), p))?
496 .on_sync::<req::SelectionRangeRequest>(|s, p| {
497 handlers::handle_selection_range(s.snapshot(), p)
498 })?
499 .on_sync::<req::FindMatchingBrace>(|s, p| {
500 handlers::handle_find_matching_brace(s.snapshot(), p)
501 })?
502 .on::<req::AnalyzerStatus>(handlers::handle_analyzer_status)?
503 .on::<req::SyntaxTree>(handlers::handle_syntax_tree)?
504 .on::<req::ExpandMacro>(handlers::handle_expand_macro)?
505 .on::<req::OnTypeFormatting>(handlers::handle_on_type_formatting)?
506 .on::<req::DocumentSymbolRequest>(handlers::handle_document_symbol)?
507 .on::<req::WorkspaceSymbol>(handlers::handle_workspace_symbol)?
508 .on::<req::GotoDefinition>(handlers::handle_goto_definition)?
509 .on::<req::GotoImplementation>(handlers::handle_goto_implementation)?
510 .on::<req::GotoTypeDefinition>(handlers::handle_goto_type_definition)?
511 .on::<req::ParentModule>(handlers::handle_parent_module)?
512 .on::<req::Runnables>(handlers::handle_runnables)?
513 .on::<req::DecorationsRequest>(handlers::handle_decorations)?
514 .on::<req::Completion>(handlers::handle_completion)?
515 .on::<req::CodeActionRequest>(handlers::handle_code_action)?
516 .on::<req::CodeLensRequest>(handlers::handle_code_lens)?
517 .on::<req::CodeLensResolve>(handlers::handle_code_lens_resolve)?
518 .on::<req::FoldingRangeRequest>(handlers::handle_folding_range)?
519 .on::<req::SignatureHelpRequest>(handlers::handle_signature_help)?
520 .on::<req::HoverRequest>(handlers::handle_hover)?
521 .on::<req::PrepareRenameRequest>(handlers::handle_prepare_rename)?
522 .on::<req::Rename>(handlers::handle_rename)?
523 .on::<req::References>(handlers::handle_references)?
524 .on::<req::Formatting>(handlers::handle_formatting)?
525 .on::<req::DocumentHighlightRequest>(handlers::handle_document_highlight)?
526 .on::<req::InlayHints>(handlers::handle_inlay_hints)?
527 .on::<req::CallHierarchyPrepare>(handlers::handle_call_hierarchy_prepare)?
528 .on::<req::CallHierarchyIncomingCalls>(handlers::handle_call_hierarchy_incoming)?
529 .on::<req::CallHierarchyOutgoingCalls>(handlers::handle_call_hierarchy_outgoing)?
530 .on::<req::Ssr>(handlers::handle_ssr)?
531 .finish();
532 Ok(())
533}
534
535fn on_notification(
536 msg_sender: &Sender<Message>,
537 state: &mut WorldState,
538 pending_requests: &mut PendingRequests,
539 subs: &mut Subscriptions,
540 not: Notification,
541) -> Result<()> {
542 let not = match notification_cast::<req::Cancel>(not) {
543 Ok(params) => {
544 let id: RequestId = match params.id {
545 NumberOrString::Number(id) => id.into(),
546 NumberOrString::String(id) => id.into(),
547 };
548 if pending_requests.cancel(&id) {
549 let response = Response::new_err(
550 id,
551 ErrorCode::RequestCanceled as i32,
552 "canceled by client".to_string(),
553 );
554 msg_sender.send(response.into()).unwrap()
555 }
556 return Ok(());
557 }
558 Err(not) => not,
559 };
560 let not = match notification_cast::<req::DidOpenTextDocument>(not) {
561 Ok(params) => {
562 let uri = params.text_document.uri;
563 let path = uri.to_file_path().map_err(|()| format!("invalid uri: {}", uri))?;
564 if let Some(file_id) =
565 state.vfs.write().add_file_overlay(&path, params.text_document.text)
566 {
567 subs.add_sub(FileId(file_id.0));
568 }
569 return Ok(());
570 }
571 Err(not) => not,
572 };
573 let not = match notification_cast::<req::DidChangeTextDocument>(not) {
574 Ok(mut params) => {
575 let uri = params.text_document.uri;
576 let path = uri.to_file_path().map_err(|()| format!("invalid uri: {}", uri))?;
577 let text =
578 params.content_changes.pop().ok_or_else(|| "empty changes".to_string())?.text;
579 state.vfs.write().change_file_overlay(path.as_path(), text);
580 return Ok(());
581 }
582 Err(not) => not,
583 };
584 let not = match notification_cast::<req::DidSaveTextDocument>(not) {
585 Ok(_params) => {
586 state.check_watcher.update();
587 return Ok(());
588 }
589 Err(not) => not,
590 };
591 let not = match notification_cast::<req::DidCloseTextDocument>(not) {
592 Ok(params) => {
593 let uri = params.text_document.uri;
594 let path = uri.to_file_path().map_err(|()| format!("invalid uri: {}", uri))?;
595 if let Some(file_id) = state.vfs.write().remove_file_overlay(path.as_path()) {
596 subs.remove_sub(FileId(file_id.0));
597 }
598 let params =
599 req::PublishDiagnosticsParams { uri, diagnostics: Vec::new(), version: None };
600 let not = notification_new::<req::PublishDiagnostics>(params);
601 msg_sender.send(not.into()).unwrap();
602 return Ok(());
603 }
604 Err(not) => not,
605 };
606 let not = match notification_cast::<req::DidChangeConfiguration>(not) {
607 Ok(_params) => {
608 return Ok(());
609 }
610 Err(not) => not,
611 };
612 let not = match notification_cast::<req::DidChangeWatchedFiles>(not) {
613 Ok(params) => {
614 let mut vfs = state.vfs.write();
615 for change in params.changes {
616 let uri = change.uri;
617 let path = uri.to_file_path().map_err(|()| format!("invalid uri: {}", uri))?;
618 vfs.notify_changed(path)
619 }
620 return Ok(());
621 }
622 Err(not) => not,
623 };
624 log::error!("unhandled notification: {:?}", not);
625 Ok(())
626}
627
628fn on_check_task(
629 task: CheckTask,
630 world_state: &mut WorldState,
631 task_sender: &Sender<Task>,
632) -> Result<()> {
633 match task {
634 CheckTask::ClearDiagnostics => {
635 task_sender.send(Task::Diagnostic(DiagnosticTask::ClearCheck))?;
636 }
637
638 CheckTask::AddDiagnostic { url, diagnostic, fixes } => {
639 let path = url.to_file_path().map_err(|()| format!("invalid uri: {}", url))?;
640 let file_id = match world_state.vfs.read().path2file(&path) {
641 Some(file) => FileId(file.0),
642 None => {
643 log::error!("File with cargo diagnostic not found in VFS: {}", path.display());
644 return Ok(());
645 }
646 };
647
648 task_sender
649 .send(Task::Diagnostic(DiagnosticTask::AddCheck(file_id, diagnostic, fixes)))?;
650 }
651
652 CheckTask::Status(progress) => {
653 let params = req::ProgressParams {
654 token: req::ProgressToken::String("rustAnalyzer/cargoWatcher".to_string()),
655 value: req::ProgressParamsValue::WorkDone(progress),
656 };
657 let not = notification_new::<req::Progress>(params);
658 task_sender.send(Task::Notify(not)).unwrap();
659 }
660 };
661
662 Ok(())
663}
664
665fn on_diagnostic_task(task: DiagnosticTask, msg_sender: &Sender<Message>, state: &mut WorldState) {
666 let subscriptions = state.diagnostics.handle_task(task);
667
668 for file_id in subscriptions {
669 let path = state.vfs.read().file2path(VfsFile(file_id.0));
670 let uri = match url_from_path_with_drive_lowercasing(&path) {
671 Ok(uri) => uri,
672 Err(err) => {
673 log::error!("Couldn't convert path to url ({}): {:?}", err, path.to_string_lossy());
674 continue;
675 }
676 };
677
678 let diagnostics = state.diagnostics.diagnostics_for(file_id).cloned().collect();
679 let params = req::PublishDiagnosticsParams { uri, diagnostics, version: None };
680 let not = notification_new::<req::PublishDiagnostics>(params);
681 msg_sender.send(not.into()).unwrap();
682 }
683}
684
685struct PoolDispatcher<'a> {
686 req: Option<Request>,
687 pool: &'a ThreadPool,
688 world: &'a mut WorldState,
689 pending_requests: &'a mut PendingRequests,
690 msg_sender: &'a Sender<Message>,
691 task_sender: &'a Sender<Task>,
692 request_received: Instant,
693}
694
695impl<'a> PoolDispatcher<'a> {
696 /// Dispatches the request onto the current thread
697 fn on_sync<R>(
698 &mut self,
699 f: fn(&mut WorldState, R::Params) -> Result<R::Result>,
700 ) -> Result<&mut Self>
701 where
702 R: req::Request + 'static,
703 R::Params: DeserializeOwned + panic::UnwindSafe + 'static,
704 R::Result: Serialize + 'static,
705 {
706 let (id, params) = match self.parse::<R>() {
707 Some(it) => it,
708 None => {
709 return Ok(self);
710 }
711 };
712 let world = panic::AssertUnwindSafe(&mut *self.world);
713 let task = panic::catch_unwind(move || {
714 let result = f(world.0, params);
715 result_to_task::<R>(id, result)
716 })
717 .map_err(|_| format!("sync task {:?} panicked", R::METHOD))?;
718 on_task(task, self.msg_sender, self.pending_requests, self.world);
719 Ok(self)
720 }
721
722 /// Dispatches the request onto thread pool
723 fn on<R>(&mut self, f: fn(WorldSnapshot, R::Params) -> Result<R::Result>) -> Result<&mut Self>
724 where
725 R: req::Request + 'static,
726 R::Params: DeserializeOwned + Send + 'static,
727 R::Result: Serialize + 'static,
728 {
729 let (id, params) = match self.parse::<R>() {
730 Some(it) => it,
731 None => {
732 return Ok(self);
733 }
734 };
735
736 self.pool.execute({
737 let world = self.world.snapshot();
738 let sender = self.task_sender.clone();
739 move || {
740 let result = f(world, params);
741 let task = result_to_task::<R>(id, result);
742 sender.send(task).unwrap();
743 }
744 });
745
746 Ok(self)
747 }
748
749 fn parse<R>(&mut self) -> Option<(RequestId, R::Params)>
750 where
751 R: req::Request + 'static,
752 R::Params: DeserializeOwned + 'static,
753 {
754 let req = self.req.take()?;
755 let (id, params) = match req.extract::<R::Params>(R::METHOD) {
756 Ok(it) => it,
757 Err(req) => {
758 self.req = Some(req);
759 return None;
760 }
761 };
762 self.pending_requests.start(PendingRequest {
763 id: id.clone(),
764 method: R::METHOD.to_string(),
765 received: self.request_received,
766 });
767 Some((id, params))
768 }
769
770 fn finish(&mut self) {
771 match self.req.take() {
772 None => (),
773 Some(req) => {
774 log::error!("unknown request: {:?}", req);
775 let resp = Response::new_err(
776 req.id,
777 ErrorCode::MethodNotFound as i32,
778 "unknown request".to_string(),
779 );
780 self.msg_sender.send(resp.into()).unwrap();
781 }
782 }
783 }
784}
785
786fn result_to_task<R>(id: RequestId, result: Result<R::Result>) -> Task
787where
788 R: req::Request + 'static,
789 R::Params: DeserializeOwned + 'static,
790 R::Result: Serialize + 'static,
791{
792 let response = match result {
793 Ok(resp) => Response::new_ok(id, &resp),
794 Err(e) => match e.downcast::<LspError>() {
795 Ok(lsp_error) => Response::new_err(id, lsp_error.code, lsp_error.message),
796 Err(e) => {
797 if is_canceled(&e) {
798 Response::new_err(
799 id,
800 ErrorCode::ContentModified as i32,
801 "content modified".to_string(),
802 )
803 } else {
804 Response::new_err(id, ErrorCode::InternalError as i32, e.to_string())
805 }
806 }
807 },
808 };
809 Task::Respond(response)
810}
811
812fn update_file_notifications_on_threadpool(
813 pool: &ThreadPool,
814 world: WorldSnapshot,
815 publish_decorations: bool,
816 task_sender: Sender<Task>,
817 subscriptions: Vec<FileId>,
818) {
819 log::trace!("updating notifications for {:?}", subscriptions);
820 let publish_diagnostics = world.feature_flags().get("lsp.diagnostics");
821 pool.execute(move || {
822 for file_id in subscriptions {
823 if publish_diagnostics {
824 match handlers::publish_diagnostics(&world, file_id) {
825 Err(e) => {
826 if !is_canceled(&e) {
827 log::error!("failed to compute diagnostics: {:?}", e);
828 }
829 }
830 Ok(task) => {
831 task_sender.send(Task::Diagnostic(task)).unwrap();
832 }
833 }
834 }
835 if publish_decorations {
836 match handlers::publish_decorations(&world, file_id) {
837 Err(e) => {
838 if !is_canceled(&e) {
839 log::error!("failed to compute decorations: {:?}", e);
840 }
841 }
842 Ok(params) => {
843 let not = notification_new::<req::PublishDecorations>(params);
844 task_sender.send(Task::Notify(not)).unwrap();
845 }
846 }
847 }
848 }
849 });
850}
851
852pub fn show_message(typ: req::MessageType, message: impl Into<String>, sender: &Sender<Message>) {
853 let message = message.into();
854 let params = req::ShowMessageParams { typ, message };
855 let not = notification_new::<req::ShowMessage>(params);
856 sender.send(not.into()).unwrap();
857}
858
859fn is_canceled(e: &Box<dyn std::error::Error + Send + Sync>) -> bool {
860 e.downcast_ref::<Canceled>().is_some()
861}
862
863fn notification_is<N: lsp_types::notification::Notification>(notification: &Notification) -> bool {
864 notification.method == N::METHOD
865}
866
867fn notification_cast<N>(notification: Notification) -> std::result::Result<N::Params, Notification>
868where
869 N: lsp_types::notification::Notification,
870 N::Params: DeserializeOwned,
871{
872 notification.extract(N::METHOD)
873}
874
875fn notification_new<N>(params: N::Params) -> Notification
876where
877 N: lsp_types::notification::Notification,
878 N::Params: Serialize,
879{
880 Notification::new(N::METHOD.to_string(), params)
881}
882
883fn request_new<R>(id: RequestId, params: R::Params) -> Request
884where
885 R: lsp_types::request::Request,
886 R::Params: Serialize,
887{
888 Request::new(id, R::METHOD.to_string(), params)
889}
diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs
deleted file mode 100644
index ae51141cb..000000000
--- a/crates/ra_lsp_server/src/main_loop/handlers.rs
+++ /dev/null
@@ -1,1068 +0,0 @@
1//! This module is responsible for implementing handlers for Lanuage Server Protocol.
2//! The majority of requests are fulfilled by calling into the `ra_ide` crate.
3
4use std::{
5 collections::hash_map::Entry,
6 fmt::Write as _,
7 io::Write as _,
8 process::{self, Stdio},
9};
10
11use lsp_server::ErrorCode;
12use lsp_types::{
13 CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
14 CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams,
15 CodeAction, CodeActionOrCommand, CodeActionResponse, CodeLens, Command, CompletionItem,
16 Diagnostic, DocumentFormattingParams, DocumentHighlight, DocumentSymbol, FoldingRange,
17 FoldingRangeParams, Hover, HoverContents, Location, MarkupContent, MarkupKind, Position,
18 PrepareRenameResponse, Range, RenameParams, SymbolInformation, TextDocumentIdentifier,
19 TextEdit, WorkspaceEdit,
20};
21use ra_ide::{
22 AssistId, FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind,
23 SearchScope,
24};
25use ra_prof::profile;
26use ra_syntax::{AstNode, SyntaxKind, TextRange, TextUnit};
27use rustc_hash::FxHashMap;
28use serde::{Deserialize, Serialize};
29use serde_json::to_value;
30
31use crate::{
32 cargo_target_spec::{runnable_args, CargoTargetSpec},
33 conv::{
34 to_call_hierarchy_item, to_location, Conv, ConvWith, FoldConvCtx, MapConvWith, TryConvWith,
35 TryConvWithToVec,
36 },
37 diagnostics::DiagnosticTask,
38 from_json,
39 req::{self, Decoration, InlayHint, InlayHintsParams, InlayKind},
40 world::WorldSnapshot,
41 LspError, Result,
42};
43
44pub fn handle_analyzer_status(world: WorldSnapshot, _: ()) -> Result<String> {
45 let _p = profile("handle_analyzer_status");
46 let mut buf = world.status();
47 writeln!(buf, "\n\nrequests:").unwrap();
48 let requests = world.latest_requests.read();
49 for (is_last, r) in requests.iter() {
50 let mark = if is_last { "*" } else { " " };
51 writeln!(buf, "{}{:4} {:<36}{}ms", mark, r.id, r.method, r.duration.as_millis()).unwrap();
52 }
53 Ok(buf)
54}
55
56pub fn handle_syntax_tree(world: WorldSnapshot, params: req::SyntaxTreeParams) -> Result<String> {
57 let _p = profile("handle_syntax_tree");
58 let id = params.text_document.try_conv_with(&world)?;
59 let line_index = world.analysis().file_line_index(id)?;
60 let text_range = params.range.map(|p| p.conv_with(&line_index));
61 let res = world.analysis().syntax_tree(id, text_range)?;
62 Ok(res)
63}
64
65pub fn handle_expand_macro(
66 world: WorldSnapshot,
67 params: req::ExpandMacroParams,
68) -> Result<Option<req::ExpandedMacro>> {
69 let _p = profile("handle_expand_macro");
70 let file_id = params.text_document.try_conv_with(&world)?;
71 let line_index = world.analysis().file_line_index(file_id)?;
72 let offset = params.position.map(|p| p.conv_with(&line_index));
73
74 match offset {
75 None => Ok(None),
76 Some(offset) => {
77 let res = world.analysis().expand_macro(FilePosition { file_id, offset })?;
78 Ok(res.map(|it| req::ExpandedMacro { name: it.name, expansion: it.expansion }))
79 }
80 }
81}
82
83pub fn handle_selection_range(
84 world: WorldSnapshot,
85 params: req::SelectionRangeParams,
86) -> Result<Vec<req::SelectionRange>> {
87 let _p = profile("handle_selection_range");
88 let file_id = params.text_document.try_conv_with(&world)?;
89 let line_index = world.analysis().file_line_index(file_id)?;
90 params
91 .positions
92 .into_iter()
93 .map_conv_with(&line_index)
94 .map(|position| {
95 let mut ranges = Vec::new();
96 {
97 let mut range = TextRange::from_to(position, position);
98 loop {
99 ranges.push(range);
100 let frange = FileRange { file_id, range };
101 let next = world.analysis().extend_selection(frange)?;
102 if next == range {
103 break;
104 } else {
105 range = next
106 }
107 }
108 }
109 let mut range = req::SelectionRange {
110 range: ranges.last().unwrap().conv_with(&line_index),
111 parent: None,
112 };
113 for r in ranges.iter().rev().skip(1) {
114 range = req::SelectionRange {
115 range: r.conv_with(&line_index),
116 parent: Some(Box::new(range)),
117 }
118 }
119 Ok(range)
120 })
121 .collect()
122}
123
124pub fn handle_find_matching_brace(
125 world: WorldSnapshot,
126 params: req::FindMatchingBraceParams,
127) -> Result<Vec<Position>> {
128 let _p = profile("handle_find_matching_brace");
129 let file_id = params.text_document.try_conv_with(&world)?;
130 let line_index = world.analysis().file_line_index(file_id)?;
131 let res = params
132 .offsets
133 .into_iter()
134 .map_conv_with(&line_index)
135 .map(|offset| {
136 if let Ok(Some(matching_brace_offset)) =
137 world.analysis().matching_brace(FilePosition { file_id, offset })
138 {
139 matching_brace_offset
140 } else {
141 offset
142 }
143 })
144 .map_conv_with(&line_index)
145 .collect();
146 Ok(res)
147}
148
149pub fn handle_join_lines(
150 world: WorldSnapshot,
151 params: req::JoinLinesParams,
152) -> Result<req::SourceChange> {
153 let _p = profile("handle_join_lines");
154 let frange = (&params.text_document, params.range).try_conv_with(&world)?;
155 world.analysis().join_lines(frange)?.try_conv_with(&world)
156}
157
158pub fn handle_on_enter(
159 world: WorldSnapshot,
160 params: req::TextDocumentPositionParams,
161) -> Result<Option<req::SourceChange>> {
162 let _p = profile("handle_on_enter");
163 let position = params.try_conv_with(&world)?;
164 match world.analysis().on_enter(position)? {
165 None => Ok(None),
166 Some(edit) => Ok(Some(edit.try_conv_with(&world)?)),
167 }
168}
169
170// Don't forget to add new trigger characters to `ServerCapabilities` in `caps.rs`.
171pub fn handle_on_type_formatting(
172 world: WorldSnapshot,
173 params: req::DocumentOnTypeFormattingParams,
174) -> Result<Option<Vec<TextEdit>>> {
175 let _p = profile("handle_on_type_formatting");
176 let mut position = params.text_document_position.try_conv_with(&world)?;
177 let line_index = world.analysis().file_line_index(position.file_id)?;
178 let line_endings = world.file_line_endings(position.file_id);
179
180 // in `ra_ide`, the `on_type` invariant is that
181 // `text.char_at(position) == typed_char`.
182 position.offset -= TextUnit::of_char('.');
183 let char_typed = params.ch.chars().next().unwrap_or('\0');
184
185 // We have an assist that inserts ` ` after typing `->` in `fn foo() ->{`,
186 // but it requires precise cursor positioning to work, and one can't
187 // position the cursor with on_type formatting. So, let's just toggle this
188 // feature off here, hoping that we'll enable it one day, 😿.
189 if char_typed == '>' {
190 return Ok(None);
191 }
192
193 let edit = world.analysis().on_char_typed(position, char_typed)?;
194 let mut edit = match edit {
195 Some(it) => it,
196 None => return Ok(None),
197 };
198
199 // This should be a single-file edit
200 let edit = edit.source_file_edits.pop().unwrap();
201
202 let change: Vec<TextEdit> = edit.edit.conv_with((&line_index, line_endings));
203 Ok(Some(change))
204}
205
206pub fn handle_document_symbol(
207 world: WorldSnapshot,
208 params: req::DocumentSymbolParams,
209) -> Result<Option<req::DocumentSymbolResponse>> {
210 let _p = profile("handle_document_symbol");
211 let file_id = params.text_document.try_conv_with(&world)?;
212 let line_index = world.analysis().file_line_index(file_id)?;
213
214 let mut parents: Vec<(DocumentSymbol, Option<usize>)> = Vec::new();
215
216 for symbol in world.analysis().file_structure(file_id)? {
217 let doc_symbol = DocumentSymbol {
218 name: symbol.label,
219 detail: symbol.detail,
220 kind: symbol.kind.conv(),
221 deprecated: Some(symbol.deprecated),
222 range: symbol.node_range.conv_with(&line_index),
223 selection_range: symbol.navigation_range.conv_with(&line_index),
224 children: None,
225 };
226 parents.push((doc_symbol, symbol.parent));
227 }
228 let mut res = Vec::new();
229 while let Some((node, parent)) = parents.pop() {
230 match parent {
231 None => res.push(node),
232 Some(i) => {
233 let children = &mut parents[i].0.children;
234 if children.is_none() {
235 *children = Some(Vec::new());
236 }
237 children.as_mut().unwrap().push(node);
238 }
239 }
240 }
241
242 Ok(Some(res.into()))
243}
244
245pub fn handle_workspace_symbol(
246 world: WorldSnapshot,
247 params: req::WorkspaceSymbolParams,
248) -> Result<Option<Vec<SymbolInformation>>> {
249 let _p = profile("handle_workspace_symbol");
250 let all_symbols = params.query.contains('#');
251 let libs = params.query.contains('*');
252 let query = {
253 let query: String = params.query.chars().filter(|&c| c != '#' && c != '*').collect();
254 let mut q = Query::new(query);
255 if !all_symbols {
256 q.only_types();
257 }
258 if libs {
259 q.libs();
260 }
261 q.limit(128);
262 q
263 };
264 let mut res = exec_query(&world, query)?;
265 if res.is_empty() && !all_symbols {
266 let mut query = Query::new(params.query);
267 query.limit(128);
268 res = exec_query(&world, query)?;
269 }
270
271 return Ok(Some(res));
272
273 fn exec_query(world: &WorldSnapshot, query: Query) -> Result<Vec<SymbolInformation>> {
274 let mut res = Vec::new();
275 for nav in world.analysis().symbol_search(query)? {
276 let info = SymbolInformation {
277 name: nav.name().to_string(),
278 kind: nav.kind().conv(),
279 location: nav.try_conv_with(world)?,
280 container_name: nav.container_name().map(|v| v.to_string()),
281 deprecated: None,
282 };
283 res.push(info);
284 }
285 Ok(res)
286 }
287}
288
289pub fn handle_goto_definition(
290 world: WorldSnapshot,
291 params: req::TextDocumentPositionParams,
292) -> Result<Option<req::GotoDefinitionResponse>> {
293 let _p = profile("handle_goto_definition");
294 let position = params.try_conv_with(&world)?;
295 let nav_info = match world.analysis().goto_definition(position)? {
296 None => return Ok(None),
297 Some(it) => it,
298 };
299 let res = (position.file_id, nav_info).try_conv_with(&world)?;
300 Ok(Some(res))
301}
302
303pub fn handle_goto_implementation(
304 world: WorldSnapshot,
305 params: req::TextDocumentPositionParams,
306) -> Result<Option<req::GotoImplementationResponse>> {
307 let _p = profile("handle_goto_implementation");
308 let position = params.try_conv_with(&world)?;
309 let nav_info = match world.analysis().goto_implementation(position)? {
310 None => return Ok(None),
311 Some(it) => it,
312 };
313 let res = (position.file_id, nav_info).try_conv_with(&world)?;
314 Ok(Some(res))
315}
316
317pub fn handle_goto_type_definition(
318 world: WorldSnapshot,
319 params: req::TextDocumentPositionParams,
320) -> Result<Option<req::GotoTypeDefinitionResponse>> {
321 let _p = profile("handle_goto_type_definition");
322 let position = params.try_conv_with(&world)?;
323 let nav_info = match world.analysis().goto_type_definition(position)? {
324 None => return Ok(None),
325 Some(it) => it,
326 };
327 let res = (position.file_id, nav_info).try_conv_with(&world)?;
328 Ok(Some(res))
329}
330
331pub fn handle_parent_module(
332 world: WorldSnapshot,
333 params: req::TextDocumentPositionParams,
334) -> Result<Vec<Location>> {
335 let _p = profile("handle_parent_module");
336 let position = params.try_conv_with(&world)?;
337 world.analysis().parent_module(position)?.iter().try_conv_with_to_vec(&world)
338}
339
340pub fn handle_runnables(
341 world: WorldSnapshot,
342 params: req::RunnablesParams,
343) -> Result<Vec<req::Runnable>> {
344 let _p = profile("handle_runnables");
345 let file_id = params.text_document.try_conv_with(&world)?;
346 let line_index = world.analysis().file_line_index(file_id)?;
347 let offset = params.position.map(|it| it.conv_with(&line_index));
348 let mut res = Vec::new();
349 let workspace_root = world.workspace_root_for(file_id);
350 for runnable in world.analysis().runnables(file_id)? {
351 if let Some(offset) = offset {
352 if !runnable.range.contains_inclusive(offset) {
353 continue;
354 }
355 }
356 res.push(to_lsp_runnable(&world, file_id, runnable)?);
357 }
358 let mut check_args = vec!["check".to_string()];
359 let label;
360 match CargoTargetSpec::for_file(&world, file_id)? {
361 Some(spec) => {
362 label = format!("cargo check -p {}", spec.package);
363 spec.push_to(&mut check_args);
364 }
365 None => {
366 label = "cargo check --all".to_string();
367 check_args.push("--all".to_string())
368 }
369 }
370 // Always add `cargo check`.
371 res.push(req::Runnable {
372 range: Default::default(),
373 label,
374 bin: "cargo".to_string(),
375 args: check_args,
376 env: FxHashMap::default(),
377 cwd: workspace_root.map(|root| root.to_string_lossy().to_string()),
378 });
379 Ok(res)
380}
381
382pub fn handle_decorations(
383 world: WorldSnapshot,
384 params: TextDocumentIdentifier,
385) -> Result<Vec<Decoration>> {
386 let _p = profile("handle_decorations");
387 let file_id = params.try_conv_with(&world)?;
388 highlight(&world, file_id)
389}
390
391pub fn handle_completion(
392 world: WorldSnapshot,
393 params: req::CompletionParams,
394) -> Result<Option<req::CompletionResponse>> {
395 let _p = profile("handle_completion");
396 let position = params.text_document_position.try_conv_with(&world)?;
397 let completion_triggered_after_single_colon = {
398 let mut res = false;
399 if let Some(ctx) = params.context {
400 if ctx.trigger_character.unwrap_or_default() == ":" {
401 let source_file = world.analysis().parse(position.file_id)?;
402 let syntax = source_file.syntax();
403 let text = syntax.text();
404 if let Some(next_char) = text.char_at(position.offset) {
405 let diff = TextUnit::of_char(next_char) + TextUnit::of_char(':');
406 let prev_char = position.offset - diff;
407 if text.char_at(prev_char) != Some(':') {
408 res = true;
409 }
410 }
411 }
412 }
413 res
414 };
415 if completion_triggered_after_single_colon {
416 return Ok(None);
417 }
418
419 let items = match world.analysis().completions(position)? {
420 None => return Ok(None),
421 Some(items) => items,
422 };
423 let line_index = world.analysis().file_line_index(position.file_id)?;
424 let line_endings = world.file_line_endings(position.file_id);
425 let items: Vec<CompletionItem> =
426 items.into_iter().map(|item| item.conv_with((&line_index, line_endings))).collect();
427
428 Ok(Some(items.into()))
429}
430
431pub fn handle_folding_range(
432 world: WorldSnapshot,
433 params: FoldingRangeParams,
434) -> Result<Option<Vec<FoldingRange>>> {
435 let _p = profile("handle_folding_range");
436 let file_id = params.text_document.try_conv_with(&world)?;
437 let folds = world.analysis().folding_ranges(file_id)?;
438 let text = world.analysis().file_text(file_id)?;
439 let line_index = world.analysis().file_line_index(file_id)?;
440 let ctx = FoldConvCtx {
441 text: &text,
442 line_index: &line_index,
443 line_folding_only: world.options.line_folding_only,
444 };
445 let res = Some(folds.into_iter().map_conv_with(&ctx).collect());
446 Ok(res)
447}
448
449pub fn handle_signature_help(
450 world: WorldSnapshot,
451 params: req::TextDocumentPositionParams,
452) -> Result<Option<req::SignatureHelp>> {
453 let _p = profile("handle_signature_help");
454 let position = params.try_conv_with(&world)?;
455 if let Some(call_info) = world.analysis().call_info(position)? {
456 let active_parameter = call_info.active_parameter.map(|it| it as i64);
457 let sig_info = call_info.signature.conv();
458
459 Ok(Some(req::SignatureHelp {
460 signatures: vec![sig_info],
461 active_signature: Some(0),
462 active_parameter,
463 }))
464 } else {
465 Ok(None)
466 }
467}
468
469pub fn handle_hover(
470 world: WorldSnapshot,
471 params: req::TextDocumentPositionParams,
472) -> Result<Option<Hover>> {
473 let _p = profile("handle_hover");
474 let position = params.try_conv_with(&world)?;
475 let info = match world.analysis().hover(position)? {
476 None => return Ok(None),
477 Some(info) => info,
478 };
479 let line_index = world.analysis.file_line_index(position.file_id)?;
480 let range = info.range.conv_with(&line_index);
481 let res = Hover {
482 contents: HoverContents::Markup(MarkupContent {
483 kind: MarkupKind::Markdown,
484 value: crate::markdown::format_docs(&info.info.to_markup()),
485 }),
486 range: Some(range),
487 };
488 Ok(Some(res))
489}
490
491pub fn handle_prepare_rename(
492 world: WorldSnapshot,
493 params: req::TextDocumentPositionParams,
494) -> Result<Option<PrepareRenameResponse>> {
495 let _p = profile("handle_prepare_rename");
496 let position = params.try_conv_with(&world)?;
497
498 let optional_change = world.analysis().rename(position, "dummy")?;
499 let range = match optional_change {
500 None => return Ok(None),
501 Some(it) => it.range,
502 };
503
504 let file_id = params.text_document.try_conv_with(&world)?;
505 let line_index = world.analysis().file_line_index(file_id)?;
506 let range = range.conv_with(&line_index);
507 Ok(Some(PrepareRenameResponse::Range(range)))
508}
509
510pub fn handle_rename(world: WorldSnapshot, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
511 let _p = profile("handle_rename");
512 let position = params.text_document_position.try_conv_with(&world)?;
513
514 if params.new_name.is_empty() {
515 return Err(LspError::new(
516 ErrorCode::InvalidParams as i32,
517 "New Name cannot be empty".into(),
518 )
519 .into());
520 }
521
522 let optional_change = world.analysis().rename(position, &*params.new_name)?;
523 let change = match optional_change {
524 None => return Ok(None),
525 Some(it) => it.info,
526 };
527
528 let source_change_req = change.try_conv_with(&world)?;
529
530 Ok(Some(source_change_req.workspace_edit))
531}
532
533pub fn handle_references(
534 world: WorldSnapshot,
535 params: req::ReferenceParams,
536) -> Result<Option<Vec<Location>>> {
537 let _p = profile("handle_references");
538 let position = params.text_document_position.try_conv_with(&world)?;
539
540 let refs = match world.analysis().find_all_refs(position, None)? {
541 None => return Ok(None),
542 Some(refs) => refs,
543 };
544
545 let locations = if params.context.include_declaration {
546 refs.into_iter()
547 .filter_map(|reference| {
548 let line_index =
549 world.analysis().file_line_index(reference.file_range.file_id).ok()?;
550 to_location(
551 reference.file_range.file_id,
552 reference.file_range.range,
553 &world,
554 &line_index,
555 )
556 .ok()
557 })
558 .collect()
559 } else {
560 // Only iterate over the references if include_declaration was false
561 refs.references()
562 .iter()
563 .filter_map(|reference| {
564 let line_index =
565 world.analysis().file_line_index(reference.file_range.file_id).ok()?;
566 to_location(
567 reference.file_range.file_id,
568 reference.file_range.range,
569 &world,
570 &line_index,
571 )
572 .ok()
573 })
574 .collect()
575 };
576
577 Ok(Some(locations))
578}
579
580pub fn handle_formatting(
581 world: WorldSnapshot,
582 params: DocumentFormattingParams,
583) -> Result<Option<Vec<TextEdit>>> {
584 let _p = profile("handle_formatting");
585 let file_id = params.text_document.try_conv_with(&world)?;
586 let file = world.analysis().file_text(file_id)?;
587 let crate_ids = world.analysis().crate_for(file_id)?;
588
589 let file_line_index = world.analysis().file_line_index(file_id)?;
590 let end_position = TextUnit::of_str(&file).conv_with(&file_line_index);
591
592 let mut rustfmt = process::Command::new("rustfmt");
593 rustfmt.args(&world.options.rustfmt_args);
594 if let Some(&crate_id) = crate_ids.first() {
595 // Assume all crates are in the same edition
596 let edition = world.analysis().crate_edition(crate_id)?;
597 rustfmt.args(&["--edition", &edition.to_string()]);
598 }
599
600 if let Ok(path) = params.text_document.uri.to_file_path() {
601 if let Some(parent) = path.parent() {
602 rustfmt.current_dir(parent);
603 }
604 }
605 let mut rustfmt = rustfmt.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn()?;
606
607 rustfmt.stdin.as_mut().unwrap().write_all(file.as_bytes())?;
608
609 let output = rustfmt.wait_with_output()?;
610 let captured_stdout = String::from_utf8(output.stdout)?;
611
612 if !output.status.success() {
613 match output.status.code() {
614 Some(1) => {
615 // While `rustfmt` doesn't have a specific exit code for parse errors this is the
616 // likely cause exiting with 1. Most Language Servers swallow parse errors on
617 // formatting because otherwise an error is surfaced to the user on top of the
618 // syntax error diagnostics they're already receiving. This is especially jarring
619 // if they have format on save enabled.
620 log::info!("rustfmt exited with status 1, assuming parse error and ignoring");
621 return Ok(None);
622 }
623 _ => {
624 // Something else happened - e.g. `rustfmt` is missing or caught a signal
625 return Err(LspError::new(
626 -32900,
627 format!(
628 r#"rustfmt exited with:
629 Status: {}
630 stdout: {}"#,
631 output.status, captured_stdout,
632 ),
633 )
634 .into());
635 }
636 }
637 }
638
639 Ok(Some(vec![TextEdit {
640 range: Range::new(Position::new(0, 0), end_position),
641 new_text: captured_stdout,
642 }]))
643}
644
645pub fn handle_code_action(
646 world: WorldSnapshot,
647 params: req::CodeActionParams,
648) -> Result<Option<CodeActionResponse>> {
649 let _p = profile("handle_code_action");
650 let file_id = params.text_document.try_conv_with(&world)?;
651 let line_index = world.analysis().file_line_index(file_id)?;
652 let range = params.range.conv_with(&line_index);
653
654 let diagnostics = world.analysis().diagnostics(file_id)?;
655 let mut res = CodeActionResponse::default();
656
657 let fixes_from_diagnostics = diagnostics
658 .into_iter()
659 .filter_map(|d| Some((d.range, d.fix?)))
660 .filter(|(diag_range, _fix)| diag_range.intersection(&range).is_some())
661 .map(|(_range, fix)| fix);
662
663 for source_edit in fixes_from_diagnostics {
664 let title = source_edit.label.clone();
665 let edit = source_edit.try_conv_with(&world)?;
666
667 let command = Command {
668 title,
669 command: "rust-analyzer.applySourceChange".to_string(),
670 arguments: Some(vec![to_value(edit).unwrap()]),
671 };
672 let action = CodeAction {
673 title: command.title.clone(),
674 kind: None,
675 diagnostics: None,
676 edit: None,
677 command: Some(command),
678 is_preferred: None,
679 };
680 res.push(action.into());
681 }
682
683 for fix in world.check_fixes.get(&file_id).into_iter().flatten() {
684 let fix_range = fix.range.conv_with(&line_index);
685 if fix_range.intersection(&range).is_none() {
686 continue;
687 }
688 res.push(fix.action.clone());
689 }
690
691 let mut groups = FxHashMap::default();
692 for assist in world.analysis().assists(FileRange { file_id, range })?.into_iter() {
693 let arg = to_value(assist.source_change.try_conv_with(&world)?)?;
694
695 let (command, title, arg) = match assist.group_label {
696 None => ("rust-analyzer.applySourceChange", assist.label.clone(), arg),
697
698 // Group all assists with the same `group_label` into a single CodeAction.
699 Some(group_label) => {
700 match groups.entry(group_label.clone()) {
701 Entry::Occupied(entry) => {
702 let idx: usize = *entry.get();
703 match &mut res[idx] {
704 CodeActionOrCommand::CodeAction(CodeAction {
705 command: Some(Command { arguments: Some(arguments), .. }),
706 ..
707 }) => match arguments.as_mut_slice() {
708 [serde_json::Value::Array(arguments)] => arguments.push(arg),
709 _ => panic!("invalid group"),
710 },
711 _ => panic!("invalid group"),
712 }
713 continue;
714 }
715 Entry::Vacant(entry) => {
716 entry.insert(res.len());
717 }
718 }
719 ("rust-analyzer.selectAndApplySourceChange", group_label, to_value(vec![arg])?)
720 }
721 };
722
723 let command = Command {
724 title: assist.label.clone(),
725 command: command.to_string(),
726 arguments: Some(vec![arg]),
727 };
728
729 let kind = match assist.id {
730 AssistId("introduce_variable") => Some("refactor.extract.variable".to_string()),
731 AssistId("add_custom_impl") => Some("refactor.rewrite.add_custom_impl".to_string()),
732 _ => None,
733 };
734
735 let action = CodeAction {
736 title,
737 kind,
738 diagnostics: None,
739 edit: None,
740 command: Some(command),
741 is_preferred: None,
742 };
743 res.push(action.into());
744 }
745
746 Ok(Some(res))
747}
748
749pub fn handle_code_lens(
750 world: WorldSnapshot,
751 params: req::CodeLensParams,
752) -> Result<Option<Vec<CodeLens>>> {
753 let _p = profile("handle_code_lens");
754 let file_id = params.text_document.try_conv_with(&world)?;
755 let line_index = world.analysis().file_line_index(file_id)?;
756
757 let mut lenses: Vec<CodeLens> = Default::default();
758
759 // Gather runnables
760 for runnable in world.analysis().runnables(file_id)? {
761 let title = match &runnable.kind {
762 RunnableKind::Test { .. } | RunnableKind::TestMod { .. } => "▶️\u{fe0e}Run Test",
763 RunnableKind::Bench { .. } => "Run Bench",
764 RunnableKind::Bin => "Run",
765 }
766 .to_string();
767 let r = to_lsp_runnable(&world, file_id, runnable)?;
768 let lens = CodeLens {
769 range: r.range,
770 command: Some(Command {
771 title,
772 command: "rust-analyzer.runSingle".into(),
773 arguments: Some(vec![to_value(r).unwrap()]),
774 }),
775 data: None,
776 };
777
778 lenses.push(lens);
779 }
780
781 // Handle impls
782 lenses.extend(
783 world
784 .analysis()
785 .file_structure(file_id)?
786 .into_iter()
787 .filter(|it| match it.kind {
788 SyntaxKind::TRAIT_DEF | SyntaxKind::STRUCT_DEF | SyntaxKind::ENUM_DEF => true,
789 _ => false,
790 })
791 .map(|it| {
792 let range = it.node_range.conv_with(&line_index);
793 let pos = range.start;
794 let lens_params =
795 req::TextDocumentPositionParams::new(params.text_document.clone(), pos);
796 CodeLens {
797 range,
798 command: None,
799 data: Some(to_value(CodeLensResolveData::Impls(lens_params)).unwrap()),
800 }
801 }),
802 );
803
804 Ok(Some(lenses))
805}
806
807#[derive(Debug, Serialize, Deserialize)]
808#[serde(rename_all = "camelCase")]
809enum CodeLensResolveData {
810 Impls(req::TextDocumentPositionParams),
811}
812
813pub fn handle_code_lens_resolve(world: WorldSnapshot, code_lens: CodeLens) -> Result<CodeLens> {
814 let _p = profile("handle_code_lens_resolve");
815 let data = code_lens.data.unwrap();
816 let resolve = from_json::<Option<CodeLensResolveData>>("CodeLensResolveData", data)?;
817 match resolve {
818 Some(CodeLensResolveData::Impls(lens_params)) => {
819 let locations: Vec<Location> =
820 match handle_goto_implementation(world, lens_params.clone())? {
821 Some(req::GotoDefinitionResponse::Scalar(loc)) => vec![loc],
822 Some(req::GotoDefinitionResponse::Array(locs)) => locs,
823 Some(req::GotoDefinitionResponse::Link(links)) => links
824 .into_iter()
825 .map(|link| Location::new(link.target_uri, link.target_selection_range))
826 .collect(),
827 _ => vec![],
828 };
829
830 let title = if locations.len() == 1 {
831 "1 implementation".into()
832 } else {
833 format!("{} implementations", locations.len())
834 };
835
836 // We cannot use the 'editor.action.showReferences' command directly
837 // because that command requires vscode types which we convert in the handler
838 // on the client side.
839 let cmd = Command {
840 title,
841 command: "rust-analyzer.showReferences".into(),
842 arguments: Some(vec![
843 to_value(&lens_params.text_document.uri).unwrap(),
844 to_value(code_lens.range.start).unwrap(),
845 to_value(locations).unwrap(),
846 ]),
847 };
848 Ok(CodeLens { range: code_lens.range, command: Some(cmd), data: None })
849 }
850 None => Ok(CodeLens {
851 range: code_lens.range,
852 command: Some(Command { title: "Error".into(), ..Default::default() }),
853 data: None,
854 }),
855 }
856}
857
858pub fn handle_document_highlight(
859 world: WorldSnapshot,
860 params: req::TextDocumentPositionParams,
861) -> Result<Option<Vec<DocumentHighlight>>> {
862 let _p = profile("handle_document_highlight");
863 let file_id = params.text_document.try_conv_with(&world)?;
864 let line_index = world.analysis().file_line_index(file_id)?;
865
866 let refs = match world
867 .analysis()
868 .find_all_refs(params.try_conv_with(&world)?, Some(SearchScope::single_file(file_id)))?
869 {
870 None => return Ok(None),
871 Some(refs) => refs,
872 };
873
874 Ok(Some(
875 refs.into_iter()
876 .filter(|reference| reference.file_range.file_id == file_id)
877 .map(|reference| DocumentHighlight {
878 range: reference.file_range.range.conv_with(&line_index),
879 kind: reference.access.map(|it| it.conv()),
880 })
881 .collect(),
882 ))
883}
884
885pub fn handle_ssr(world: WorldSnapshot, params: req::SsrParams) -> Result<req::SourceChange> {
886 let _p = profile("handle_ssr");
887 world.analysis().structural_search_replace(&params.arg)??.try_conv_with(&world)
888}
889
890pub fn publish_diagnostics(world: &WorldSnapshot, file_id: FileId) -> Result<DiagnosticTask> {
891 let _p = profile("publish_diagnostics");
892 let line_index = world.analysis().file_line_index(file_id)?;
893 let diagnostics: Vec<Diagnostic> = world
894 .analysis()
895 .diagnostics(file_id)?
896 .into_iter()
897 .map(|d| Diagnostic {
898 range: d.range.conv_with(&line_index),
899 severity: Some(d.severity.conv()),
900 code: None,
901 source: Some("rust-analyzer".to_string()),
902 message: d.message,
903 related_information: None,
904 tags: None,
905 })
906 .collect();
907 Ok(DiagnosticTask::SetNative(file_id, diagnostics))
908}
909
910pub fn publish_decorations(
911 world: &WorldSnapshot,
912 file_id: FileId,
913) -> Result<req::PublishDecorationsParams> {
914 let _p = profile("publish_decorations");
915 let uri = world.file_id_to_uri(file_id)?;
916 Ok(req::PublishDecorationsParams { uri, decorations: highlight(&world, file_id)? })
917}
918
919fn to_lsp_runnable(
920 world: &WorldSnapshot,
921 file_id: FileId,
922 runnable: Runnable,
923) -> Result<req::Runnable> {
924 let args = runnable_args(world, file_id, &runnable.kind)?;
925 let line_index = world.analysis().file_line_index(file_id)?;
926 let label = match &runnable.kind {
927 RunnableKind::Test { test_id } => format!("test {}", test_id),
928 RunnableKind::TestMod { path } => format!("test-mod {}", path),
929 RunnableKind::Bench { test_id } => format!("bench {}", test_id),
930 RunnableKind::Bin => "run binary".to_string(),
931 };
932 Ok(req::Runnable {
933 range: runnable.range.conv_with(&line_index),
934 label,
935 bin: "cargo".to_string(),
936 args,
937 env: {
938 let mut m = FxHashMap::default();
939 m.insert("RUST_BACKTRACE".to_string(), "short".to_string());
940 m
941 },
942 cwd: world.workspace_root_for(file_id).map(|root| root.to_string_lossy().to_string()),
943 })
944}
945fn highlight(world: &WorldSnapshot, file_id: FileId) -> Result<Vec<Decoration>> {
946 let line_index = world.analysis().file_line_index(file_id)?;
947 let res = world
948 .analysis()
949 .highlight(file_id)?
950 .into_iter()
951 .map(|h| Decoration {
952 range: h.range.conv_with(&line_index),
953 tag: h.tag,
954 binding_hash: h.binding_hash.map(|x| x.to_string()),
955 })
956 .collect();
957 Ok(res)
958}
959
960pub fn handle_inlay_hints(
961 world: WorldSnapshot,
962 params: InlayHintsParams,
963) -> Result<Vec<InlayHint>> {
964 let _p = profile("handle_inlay_hints");
965 let file_id = params.text_document.try_conv_with(&world)?;
966 let analysis = world.analysis();
967 let line_index = analysis.file_line_index(file_id)?;
968 Ok(analysis
969 .inlay_hints(file_id, world.options.max_inlay_hint_length)?
970 .into_iter()
971 .map(|api_type| InlayHint {
972 label: api_type.label.to_string(),
973 range: api_type.range.conv_with(&line_index),
974 kind: match api_type.kind {
975 ra_ide::InlayKind::TypeHint => InlayKind::TypeHint,
976 ra_ide::InlayKind::ParameterHint => InlayKind::ParameterHint,
977 },
978 })
979 .collect())
980}
981
982pub fn handle_call_hierarchy_prepare(
983 world: WorldSnapshot,
984 params: CallHierarchyPrepareParams,
985) -> Result<Option<Vec<CallHierarchyItem>>> {
986 let _p = profile("handle_call_hierarchy_prepare");
987 let position = params.text_document_position_params.try_conv_with(&world)?;
988 let file_id = position.file_id;
989
990 let nav_info = match world.analysis().call_hierarchy(position)? {
991 None => return Ok(None),
992 Some(it) => it,
993 };
994
995 let line_index = world.analysis().file_line_index(file_id)?;
996 let RangeInfo { range, info: navs } = nav_info;
997 let res = navs
998 .into_iter()
999 .filter(|it| it.kind() == SyntaxKind::FN_DEF)
1000 .filter_map(|it| to_call_hierarchy_item(file_id, range, &world, &line_index, it).ok())
1001 .collect();
1002
1003 Ok(Some(res))
1004}
1005
1006pub fn handle_call_hierarchy_incoming(
1007 world: WorldSnapshot,
1008 params: CallHierarchyIncomingCallsParams,
1009) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
1010 let _p = profile("handle_call_hierarchy_incoming");
1011 let item = params.item;
1012
1013 let doc = TextDocumentIdentifier::new(item.uri);
1014 let frange: FileRange = (&doc, item.range).try_conv_with(&world)?;
1015 let fpos = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
1016
1017 let call_items = match world.analysis().incoming_calls(fpos)? {
1018 None => return Ok(None),
1019 Some(it) => it,
1020 };
1021
1022 let mut res = vec![];
1023
1024 for call_item in call_items.into_iter() {
1025 let file_id = call_item.target.file_id();
1026 let line_index = world.analysis().file_line_index(file_id)?;
1027 let range = call_item.target.range();
1028 let item = to_call_hierarchy_item(file_id, range, &world, &line_index, call_item.target)?;
1029 res.push(CallHierarchyIncomingCall {
1030 from: item,
1031 from_ranges: call_item.ranges.iter().map(|it| it.conv_with(&line_index)).collect(),
1032 });
1033 }
1034
1035 Ok(Some(res))
1036}
1037
1038pub fn handle_call_hierarchy_outgoing(
1039 world: WorldSnapshot,
1040 params: CallHierarchyOutgoingCallsParams,
1041) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
1042 let _p = profile("handle_call_hierarchy_outgoing");
1043 let item = params.item;
1044
1045 let doc = TextDocumentIdentifier::new(item.uri);
1046 let frange: FileRange = (&doc, item.range).try_conv_with(&world)?;
1047 let fpos = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
1048
1049 let call_items = match world.analysis().outgoing_calls(fpos)? {
1050 None => return Ok(None),
1051 Some(it) => it,
1052 };
1053
1054 let mut res = vec![];
1055
1056 for call_item in call_items.into_iter() {
1057 let file_id = call_item.target.file_id();
1058 let line_index = world.analysis().file_line_index(file_id)?;
1059 let range = call_item.target.range();
1060 let item = to_call_hierarchy_item(file_id, range, &world, &line_index, call_item.target)?;
1061 res.push(CallHierarchyOutgoingCall {
1062 to: item,
1063 from_ranges: call_item.ranges.iter().map(|it| it.conv_with(&line_index)).collect(),
1064 });
1065 }
1066
1067 Ok(Some(res))
1068}
diff --git a/crates/ra_lsp_server/src/main_loop/pending_requests.rs b/crates/ra_lsp_server/src/main_loop/pending_requests.rs
deleted file mode 100644
index 2d2213464..000000000
--- a/crates/ra_lsp_server/src/main_loop/pending_requests.rs
+++ /dev/null
@@ -1,75 +0,0 @@
1//! Datastructures that keep track of inflight requests.
2
3use std::time::{Duration, Instant};
4
5use lsp_server::RequestId;
6use rustc_hash::FxHashMap;
7
8#[derive(Debug)]
9pub struct CompletedRequest {
10 pub id: RequestId,
11 pub method: String,
12 pub duration: Duration,
13}
14
15#[derive(Debug)]
16pub(crate) struct PendingRequest {
17 pub(crate) id: RequestId,
18 pub(crate) method: String,
19 pub(crate) received: Instant,
20}
21
22impl From<PendingRequest> for CompletedRequest {
23 fn from(pending: PendingRequest) -> CompletedRequest {
24 CompletedRequest {
25 id: pending.id,
26 method: pending.method,
27 duration: pending.received.elapsed(),
28 }
29 }
30}
31
32#[derive(Debug, Default)]
33pub(crate) struct PendingRequests {
34 map: FxHashMap<RequestId, PendingRequest>,
35}
36
37impl PendingRequests {
38 pub(crate) fn start(&mut self, request: PendingRequest) {
39 let id = request.id.clone();
40 let prev = self.map.insert(id.clone(), request);
41 assert!(prev.is_none(), "duplicate request with id {}", id);
42 }
43 pub(crate) fn cancel(&mut self, id: &RequestId) -> bool {
44 self.map.remove(id).is_some()
45 }
46 pub(crate) fn finish(&mut self, id: &RequestId) -> Option<CompletedRequest> {
47 self.map.remove(id).map(CompletedRequest::from)
48 }
49}
50
51const N_COMPLETED_REQUESTS: usize = 10;
52
53#[derive(Debug, Default)]
54pub struct LatestRequests {
55 // hand-rolling VecDeque here to print things in a nicer way
56 buf: [Option<CompletedRequest>; N_COMPLETED_REQUESTS],
57 idx: usize,
58}
59
60impl LatestRequests {
61 pub(crate) fn record(&mut self, request: CompletedRequest) {
62 // special case: don't track status request itself
63 if request.method == "rust-analyzer/analyzerStatus" {
64 return;
65 }
66 let idx = self.idx;
67 self.buf[idx] = Some(request);
68 self.idx = (idx + 1) % N_COMPLETED_REQUESTS;
69 }
70
71 pub(crate) fn iter(&self) -> impl Iterator<Item = (bool, &CompletedRequest)> {
72 let idx = self.idx;
73 self.buf.iter().enumerate().filter_map(move |(i, req)| Some((i == idx, req.as_ref()?)))
74 }
75}
diff --git a/crates/ra_lsp_server/src/main_loop/subscriptions.rs b/crates/ra_lsp_server/src/main_loop/subscriptions.rs
deleted file mode 100644
index b0bae90f5..000000000
--- a/crates/ra_lsp_server/src/main_loop/subscriptions.rs
+++ /dev/null
@@ -1,21 +0,0 @@
1//! Keeps track of file subscriptions.
2
3use ra_ide::FileId;
4use rustc_hash::FxHashSet;
5
6#[derive(Default, Debug)]
7pub(crate) struct Subscriptions {
8 subs: FxHashSet<FileId>,
9}
10
11impl Subscriptions {
12 pub(crate) fn add_sub(&mut self, file_id: FileId) {
13 self.subs.insert(file_id);
14 }
15 pub(crate) fn remove_sub(&mut self, file_id: FileId) {
16 self.subs.remove(&file_id);
17 }
18 pub(crate) fn subscriptions(&self) -> Vec<FileId> {
19 self.subs.iter().cloned().collect()
20 }
21}
diff --git a/crates/ra_lsp_server/src/markdown.rs b/crates/ra_lsp_server/src/markdown.rs
deleted file mode 100644
index 76bef45cc..000000000
--- a/crates/ra_lsp_server/src/markdown.rs
+++ /dev/null
@@ -1,75 +0,0 @@
1//! Transforms markdown
2
3pub(crate) fn format_docs(src: &str) -> String {
4 let mut processed_lines = Vec::new();
5 let mut in_code_block = false;
6 for line in src.lines() {
7 if in_code_block && code_line_ignored_by_rustdoc(line) {
8 continue;
9 }
10
11 if line.starts_with("```") {
12 in_code_block ^= true
13 }
14
15 let line = if in_code_block && line.starts_with("```") && !line.contains("rust") {
16 "```rust"
17 } else {
18 line
19 };
20
21 processed_lines.push(line);
22 }
23 processed_lines.join("\n")
24}
25
26fn code_line_ignored_by_rustdoc(line: &str) -> bool {
27 let trimmed = line.trim();
28 trimmed == "#" || trimmed.starts_with("# ") || trimmed.starts_with("#\t")
29}
30
31#[cfg(test)]
32mod tests {
33 use super::*;
34
35 #[test]
36 fn test_format_docs_adds_rust() {
37 let comment = "```\nfn some_rust() {}\n```";
38 assert_eq!(format_docs(comment), "```rust\nfn some_rust() {}\n```");
39 }
40
41 #[test]
42 fn test_format_docs_skips_comments_in_rust_block() {
43 let comment =
44 "```rust\n # skip1\n# skip2\n#stay1\nstay2\n#\n #\n # \n #\tskip3\n\t#\t\n```";
45 assert_eq!(format_docs(comment), "```rust\n#stay1\nstay2\n```");
46 }
47
48 #[test]
49 fn test_format_docs_keeps_comments_outside_of_rust_block() {
50 let comment = " # stay1\n# stay2\n#stay3\nstay4\n#\n #\n # \n #\tstay5\n\t#\t";
51 assert_eq!(format_docs(comment), comment);
52 }
53
54 #[test]
55 fn test_format_docs_preserves_newlines() {
56 let comment = "this\nis\nultiline";
57 assert_eq!(format_docs(comment), comment);
58 }
59
60 #[test]
61 fn test_code_blocks_in_comments_marked_as_rust() {
62 let comment = r#"```rust
63fn main(){}
64```
65Some comment.
66```
67let a = 1;
68```"#;
69
70 assert_eq!(
71 format_docs(comment),
72 "```rust\nfn main(){}\n```\nSome comment.\n```rust\nlet a = 1;\n```"
73 );
74 }
75}
diff --git a/crates/ra_lsp_server/src/req.rs b/crates/ra_lsp_server/src/req.rs
deleted file mode 100644
index 7ff7f60b3..000000000
--- a/crates/ra_lsp_server/src/req.rs
+++ /dev/null
@@ -1,221 +0,0 @@
1//! Defines `rust-analyzer` specific custom messages.
2
3use lsp_types::{Location, Position, Range, TextDocumentIdentifier, Url};
4use rustc_hash::FxHashMap;
5use serde::{Deserialize, Serialize};
6
7pub use lsp_types::{
8 notification::*, request::*, ApplyWorkspaceEditParams, CodeActionParams, CodeLens,
9 CodeLensParams, CompletionParams, CompletionResponse, DiagnosticTag,
10 DidChangeConfigurationParams, DidChangeWatchedFilesParams,
11 DidChangeWatchedFilesRegistrationOptions, DocumentOnTypeFormattingParams, DocumentSymbolParams,
12 DocumentSymbolResponse, FileSystemWatcher, Hover, InitializeResult, MessageType,
13 PartialResultParams, ProgressParams, ProgressParamsValue, ProgressToken,
14 PublishDiagnosticsParams, ReferenceParams, Registration, RegistrationParams, SelectionRange,
15 SelectionRangeParams, ServerCapabilities, ShowMessageParams, SignatureHelp, SymbolKind,
16 TextDocumentEdit, TextDocumentPositionParams, TextEdit, WorkDoneProgressParams, WorkspaceEdit,
17 WorkspaceSymbolParams,
18};
19
20pub enum AnalyzerStatus {}
21
22impl Request for AnalyzerStatus {
23 type Params = ();
24 type Result = String;
25 const METHOD: &'static str = "rust-analyzer/analyzerStatus";
26}
27
28pub enum CollectGarbage {}
29
30impl Request for CollectGarbage {
31 type Params = ();
32 type Result = ();
33 const METHOD: &'static str = "rust-analyzer/collectGarbage";
34}
35
36pub enum SyntaxTree {}
37
38impl Request for SyntaxTree {
39 type Params = SyntaxTreeParams;
40 type Result = String;
41 const METHOD: &'static str = "rust-analyzer/syntaxTree";
42}
43
44#[derive(Deserialize, Debug)]
45#[serde(rename_all = "camelCase")]
46pub struct SyntaxTreeParams {
47 pub text_document: TextDocumentIdentifier,
48 pub range: Option<Range>,
49}
50
51#[derive(Serialize, Debug)]
52#[serde(rename_all = "camelCase")]
53pub struct ExpandedMacro {
54 pub name: String,
55 pub expansion: String,
56}
57
58pub enum ExpandMacro {}
59
60impl Request for ExpandMacro {
61 type Params = ExpandMacroParams;
62 type Result = Option<ExpandedMacro>;
63 const METHOD: &'static str = "rust-analyzer/expandMacro";
64}
65
66#[derive(Deserialize, Debug)]
67#[serde(rename_all = "camelCase")]
68pub struct ExpandMacroParams {
69 pub text_document: TextDocumentIdentifier,
70 pub position: Option<Position>,
71}
72
73pub enum FindMatchingBrace {}
74
75impl Request for FindMatchingBrace {
76 type Params = FindMatchingBraceParams;
77 type Result = Vec<Position>;
78 const METHOD: &'static str = "rust-analyzer/findMatchingBrace";
79}
80
81#[derive(Deserialize, Debug)]
82#[serde(rename_all = "camelCase")]
83pub struct FindMatchingBraceParams {
84 pub text_document: TextDocumentIdentifier,
85 pub offsets: Vec<Position>,
86}
87
88pub enum DecorationsRequest {}
89
90impl Request for DecorationsRequest {
91 type Params = TextDocumentIdentifier;
92 type Result = Vec<Decoration>;
93 const METHOD: &'static str = "rust-analyzer/decorationsRequest";
94}
95
96pub enum PublishDecorations {}
97
98impl Notification for PublishDecorations {
99 type Params = PublishDecorationsParams;
100 const METHOD: &'static str = "rust-analyzer/publishDecorations";
101}
102
103#[derive(Serialize, Debug)]
104#[serde(rename_all = "camelCase")]
105pub struct PublishDecorationsParams {
106 pub uri: Url,
107 pub decorations: Vec<Decoration>,
108}
109
110#[derive(Serialize, Debug)]
111#[serde(rename_all = "camelCase")]
112pub struct Decoration {
113 pub range: Range,
114 pub tag: &'static str,
115 pub binding_hash: Option<String>,
116}
117
118pub enum ParentModule {}
119
120impl Request for ParentModule {
121 type Params = TextDocumentPositionParams;
122 type Result = Vec<Location>;
123 const METHOD: &'static str = "rust-analyzer/parentModule";
124}
125
126pub enum JoinLines {}
127
128impl Request for JoinLines {
129 type Params = JoinLinesParams;
130 type Result = SourceChange;
131 const METHOD: &'static str = "rust-analyzer/joinLines";
132}
133
134#[derive(Deserialize, Debug)]
135#[serde(rename_all = "camelCase")]
136pub struct JoinLinesParams {
137 pub text_document: TextDocumentIdentifier,
138 pub range: Range,
139}
140
141pub enum OnEnter {}
142
143impl Request for OnEnter {
144 type Params = TextDocumentPositionParams;
145 type Result = Option<SourceChange>;
146 const METHOD: &'static str = "rust-analyzer/onEnter";
147}
148
149pub enum Runnables {}
150
151impl Request for Runnables {
152 type Params = RunnablesParams;
153 type Result = Vec<Runnable>;
154 const METHOD: &'static str = "rust-analyzer/runnables";
155}
156
157#[derive(Serialize, Deserialize, Debug)]
158#[serde(rename_all = "camelCase")]
159pub struct RunnablesParams {
160 pub text_document: TextDocumentIdentifier,
161 pub position: Option<Position>,
162}
163
164#[derive(Serialize, Debug)]
165#[serde(rename_all = "camelCase")]
166pub struct Runnable {
167 pub range: Range,
168 pub label: String,
169 pub bin: String,
170 pub args: Vec<String>,
171 pub env: FxHashMap<String, String>,
172 pub cwd: Option<String>,
173}
174
175#[derive(Serialize, Debug)]
176#[serde(rename_all = "camelCase")]
177pub struct SourceChange {
178 pub label: String,
179 pub workspace_edit: WorkspaceEdit,
180 pub cursor_position: Option<TextDocumentPositionParams>,
181}
182
183pub enum InlayHints {}
184
185impl Request for InlayHints {
186 type Params = InlayHintsParams;
187 type Result = Vec<InlayHint>;
188 const METHOD: &'static str = "rust-analyzer/inlayHints";
189}
190
191#[derive(Serialize, Deserialize, Debug)]
192#[serde(rename_all = "camelCase")]
193pub struct InlayHintsParams {
194 pub text_document: TextDocumentIdentifier,
195}
196
197#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)]
198pub enum InlayKind {
199 TypeHint,
200 ParameterHint,
201}
202
203#[derive(Debug, Deserialize, Serialize)]
204pub struct InlayHint {
205 pub range: Range,
206 pub kind: InlayKind,
207 pub label: String,
208}
209
210pub enum Ssr {}
211
212impl Request for Ssr {
213 type Params = SsrParams;
214 type Result = SourceChange;
215 const METHOD: &'static str = "rust-analyzer/ssr";
216}
217
218#[derive(Debug, Deserialize, Serialize)]
219pub struct SsrParams {
220 pub arg: String,
221}
diff --git a/crates/ra_lsp_server/src/vfs_glob.rs b/crates/ra_lsp_server/src/vfs_glob.rs
deleted file mode 100644
index 12401d75a..000000000
--- a/crates/ra_lsp_server/src/vfs_glob.rs
+++ /dev/null
@@ -1,94 +0,0 @@
1//! `ra_vfs_glob` crate implements exclusion rules for vfs.
2//!
3//! By default, we include only `.rs` files, and skip some know offenders like
4//! `/target` or `/node_modules` altogether.
5//!
6//! It's also possible to add custom exclusion globs.
7
8use globset::{GlobSet, GlobSetBuilder};
9use ra_vfs::{Filter, RelativePath};
10
11pub use globset::{Glob, GlobBuilder};
12
13const ALWAYS_IGNORED: &[&str] = &["target/**", "**/node_modules/**", "**/.git/**"];
14const IGNORED_FOR_NON_MEMBERS: &[&str] = &["examples/**", "tests/**", "benches/**"];
15
16pub struct RustPackageFilterBuilder {
17 is_member: bool,
18 exclude: GlobSetBuilder,
19}
20
21impl Default for RustPackageFilterBuilder {
22 fn default() -> RustPackageFilterBuilder {
23 RustPackageFilterBuilder { is_member: false, exclude: GlobSetBuilder::new() }
24 }
25}
26
27impl RustPackageFilterBuilder {
28 pub fn set_member(mut self, is_member: bool) -> RustPackageFilterBuilder {
29 self.is_member = is_member;
30 self
31 }
32 pub fn exclude(mut self, glob: Glob) -> RustPackageFilterBuilder {
33 self.exclude.add(glob);
34 self
35 }
36 pub fn into_vfs_filter(self) -> Box<dyn Filter> {
37 let RustPackageFilterBuilder { is_member, mut exclude } = self;
38 for &glob in ALWAYS_IGNORED {
39 exclude.add(Glob::new(glob).unwrap());
40 }
41 if !is_member {
42 for &glob in IGNORED_FOR_NON_MEMBERS {
43 exclude.add(Glob::new(glob).unwrap());
44 }
45 }
46 Box::new(RustPackageFilter { exclude: exclude.build().unwrap() })
47 }
48}
49
50struct RustPackageFilter {
51 exclude: GlobSet,
52}
53
54impl Filter for RustPackageFilter {
55 fn include_dir(&self, dir_path: &RelativePath) -> bool {
56 !self.exclude.is_match(dir_path.as_str())
57 }
58
59 fn include_file(&self, file_path: &RelativePath) -> bool {
60 file_path.extension() == Some("rs")
61 }
62}
63
64#[test]
65fn test_globs() {
66 let filter = RustPackageFilterBuilder::default().set_member(true).into_vfs_filter();
67
68 assert!(filter.include_dir(RelativePath::new("src/tests")));
69 assert!(filter.include_dir(RelativePath::new("src/target")));
70 assert!(filter.include_dir(RelativePath::new("tests")));
71 assert!(filter.include_dir(RelativePath::new("benches")));
72
73 assert!(!filter.include_dir(RelativePath::new("target")));
74 assert!(!filter.include_dir(RelativePath::new("src/foo/.git")));
75 assert!(!filter.include_dir(RelativePath::new("foo/node_modules")));
76
77 let filter = RustPackageFilterBuilder::default().set_member(false).into_vfs_filter();
78
79 assert!(filter.include_dir(RelativePath::new("src/tests")));
80 assert!(filter.include_dir(RelativePath::new("src/target")));
81
82 assert!(!filter.include_dir(RelativePath::new("target")));
83 assert!(!filter.include_dir(RelativePath::new("src/foo/.git")));
84 assert!(!filter.include_dir(RelativePath::new("foo/node_modules")));
85 assert!(!filter.include_dir(RelativePath::new("tests")));
86 assert!(!filter.include_dir(RelativePath::new("benches")));
87
88 let filter = RustPackageFilterBuilder::default()
89 .set_member(true)
90 .exclude(Glob::new("src/llvm-project/**").unwrap())
91 .into_vfs_filter();
92
93 assert!(!filter.include_dir(RelativePath::new("src/llvm-project/clang")));
94}
diff --git a/crates/ra_lsp_server/src/world.rs b/crates/ra_lsp_server/src/world.rs
deleted file mode 100644
index 71c95d4af..000000000
--- a/crates/ra_lsp_server/src/world.rs
+++ /dev/null
@@ -1,314 +0,0 @@
1//! The context or environment in which the language server functions.
2//! In our server implementation this is know as the `WorldState`.
3//!
4//! Each tick provides an immutable snapshot of the state as `WorldSnapshot`.
5
6use std::{
7 path::{Path, PathBuf},
8 sync::Arc,
9};
10
11use crossbeam_channel::{unbounded, Receiver};
12use lsp_server::ErrorCode;
13use lsp_types::Url;
14use parking_lot::RwLock;
15use ra_cargo_watch::{url_from_path_with_drive_lowercasing, CheckOptions, CheckWatcher};
16use ra_ide::{
17 Analysis, AnalysisChange, AnalysisHost, CrateGraph, FeatureFlags, FileId, LibraryData,
18 SourceRootId,
19};
20use ra_project_model::{get_rustc_cfg_options, ProjectWorkspace};
21use ra_vfs::{LineEndings, RootEntry, Vfs, VfsChange, VfsFile, VfsRoot, VfsTask, Watch};
22use relative_path::RelativePathBuf;
23
24use crate::{
25 diagnostics::{CheckFixes, DiagnosticCollection},
26 main_loop::pending_requests::{CompletedRequest, LatestRequests},
27 vfs_glob::{Glob, RustPackageFilterBuilder},
28 LspError, Result,
29};
30
31#[derive(Debug, Clone)]
32pub struct Options {
33 pub publish_decorations: bool,
34 pub supports_location_link: bool,
35 pub line_folding_only: bool,
36 pub max_inlay_hint_length: Option<usize>,
37 pub rustfmt_args: Vec<String>,
38 pub cargo_watch: CheckOptions,
39}
40
41/// `WorldState` is the primary mutable state of the language server
42///
43/// The most interesting components are `vfs`, which stores a consistent
44/// snapshot of the file systems, and `analysis_host`, which stores our
45/// incremental salsa database.
46#[derive(Debug)]
47pub struct WorldState {
48 pub options: Options,
49 //FIXME: this belongs to `LoopState` rather than to `WorldState`
50 pub roots_to_scan: usize,
51 pub roots: Vec<PathBuf>,
52 pub workspaces: Arc<Vec<ProjectWorkspace>>,
53 pub analysis_host: AnalysisHost,
54 pub vfs: Arc<RwLock<Vfs>>,
55 pub task_receiver: Receiver<VfsTask>,
56 pub latest_requests: Arc<RwLock<LatestRequests>>,
57 pub check_watcher: CheckWatcher,
58 pub diagnostics: DiagnosticCollection,
59}
60
61/// An immutable snapshot of the world's state at a point in time.
62pub struct WorldSnapshot {
63 pub options: Options,
64 pub workspaces: Arc<Vec<ProjectWorkspace>>,
65 pub analysis: Analysis,
66 pub latest_requests: Arc<RwLock<LatestRequests>>,
67 pub check_fixes: CheckFixes,
68 vfs: Arc<RwLock<Vfs>>,
69}
70
71impl WorldState {
72 pub fn new(
73 folder_roots: Vec<PathBuf>,
74 workspaces: Vec<ProjectWorkspace>,
75 lru_capacity: Option<usize>,
76 exclude_globs: &[Glob],
77 watch: Watch,
78 options: Options,
79 feature_flags: FeatureFlags,
80 ) -> WorldState {
81 let mut change = AnalysisChange::new();
82
83 let mut roots = Vec::new();
84 roots.extend(folder_roots.iter().map(|path| {
85 let mut filter = RustPackageFilterBuilder::default().set_member(true);
86 for glob in exclude_globs.iter() {
87 filter = filter.exclude(glob.clone());
88 }
89 RootEntry::new(path.clone(), filter.into_vfs_filter())
90 }));
91 for ws in workspaces.iter() {
92 roots.extend(ws.to_roots().into_iter().map(|pkg_root| {
93 let mut filter =
94 RustPackageFilterBuilder::default().set_member(pkg_root.is_member());
95 for glob in exclude_globs.iter() {
96 filter = filter.exclude(glob.clone());
97 }
98 RootEntry::new(pkg_root.path().clone(), filter.into_vfs_filter())
99 }));
100 }
101 let (task_sender, task_receiver) = unbounded();
102 let task_sender = Box::new(move |t| task_sender.send(t).unwrap());
103 let (mut vfs, vfs_roots) = Vfs::new(roots, task_sender, watch);
104 let roots_to_scan = vfs_roots.len();
105 for r in vfs_roots {
106 let vfs_root_path = vfs.root2path(r);
107 let is_local = folder_roots.iter().any(|it| vfs_root_path.starts_with(it));
108 change.add_root(SourceRootId(r.0), is_local);
109 change.set_debug_root_path(SourceRootId(r.0), vfs_root_path.display().to_string());
110 }
111
112 // FIXME: Read default cfgs from config
113 let default_cfg_options = {
114 let mut opts = get_rustc_cfg_options();
115 opts.insert_atom("test".into());
116 opts.insert_atom("debug_assertion".into());
117 opts
118 };
119
120 // Create crate graph from all the workspaces
121 let mut crate_graph = CrateGraph::default();
122 let mut load = |path: &std::path::Path| {
123 let vfs_file = vfs.load(path);
124 vfs_file.map(|f| FileId(f.0))
125 };
126 for ws in workspaces.iter() {
127 let (graph, crate_names) = ws.to_crate_graph(&default_cfg_options, &mut load);
128 let shift = crate_graph.extend(graph);
129 for (crate_id, name) in crate_names {
130 change.set_debug_crate_name(crate_id.shift(shift), name)
131 }
132 }
133 change.set_crate_graph(crate_graph);
134
135 // FIXME: Figure out the multi-workspace situation
136 let check_watcher = workspaces
137 .iter()
138 .find_map(|w| match w {
139 ProjectWorkspace::Cargo { cargo, .. } => Some(cargo),
140 ProjectWorkspace::Json { .. } => None,
141 })
142 .map(|cargo| {
143 let cargo_project_root = cargo.workspace_root().to_path_buf();
144 CheckWatcher::new(&options.cargo_watch, cargo_project_root)
145 })
146 .unwrap_or_else(|| {
147 log::warn!("Cargo check watching only supported for cargo workspaces, disabling");
148 CheckWatcher::dummy()
149 });
150
151 let mut analysis_host = AnalysisHost::new(lru_capacity, feature_flags);
152 analysis_host.apply_change(change);
153 WorldState {
154 options,
155 roots_to_scan,
156 roots: folder_roots,
157 workspaces: Arc::new(workspaces),
158 analysis_host,
159 vfs: Arc::new(RwLock::new(vfs)),
160 task_receiver,
161 latest_requests: Default::default(),
162 check_watcher,
163 diagnostics: Default::default(),
164 }
165 }
166
167 /// Returns a vec of libraries
168 /// FIXME: better API here
169 pub fn process_changes(
170 &mut self,
171 ) -> Option<Vec<(SourceRootId, Vec<(FileId, RelativePathBuf, Arc<String>)>)>> {
172 let changes = self.vfs.write().commit_changes();
173 if changes.is_empty() {
174 return None;
175 }
176 let mut libs = Vec::new();
177 let mut change = AnalysisChange::new();
178 for c in changes {
179 match c {
180 VfsChange::AddRoot { root, files } => {
181 let root_path = self.vfs.read().root2path(root);
182 let is_local = self.roots.iter().any(|r| root_path.starts_with(r));
183 if is_local {
184 self.roots_to_scan -= 1;
185 for (file, path, text) in files {
186 change.add_file(SourceRootId(root.0), FileId(file.0), path, text);
187 }
188 } else {
189 let files = files
190 .into_iter()
191 .map(|(vfsfile, path, text)| (FileId(vfsfile.0), path, text))
192 .collect();
193 libs.push((SourceRootId(root.0), files));
194 }
195 }
196 VfsChange::AddFile { root, file, path, text } => {
197 change.add_file(SourceRootId(root.0), FileId(file.0), path, text);
198 }
199 VfsChange::RemoveFile { root, file, path } => {
200 change.remove_file(SourceRootId(root.0), FileId(file.0), path)
201 }
202 VfsChange::ChangeFile { file, text } => {
203 change.change_file(FileId(file.0), text);
204 }
205 }
206 }
207 self.analysis_host.apply_change(change);
208 Some(libs)
209 }
210
211 pub fn add_lib(&mut self, data: LibraryData) {
212 self.roots_to_scan -= 1;
213 let mut change = AnalysisChange::new();
214 change.add_library(data);
215 self.analysis_host.apply_change(change);
216 }
217
218 pub fn snapshot(&self) -> WorldSnapshot {
219 WorldSnapshot {
220 options: self.options.clone(),
221 workspaces: Arc::clone(&self.workspaces),
222 analysis: self.analysis_host.analysis(),
223 vfs: Arc::clone(&self.vfs),
224 latest_requests: Arc::clone(&self.latest_requests),
225 check_fixes: Arc::clone(&self.diagnostics.check_fixes),
226 }
227 }
228
229 pub fn maybe_collect_garbage(&mut self) {
230 self.analysis_host.maybe_collect_garbage()
231 }
232
233 pub fn collect_garbage(&mut self) {
234 self.analysis_host.collect_garbage()
235 }
236
237 pub fn complete_request(&mut self, request: CompletedRequest) {
238 self.latest_requests.write().record(request)
239 }
240
241 pub fn feature_flags(&self) -> &FeatureFlags {
242 self.analysis_host.feature_flags()
243 }
244}
245
246impl WorldSnapshot {
247 pub fn analysis(&self) -> &Analysis {
248 &self.analysis
249 }
250
251 pub fn uri_to_file_id(&self, uri: &Url) -> Result<FileId> {
252 let path = uri.to_file_path().map_err(|()| format!("invalid uri: {}", uri))?;
253 let file = self.vfs.read().path2file(&path).ok_or_else(|| {
254 // Show warning as this file is outside current workspace
255 LspError {
256 code: ErrorCode::InvalidRequest as i32,
257 message: "Rust file outside current workspace is not supported yet.".to_string(),
258 }
259 })?;
260 Ok(FileId(file.0))
261 }
262
263 pub fn file_id_to_uri(&self, id: FileId) -> Result<Url> {
264 let path = self.vfs.read().file2path(VfsFile(id.0));
265 let url = url_from_path_with_drive_lowercasing(path)?;
266
267 Ok(url)
268 }
269
270 pub fn file_id_to_path(&self, id: FileId) -> PathBuf {
271 self.vfs.read().file2path(VfsFile(id.0))
272 }
273
274 pub fn file_line_endings(&self, id: FileId) -> LineEndings {
275 self.vfs.read().file_line_endings(VfsFile(id.0))
276 }
277
278 pub fn path_to_uri(&self, root: SourceRootId, path: &RelativePathBuf) -> Result<Url> {
279 let base = self.vfs.read().root2path(VfsRoot(root.0));
280 let path = path.to_path(base);
281 let url = Url::from_file_path(&path)
282 .map_err(|_| format!("can't convert path to url: {}", path.display()))?;
283 Ok(url)
284 }
285
286 pub fn status(&self) -> String {
287 let mut res = String::new();
288 if self.workspaces.is_empty() {
289 res.push_str("no workspaces\n")
290 } else {
291 res.push_str("workspaces:\n");
292 for w in self.workspaces.iter() {
293 res += &format!("{} packages loaded\n", w.n_packages());
294 }
295 }
296 res.push_str("\nanalysis:\n");
297 res.push_str(
298 &self
299 .analysis
300 .status()
301 .unwrap_or_else(|_| "Analysis retrieval was cancelled".to_owned()),
302 );
303 res
304 }
305
306 pub fn workspace_root_for(&self, file_id: FileId) -> Option<&Path> {
307 let path = self.vfs.read().file2path(VfsFile(file_id.0));
308 self.workspaces.iter().find_map(|ws| ws.workspace_root_for(&path))
309 }
310
311 pub fn feature_flags(&self) -> &FeatureFlags {
312 self.analysis.feature_flags()
313 }
314}
diff --git a/crates/ra_lsp_server/tests/heavy_tests/main.rs b/crates/ra_lsp_server/tests/heavy_tests/main.rs
deleted file mode 100644
index 9ca31cbcc..000000000
--- a/crates/ra_lsp_server/tests/heavy_tests/main.rs
+++ /dev/null
@@ -1,582 +0,0 @@
1mod support;
2
3use std::{collections::HashMap, time::Instant};
4
5use lsp_types::{
6 CodeActionContext, DidOpenTextDocumentParams, DocumentFormattingParams, FormattingOptions,
7 PartialResultParams, Position, Range, TextDocumentItem, TextDocumentPositionParams,
8 WorkDoneProgressParams,
9};
10use ra_lsp_server::req::{
11 CodeActionParams, CodeActionRequest, Completion, CompletionParams, DidOpenTextDocument,
12 Formatting, OnEnter, Runnables, RunnablesParams,
13};
14use serde_json::json;
15use tempfile::TempDir;
16use test_utils::skip_slow_tests;
17
18use crate::support::{project, Project};
19
20const PROFILE: &'static str = "";
21// const PROFILE: &'static str = "*@3>100";
22
23#[test]
24fn completes_items_from_standard_library() {
25 if skip_slow_tests() {
26 return;
27 }
28
29 let project_start = Instant::now();
30 let server = Project::with_fixture(
31 r#"
32//- Cargo.toml
33[package]
34name = "foo"
35version = "0.0.0"
36
37//- src/lib.rs
38use std::collections::Spam;
39"#,
40 )
41 .with_sysroot(true)
42 .server();
43 server.wait_until_workspace_is_loaded();
44 eprintln!("loading took {:?}", project_start.elapsed());
45 let completion_start = Instant::now();
46 let res = server.send_request::<Completion>(CompletionParams {
47 text_document_position: TextDocumentPositionParams::new(
48 server.doc_id("src/lib.rs"),
49 Position::new(0, 23),
50 ),
51 context: None,
52 partial_result_params: PartialResultParams::default(),
53 work_done_progress_params: WorkDoneProgressParams::default(),
54 });
55 assert!(format!("{}", res).contains("HashMap"));
56 eprintln!("completion took {:?}", completion_start.elapsed());
57}
58
59#[test]
60fn test_runnables_no_project() {
61 if skip_slow_tests() {
62 return;
63 }
64
65 let server = project(
66 r"
67//- lib.rs
68#[test]
69fn foo() {
70}
71",
72 );
73 server.wait_until_workspace_is_loaded();
74 server.request::<Runnables>(
75 RunnablesParams { text_document: server.doc_id("lib.rs"), position: None },
76 json!([
77 {
78 "args": [ "test", "--", "foo", "--nocapture" ],
79 "bin": "cargo",
80 "env": { "RUST_BACKTRACE": "short" },
81 "cwd": null,
82 "label": "test foo",
83 "range": {
84 "end": { "character": 1, "line": 2 },
85 "start": { "character": 0, "line": 0 }
86 }
87 },
88 {
89 "args": [
90 "check",
91 "--all"
92 ],
93 "bin": "cargo",
94 "env": {},
95 "cwd": null,
96 "label": "cargo check --all",
97 "range": {
98 "end": {
99 "character": 0,
100 "line": 0
101 },
102 "start": {
103 "character": 0,
104 "line": 0
105 }
106 }
107 }
108 ]),
109 );
110}
111
112#[test]
113fn test_runnables_project() {
114 if skip_slow_tests() {
115 return;
116 }
117
118 let code = r#"
119//- foo/Cargo.toml
120[package]
121name = "foo"
122version = "0.0.0"
123
124//- foo/src/lib.rs
125pub fn foo() {}
126
127//- foo/tests/spam.rs
128#[test]
129fn test_eggs() {}
130
131//- bar/Cargo.toml
132[package]
133name = "bar"
134version = "0.0.0"
135
136//- bar/src/main.rs
137fn main() {}
138"#;
139
140 let server = Project::with_fixture(code).root("foo").root("bar").server();
141
142 server.wait_until_workspace_is_loaded();
143 server.request::<Runnables>(
144 RunnablesParams {
145 text_document: server.doc_id("foo/tests/spam.rs"),
146 position: None,
147 },
148 json!([
149 {
150 "args": [ "test", "--package", "foo", "--test", "spam", "--", "test_eggs", "--exact", "--nocapture" ],
151 "bin": "cargo",
152 "env": { "RUST_BACKTRACE": "short" },
153 "label": "test test_eggs",
154 "range": {
155 "end": { "character": 17, "line": 1 },
156 "start": { "character": 0, "line": 0 }
157 },
158 "cwd": server.path().join("foo")
159 },
160 {
161 "args": [
162 "check",
163 "--package",
164 "foo",
165 "--test",
166 "spam"
167 ],
168 "bin": "cargo",
169 "env": {},
170 "cwd": server.path().join("foo"),
171 "label": "cargo check -p foo",
172 "range": {
173 "end": {
174 "character": 0,
175 "line": 0
176 },
177 "start": {
178 "character": 0,
179 "line": 0
180 }
181 }
182 }
183 ])
184 );
185}
186
187#[test]
188fn test_format_document() {
189 if skip_slow_tests() {
190 return;
191 }
192
193 let server = project(
194 r#"
195//- Cargo.toml
196[package]
197name = "foo"
198version = "0.0.0"
199
200//- src/lib.rs
201mod bar;
202
203fn main() {
204}
205
206pub use std::collections::HashMap;
207"#,
208 );
209 server.wait_until_workspace_is_loaded();
210
211 server.request::<Formatting>(
212 DocumentFormattingParams {
213 text_document: server.doc_id("src/lib.rs"),
214 options: FormattingOptions {
215 tab_size: 4,
216 insert_spaces: false,
217 insert_final_newline: None,
218 trim_final_newlines: None,
219 trim_trailing_whitespace: None,
220 properties: HashMap::new(),
221 },
222 work_done_progress_params: WorkDoneProgressParams::default(),
223 },
224 json!([
225 {
226 "newText": r#"mod bar;
227
228fn main() {}
229
230pub use std::collections::HashMap;
231"#,
232 "range": {
233 "end": {
234 "character": 0,
235 "line": 7
236 },
237 "start": {
238 "character": 0,
239 "line": 0
240 }
241 }
242 }
243 ]),
244 );
245}
246
247#[test]
248fn test_format_document_2018() {
249 if skip_slow_tests() {
250 return;
251 }
252
253 let server = project(
254 r#"
255//- Cargo.toml
256[package]
257name = "foo"
258version = "0.0.0"
259edition = "2018"
260
261//- src/lib.rs
262mod bar;
263
264async fn test() {
265}
266
267fn main() {
268}
269
270pub use std::collections::HashMap;
271"#,
272 );
273 server.wait_until_workspace_is_loaded();
274
275 server.request::<Formatting>(
276 DocumentFormattingParams {
277 text_document: server.doc_id("src/lib.rs"),
278 options: FormattingOptions {
279 tab_size: 4,
280 insert_spaces: false,
281 properties: HashMap::new(),
282 insert_final_newline: None,
283 trim_final_newlines: None,
284 trim_trailing_whitespace: None,
285 },
286 work_done_progress_params: WorkDoneProgressParams::default(),
287 },
288 json!([
289 {
290 "newText": r#"mod bar;
291
292async fn test() {}
293
294fn main() {}
295
296pub use std::collections::HashMap;
297"#,
298 "range": {
299 "end": {
300 "character": 0,
301 "line": 10
302 },
303 "start": {
304 "character": 0,
305 "line": 0
306 }
307 }
308 }
309 ]),
310 );
311}
312
313#[test]
314fn test_missing_module_code_action() {
315 if skip_slow_tests() {
316 return;
317 }
318
319 let server = project(
320 r#"
321//- Cargo.toml
322[package]
323name = "foo"
324version = "0.0.0"
325
326//- src/lib.rs
327mod bar;
328
329fn main() {}
330"#,
331 );
332 server.wait_until_workspace_is_loaded();
333 let empty_context = || CodeActionContext { diagnostics: Vec::new(), only: None };
334 server.request::<CodeActionRequest>(
335 CodeActionParams {
336 text_document: server.doc_id("src/lib.rs"),
337 range: Range::new(Position::new(0, 4), Position::new(0, 7)),
338 context: empty_context(),
339 partial_result_params: PartialResultParams::default(),
340 work_done_progress_params: WorkDoneProgressParams::default(),
341 },
342 json!([
343 {
344 "command": {
345 "arguments": [
346 {
347 "cursorPosition": null,
348 "label": "create module",
349 "workspaceEdit": {
350 "documentChanges": [
351 {
352 "kind": "create",
353 "uri": "file:///[..]/src/bar.rs"
354 }
355 ]
356 }
357 }
358 ],
359 "command": "rust-analyzer.applySourceChange",
360 "title": "create module"
361 },
362 "title": "create module"
363 }
364 ]),
365 );
366
367 server.request::<CodeActionRequest>(
368 CodeActionParams {
369 text_document: server.doc_id("src/lib.rs"),
370 range: Range::new(Position::new(2, 4), Position::new(2, 7)),
371 context: empty_context(),
372 partial_result_params: PartialResultParams::default(),
373 work_done_progress_params: WorkDoneProgressParams::default(),
374 },
375 json!([]),
376 );
377}
378
379#[test]
380fn test_missing_module_code_action_in_json_project() {
381 if skip_slow_tests() {
382 return;
383 }
384
385 let tmp_dir = TempDir::new().unwrap();
386
387 let path = tmp_dir.path();
388
389 let project = json!({
390 "roots": [path],
391 "crates": [ {
392 "root_module": path.join("src/lib.rs"),
393 "deps": [],
394 "edition": "2015",
395 "atom_cfgs": [],
396 "key_value_cfgs": {}
397 } ]
398 });
399
400 let code = format!(
401 r#"
402//- rust-project.json
403{PROJECT}
404
405//- src/lib.rs
406mod bar;
407
408fn main() {{}}
409"#,
410 PROJECT = project.to_string(),
411 );
412
413 let server = Project::with_fixture(&code).tmp_dir(tmp_dir).server();
414
415 server.wait_until_workspace_is_loaded();
416 let empty_context = || CodeActionContext { diagnostics: Vec::new(), only: None };
417 server.request::<CodeActionRequest>(
418 CodeActionParams {
419 text_document: server.doc_id("src/lib.rs"),
420 range: Range::new(Position::new(0, 4), Position::new(0, 7)),
421 context: empty_context(),
422 partial_result_params: PartialResultParams::default(),
423 work_done_progress_params: WorkDoneProgressParams::default(),
424 },
425 json!([
426 {
427 "command": {
428 "arguments": [
429 {
430 "cursorPosition": null,
431 "label": "create module",
432 "workspaceEdit": {
433 "documentChanges": [
434 {
435 "kind": "create",
436 "uri": "file:///[..]/src/bar.rs"
437 }
438 ]
439 }
440 }
441 ],
442 "command": "rust-analyzer.applySourceChange",
443 "title": "create module"
444 },
445 "title": "create module"
446 }
447 ]),
448 );
449
450 server.request::<CodeActionRequest>(
451 CodeActionParams {
452 text_document: server.doc_id("src/lib.rs"),
453 range: Range::new(Position::new(2, 4), Position::new(2, 7)),
454 context: empty_context(),
455 partial_result_params: PartialResultParams::default(),
456 work_done_progress_params: WorkDoneProgressParams::default(),
457 },
458 json!([]),
459 );
460}
461
462#[test]
463fn diagnostics_dont_block_typing() {
464 if skip_slow_tests() {
465 return;
466 }
467
468 let librs: String = (0..10).map(|i| format!("mod m{};", i)).collect();
469 let libs: String = (0..10).map(|i| format!("//- src/m{}.rs\nfn foo() {{}}\n\n", i)).collect();
470 let server = Project::with_fixture(&format!(
471 r#"
472//- Cargo.toml
473[package]
474name = "foo"
475version = "0.0.0"
476
477//- src/lib.rs
478{}
479
480{}
481
482fn main() {{}}
483"#,
484 librs, libs
485 ))
486 .with_sysroot(true)
487 .server();
488
489 server.wait_until_workspace_is_loaded();
490 for i in 0..10 {
491 server.notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
492 text_document: TextDocumentItem {
493 uri: server.doc_id(&format!("src/m{}.rs", i)).uri,
494 language_id: "rust".to_string(),
495 version: 0,
496 text: "/// Docs\nfn foo() {}".to_string(),
497 },
498 });
499 }
500 let start = std::time::Instant::now();
501 server.request::<OnEnter>(
502 TextDocumentPositionParams {
503 text_document: server.doc_id("src/m0.rs"),
504 position: Position { line: 0, character: 5 },
505 },
506 json!({
507 "cursorPosition": {
508 "position": { "character": 4, "line": 1 },
509 "textDocument": { "uri": "file:///[..]src/m0.rs" }
510 },
511 "label": "on enter",
512 "workspaceEdit": {
513 "documentChanges": [
514 {
515 "edits": [
516 {
517 "newText": "\n/// ",
518 "range": {
519 "end": { "character": 5, "line": 0 },
520 "start": { "character": 5, "line": 0 }
521 }
522 }
523 ],
524 "textDocument": { "uri": "file:///[..]src/m0.rs", "version": null }
525 }
526 ]
527 }
528 }),
529 );
530 let elapsed = start.elapsed();
531 assert!(elapsed.as_millis() < 2000, "typing enter took {:?}", elapsed);
532}
533
534#[test]
535fn preserves_dos_line_endings() {
536 if skip_slow_tests() {
537 return;
538 }
539
540 let server = Project::with_fixture(
541 &"
542//- Cargo.toml
543[package]
544name = \"foo\"
545version = \"0.0.0\"
546
547//- src/main.rs
548/// Some Docs\r\nfn main() {}
549",
550 )
551 .server();
552
553 server.request::<OnEnter>(
554 TextDocumentPositionParams {
555 text_document: server.doc_id("src/main.rs"),
556 position: Position { line: 0, character: 8 },
557 },
558 json!({
559 "cursorPosition": {
560 "position": { "line": 1, "character": 4 },
561 "textDocument": { "uri": "file:///[..]src/main.rs" }
562 },
563 "label": "on enter",
564 "workspaceEdit": {
565 "documentChanges": [
566 {
567 "edits": [
568 {
569 "newText": "\r\n/// ",
570 "range": {
571 "end": { "line": 0, "character": 8 },
572 "start": { "line": 0, "character": 8 }
573 }
574 }
575 ],
576 "textDocument": { "uri": "file:///[..]src/main.rs", "version": null }
577 }
578 ]
579 }
580 }),
581 );
582}
diff --git a/crates/ra_lsp_server/tests/heavy_tests/support.rs b/crates/ra_lsp_server/tests/heavy_tests/support.rs
deleted file mode 100644
index d5ea52fa9..000000000
--- a/crates/ra_lsp_server/tests/heavy_tests/support.rs
+++ /dev/null
@@ -1,254 +0,0 @@
1use std::{
2 cell::{Cell, RefCell},
3 fs,
4 path::{Path, PathBuf},
5 sync::Once,
6 time::Duration,
7};
8
9use crossbeam_channel::{after, select, Receiver};
10use lsp_server::{Connection, Message, Notification, Request};
11use lsp_types::{
12 notification::{DidOpenTextDocument, Exit},
13 request::Shutdown,
14 ClientCapabilities, DidOpenTextDocumentParams, GotoCapability, TextDocumentClientCapabilities,
15 TextDocumentIdentifier, TextDocumentItem, Url,
16};
17use serde::Serialize;
18use serde_json::{to_string_pretty, Value};
19use tempfile::TempDir;
20use test_utils::{find_mismatch, parse_fixture};
21
22use ra_lsp_server::{main_loop, req, ServerConfig};
23
24pub struct Project<'a> {
25 fixture: &'a str,
26 with_sysroot: bool,
27 tmp_dir: Option<TempDir>,
28 roots: Vec<PathBuf>,
29}
30
31impl<'a> Project<'a> {
32 pub fn with_fixture(fixture: &str) -> Project {
33 Project { fixture, tmp_dir: None, roots: vec![], with_sysroot: false }
34 }
35
36 pub fn tmp_dir(mut self, tmp_dir: TempDir) -> Project<'a> {
37 self.tmp_dir = Some(tmp_dir);
38 self
39 }
40
41 pub fn root(mut self, path: &str) -> Project<'a> {
42 self.roots.push(path.into());
43 self
44 }
45
46 pub fn with_sysroot(mut self, sysroot: bool) -> Project<'a> {
47 self.with_sysroot = sysroot;
48 self
49 }
50
51 pub fn server(self) -> Server {
52 let tmp_dir = self.tmp_dir.unwrap_or_else(|| TempDir::new().unwrap());
53 static INIT: Once = Once::new();
54 INIT.call_once(|| {
55 let _ = env_logger::builder().is_test(true).try_init().unwrap();
56 ra_prof::set_filter(if crate::PROFILE.is_empty() {
57 ra_prof::Filter::disabled()
58 } else {
59 ra_prof::Filter::from_spec(&crate::PROFILE)
60 });
61 });
62
63 let mut paths = vec![];
64
65 for entry in parse_fixture(self.fixture) {
66 let path = tmp_dir.path().join(entry.meta);
67 fs::create_dir_all(path.parent().unwrap()).unwrap();
68 fs::write(path.as_path(), entry.text.as_bytes()).unwrap();
69 paths.push((path, entry.text));
70 }
71
72 let roots = self.roots.into_iter().map(|root| tmp_dir.path().join(root)).collect();
73
74 Server::new(tmp_dir, self.with_sysroot, roots, paths)
75 }
76}
77
78pub fn project(fixture: &str) -> Server {
79 Project::with_fixture(fixture).server()
80}
81
82pub struct Server {
83 req_id: Cell<u64>,
84 messages: RefCell<Vec<Message>>,
85 dir: TempDir,
86 _thread: jod_thread::JoinHandle<()>,
87 client: Connection,
88}
89
90impl Server {
91 fn new(
92 dir: TempDir,
93 with_sysroot: bool,
94 roots: Vec<PathBuf>,
95 files: Vec<(PathBuf, String)>,
96 ) -> Server {
97 let path = dir.path().to_path_buf();
98
99 let roots = if roots.is_empty() { vec![path] } else { roots };
100 let (connection, client) = Connection::memory();
101
102 let _thread = jod_thread::Builder::new()
103 .name("test server".to_string())
104 .spawn(move || {
105 main_loop(
106 roots,
107 ClientCapabilities {
108 workspace: None,
109 text_document: Some(TextDocumentClientCapabilities {
110 definition: Some(GotoCapability {
111 dynamic_registration: None,
112 link_support: Some(true),
113 }),
114 ..Default::default()
115 }),
116 window: None,
117 experimental: None,
118 },
119 ServerConfig { with_sysroot, ..ServerConfig::default() },
120 connection,
121 )
122 .unwrap()
123 })
124 .expect("failed to spawn a thread");
125
126 let res =
127 Server { req_id: Cell::new(1), dir, messages: Default::default(), client, _thread };
128
129 for (path, text) in files {
130 res.notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
131 text_document: TextDocumentItem {
132 uri: Url::from_file_path(path).unwrap(),
133 language_id: "rust".to_string(),
134 version: 0,
135 text,
136 },
137 })
138 }
139 res
140 }
141
142 pub fn doc_id(&self, rel_path: &str) -> TextDocumentIdentifier {
143 let path = self.dir.path().join(rel_path);
144 TextDocumentIdentifier { uri: Url::from_file_path(path).unwrap() }
145 }
146
147 pub fn notification<N>(&self, params: N::Params)
148 where
149 N: lsp_types::notification::Notification,
150 N::Params: Serialize,
151 {
152 let r = Notification::new(N::METHOD.to_string(), params);
153 self.send_notification(r)
154 }
155
156 pub fn request<R>(&self, params: R::Params, expected_resp: Value)
157 where
158 R: lsp_types::request::Request,
159 R::Params: Serialize,
160 {
161 let actual = self.send_request::<R>(params);
162 if let Some((expected_part, actual_part)) = find_mismatch(&expected_resp, &actual) {
163 panic!(
164 "JSON mismatch\nExpected:\n{}\nWas:\n{}\nExpected part:\n{}\nActual part:\n{}\n",
165 to_string_pretty(&expected_resp).unwrap(),
166 to_string_pretty(&actual).unwrap(),
167 to_string_pretty(expected_part).unwrap(),
168 to_string_pretty(actual_part).unwrap(),
169 );
170 }
171 }
172
173 pub fn send_request<R>(&self, params: R::Params) -> Value
174 where
175 R: lsp_types::request::Request,
176 R::Params: Serialize,
177 {
178 let id = self.req_id.get();
179 self.req_id.set(id + 1);
180
181 let r = Request::new(id.into(), R::METHOD.to_string(), params);
182 self.send_request_(r)
183 }
184 fn send_request_(&self, r: Request) -> Value {
185 let id = r.id.clone();
186 self.client.sender.send(r.into()).unwrap();
187 while let Some(msg) = self.recv() {
188 match msg {
189 Message::Request(req) => panic!("unexpected request: {:?}", req),
190 Message::Notification(_) => (),
191 Message::Response(res) => {
192 assert_eq!(res.id, id);
193 if let Some(err) = res.error {
194 panic!("error response: {:#?}", err);
195 }
196 return res.result.unwrap();
197 }
198 }
199 }
200 panic!("no response");
201 }
202 pub fn wait_until_workspace_is_loaded(&self) {
203 self.wait_for_message_cond(1, &|msg: &Message| match msg {
204 Message::Notification(n) if n.method == "window/showMessage" => {
205 let msg =
206 n.clone().extract::<req::ShowMessageParams>("window/showMessage").unwrap();
207 msg.message.starts_with("workspace loaded")
208 }
209 _ => false,
210 })
211 }
212 fn wait_for_message_cond(&self, n: usize, cond: &dyn Fn(&Message) -> bool) {
213 let mut total = 0;
214 for msg in self.messages.borrow().iter() {
215 if cond(msg) {
216 total += 1
217 }
218 }
219 while total < n {
220 let msg = self.recv().expect("no response");
221 if cond(&msg) {
222 total += 1;
223 }
224 }
225 }
226 fn recv(&self) -> Option<Message> {
227 recv_timeout(&self.client.receiver).map(|msg| {
228 self.messages.borrow_mut().push(msg.clone());
229 msg
230 })
231 }
232 fn send_notification(&self, not: Notification) {
233 self.client.sender.send(Message::Notification(not)).unwrap();
234 }
235
236 pub fn path(&self) -> &Path {
237 self.dir.path()
238 }
239}
240
241impl Drop for Server {
242 fn drop(&mut self) {
243 self.request::<Shutdown>((), Value::Null);
244 self.notification::<Exit>(());
245 }
246}
247
248fn recv_timeout(receiver: &Receiver<Message>) -> Option<Message> {
249 let timeout = Duration::from_secs(120);
250 select! {
251 recv(receiver) -> msg => msg.ok(),
252 recv(after(timeout)) -> _ => panic!("timed out"),
253 }
254}