1 //! Actions that the RLS can perform: responding to requests, watching files,
2 //! etc.
3
4 use crate::config::Config;
5 use crate::config::FmtConfig;
6 use crate::Span;
7 use log::{debug, error, info, trace};
8 use rls_analysis::AnalysisHost;
9 use rls_span as span;
10 use rls_vfs::{FileContents, Vfs};
11 use serde_json::{self, json};
12 use url::Url;
13 use walkdir::WalkDir;
14
15 use crate::actions::format::Rustfmt;
16 use crate::actions::post_build::{AnalysisQueue, BuildResults, PostBuildHandler};
17 use crate::actions::progress::{BuildDiagnosticsNotifier, BuildProgressNotifier};
18 use crate::build::*;
19 use crate::concurrency::{ConcurrentJob, Jobs};
20 use crate::lsp_data;
21 use crate::lsp_data::*;
22 use crate::project_model::{ProjectModel, RacerFallbackModel, RacerProjectModel};
23 use crate::server::Output;
24
25 use std::collections::{HashMap, HashSet};
26 use std::convert::TryFrom;
27 use std::io;
28 use std::path::{Path, PathBuf};
29 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
30 use std::sync::{Arc, Mutex};
31 use std::thread;
32
33 // TODO: Support non-`file` URI schemes in VFS. We're currently ignoring them because
34 // we don't want to crash the RLS in case a client opens a file under different URI scheme
35 // like with git:/ or perforce:/ (Probably even http:/? We currently don't support remote schemes).
36 macro_rules! ignore_non_file_uri {
37 ($expr: expr, $uri: expr, $log_name: expr) => {
38 $expr.map_err(|_| {
39 trace!("{}: Non-`file` URI scheme, ignoring: {:?}", $log_name, $uri);
40 ()
41 })
42 };
43 }
44
45 macro_rules! parse_file_path {
46 ($uri: expr, $log_name: expr) => {
47 ignore_non_file_uri!(parse_file_path($uri), $uri, $log_name)
48 };
49 }
50
51 pub mod diagnostics;
52 pub mod format;
53 pub mod hover;
54 pub mod notifications;
55 pub mod post_build;
56 pub mod progress;
57 pub mod requests;
58 pub mod run;
59 pub mod work_pool;
60
61 /// Persistent context shared across all requests and notifications.
62 pub enum ActionContext {
63 /// Context after server initialization.
64 Init(InitActionContext),
65 /// Context before initialization.
66 Uninit(UninitActionContext),
67 }
68
69 impl ActionContext {
70 /// Construct a new, uninitialized context.
new( analysis: Arc<AnalysisHost>, vfs: Arc<Vfs>, config: Arc<Mutex<Config>>, ) -> ActionContext71 pub fn new(
72 analysis: Arc<AnalysisHost>,
73 vfs: Arc<Vfs>,
74 config: Arc<Mutex<Config>>,
75 ) -> ActionContext {
76 ActionContext::Uninit(UninitActionContext::new(analysis, vfs, config))
77 }
78
79 /// Initialize this context, returns `Err(())` if it has already been initialized.
init<O: Output>( &mut self, current_project: PathBuf, init_options: InitializationOptions, client_capabilities: lsp_data::ClientCapabilities, out: &O, ) -> Result<(), ()>80 pub fn init<O: Output>(
81 &mut self,
82 current_project: PathBuf,
83 init_options: InitializationOptions,
84 client_capabilities: lsp_data::ClientCapabilities,
85 out: &O,
86 ) -> Result<(), ()> {
87 let ctx = match *self {
88 ActionContext::Uninit(ref uninit) => {
89 let ctx = InitActionContext::new(
90 Arc::clone(&uninit.analysis),
91 Arc::clone(&uninit.vfs),
92 Arc::clone(&uninit.config),
93 client_capabilities,
94 current_project,
95 uninit.pid,
96 init_options.cmd_run,
97 );
98 ctx.init(init_options, out);
99 ctx
100 }
101 ActionContext::Init(_) => return Err(()),
102 };
103 *self = ActionContext::Init(ctx);
104 Ok(())
105 }
106
107 /// Returns an initialiased wrapped context, or `Err(())` if not initialised.
inited(&self) -> Result<InitActionContext, ()>108 pub fn inited(&self) -> Result<InitActionContext, ()> {
109 match *self {
110 ActionContext::Uninit(_) => Err(()),
111 ActionContext::Init(ref ctx) => Ok(ctx.clone()),
112 }
113 }
114
pid(&self) -> u32115 pub fn pid(&self) -> u32 {
116 match self {
117 ActionContext::Uninit(ctx) => ctx.pid,
118 ActionContext::Init(ctx) => ctx.pid,
119 }
120 }
121 }
122
123 /// Persistent context shared across all requests and actions after the RLS has
124 /// been initialized.
125 #[derive(Clone)]
126 pub struct InitActionContext {
127 analysis: Arc<AnalysisHost>,
128 vfs: Arc<Vfs>,
129 // Queues analysis jobs so that we don't over-use the CPU.
130 analysis_queue: Arc<AnalysisQueue>,
131
132 current_project: PathBuf,
133 project_model: Arc<Mutex<Option<Arc<ProjectModel>>>>,
134
135 previous_build_results: Arc<Mutex<BuildResults>>,
136 build_queue: BuildQueue,
137 file_to_crates: Arc<Mutex<HashMap<PathBuf, HashSet<Crate>>>>,
138 // Keep a record of builds/post-build tasks currently in flight so that
139 // mutating actions can block until the data is ready.
140 active_build_count: Arc<AtomicUsize>,
141 // Whether we've shown an error message from Cargo since the last successful
142 // build.
143 shown_cargo_error: Arc<AtomicBool>,
144 // Set to true when a potentially mutating request is received. Set to false
145 // if a change arrives. We can thus tell if the RLS has been quiescent while
146 // waiting to mutate the client state.
147 pub quiescent: Arc<AtomicBool>,
148
149 prev_changes: Arc<Mutex<HashMap<PathBuf, u64>>>,
150
151 config: Arc<Mutex<Config>>,
152 jobs: Arc<Mutex<Jobs>>,
153 client_capabilities: Arc<lsp_data::ClientCapabilities>,
154 client_supports_cmd_run: bool,
155 /// Set/confirmed true once a `workspace/didChangeWatchedFile` is processed
156 /// Used to avoid other notifications like didSave causing double cargo builds
157 client_use_change_watched: bool,
158 /// Whether the server is performing cleanup (after having received
159 /// 'shutdown' request), just before final 'exit' request.
160 pub shut_down: Arc<AtomicBool>,
161 pub pid: u32,
162 }
163
164 /// Persistent context shared across all requests and actions before the RLS has
165 /// been initialized.
166 pub struct UninitActionContext {
167 analysis: Arc<AnalysisHost>,
168 vfs: Arc<Vfs>,
169 config: Arc<Mutex<Config>>,
170 pid: u32,
171 }
172
173 impl UninitActionContext {
new( analysis: Arc<AnalysisHost>, vfs: Arc<Vfs>, config: Arc<Mutex<Config>>, ) -> UninitActionContext174 fn new(
175 analysis: Arc<AnalysisHost>,
176 vfs: Arc<Vfs>,
177 config: Arc<Mutex<Config>>,
178 ) -> UninitActionContext {
179 UninitActionContext { analysis, vfs, config, pid: ::std::process::id() }
180 }
181 }
182
183 impl InitActionContext {
new( analysis: Arc<AnalysisHost>, vfs: Arc<Vfs>, config: Arc<Mutex<Config>>, client_capabilities: lsp_data::ClientCapabilities, current_project: PathBuf, pid: u32, client_supports_cmd_run: bool, ) -> InitActionContext184 fn new(
185 analysis: Arc<AnalysisHost>,
186 vfs: Arc<Vfs>,
187 config: Arc<Mutex<Config>>,
188 client_capabilities: lsp_data::ClientCapabilities,
189 current_project: PathBuf,
190 pid: u32,
191 client_supports_cmd_run: bool,
192 ) -> InitActionContext {
193 let build_queue = BuildQueue::new(Arc::clone(&vfs), Arc::clone(&config));
194 let analysis_queue = Arc::new(AnalysisQueue::init());
195 InitActionContext {
196 analysis,
197 analysis_queue,
198 vfs,
199 config,
200 jobs: Arc::default(),
201 current_project,
202 project_model: Arc::default(),
203 previous_build_results: Arc::default(),
204 build_queue,
205 file_to_crates: Arc::default(),
206 active_build_count: Arc::new(AtomicUsize::new(0)),
207 shown_cargo_error: Arc::new(AtomicBool::new(false)),
208 quiescent: Arc::new(AtomicBool::new(false)),
209 prev_changes: Arc::default(),
210 client_capabilities: Arc::new(client_capabilities),
211 client_supports_cmd_run,
212 client_use_change_watched: false,
213 shut_down: Arc::new(AtomicBool::new(false)),
214 pid,
215 }
216 }
217
invalidate_project_model(&self)218 pub fn invalidate_project_model(&self) {
219 *self.project_model.lock().unwrap() = None;
220 }
221
project_model(&self) -> Result<Arc<ProjectModel>, anyhow::Error>222 pub fn project_model(&self) -> Result<Arc<ProjectModel>, anyhow::Error> {
223 let cached: Option<Arc<ProjectModel>> = self.project_model.lock().unwrap().clone();
224 match cached {
225 Some(pm) => Ok(pm),
226 None => {
227 info!("loading cargo project model");
228 let pm = ProjectModel::load(&self.current_project.join("Cargo.toml"), &self.vfs)?;
229 let pm = Arc::new(pm);
230 *self.project_model.lock().unwrap() = Some(Arc::clone(&pm));
231 Ok(pm)
232 }
233 }
234 }
235
racer_cache(&self) -> racer::FileCache236 pub fn racer_cache(&self) -> racer::FileCache {
237 struct RacerVfs(Arc<Vfs>);
238 impl racer::FileLoader for RacerVfs {
239 fn load_file(&self, path: &Path) -> io::Result<String> {
240 match self.0.load_file(path) {
241 Ok(FileContents::Text(t)) => Ok(t),
242 Ok(FileContents::Binary(_)) => {
243 Err(io::Error::new(io::ErrorKind::Other, rls_vfs::Error::BadFileKind))
244 }
245 Err(err) => Err(io::Error::new(io::ErrorKind::Other, err)),
246 }
247 }
248 }
249 racer::FileCache::new(RacerVfs(Arc::clone(&self.vfs)))
250 }
251
racer_session<'c>(&self, cache: &'c racer::FileCache) -> racer::Session<'c>252 pub fn racer_session<'c>(&self, cache: &'c racer::FileCache) -> racer::Session<'c> {
253 let pm: Box<dyn racer::ProjectModelProvider> = match self.project_model() {
254 Ok(pm) => Box::new(RacerProjectModel(pm)),
255 Err(e) => {
256 error!("failed to fetch project model, using fallback: {}", e);
257 Box::new(RacerFallbackModel)
258 }
259 };
260 racer::Session::with_project_model(cache, pm)
261 }
262
263 /// Depending on user configuration, we might use either external Rustfmt or
264 /// the one we're shipping with.
265 /// Locks config to read `rustfmt_path` key.
formatter(&self) -> Rustfmt266 fn formatter(&self) -> Rustfmt {
267 let rustfmt = self
268 .config
269 .lock()
270 .unwrap()
271 .rustfmt_path
272 .clone()
273 .map(|path| (path, self.current_project.clone()));
274
275 Rustfmt::from(rustfmt)
276 }
277
fmt_config(&self) -> FmtConfig278 fn fmt_config(&self) -> FmtConfig {
279 FmtConfig::from(&self.current_project)
280 }
281
file_edition(&self, file: PathBuf) -> Option<Edition>282 fn file_edition(&self, file: PathBuf) -> Option<Edition> {
283 let files_to_crates = self.file_to_crates.lock().unwrap();
284
285 let editions: HashSet<_> = files_to_crates
286 .get(&file)
287 .map(|crates| crates.iter().map(|c| c.edition).collect())
288 .unwrap_or_default();
289
290 let mut iter = editions.into_iter();
291 match (iter.next(), iter.next()) {
292 (ret @ Some(_), None) => ret,
293 (Some(_), Some(_)) => None,
294 _ => {
295 // fall back on checking the root manifest for package edition
296 let manifest_path =
297 cargo::util::important_paths::find_root_manifest_for_wd(&file).ok()?;
298 edition_from_manifest(manifest_path)
299 }
300 }
301 }
302
init<O: Output>(&self, init_options: InitializationOptions, out: &O)303 fn init<O: Output>(&self, init_options: InitializationOptions, out: &O) {
304 let current_project = self.current_project.clone();
305
306 let needs_inference = {
307 let mut config = self.config.lock().unwrap();
308
309 if let Some(init_config) = init_options.settings.map(|s| s.rust) {
310 config.update(init_config);
311 }
312 config.needs_inference()
313 };
314
315 if needs_inference {
316 let config = Arc::clone(&self.config);
317 // Spawn another thread since we're shelling out to Cargo and this can
318 // cause a non-trivial amount of time due to disk access
319 thread::spawn(move || {
320 let mut config = config.lock().unwrap();
321 if let Err(e) = config.infer_defaults(¤t_project) {
322 debug!("Encountered an error while trying to infer config defaults: {:?}", e);
323 }
324 });
325 }
326
327 if !init_options.omit_init_build {
328 self.build_current_project(BuildPriority::Cargo, out);
329 }
330 }
331
build<O: Output>(&self, project_path: &Path, priority: BuildPriority, out: &O)332 fn build<O: Output>(&self, project_path: &Path, priority: BuildPriority, out: &O) {
333 let (job, token) = ConcurrentJob::new();
334 self.add_job(job);
335
336 let pbh = {
337 let config = self.config.lock().unwrap();
338 PostBuildHandler {
339 analysis: Arc::clone(&self.analysis),
340 analysis_queue: Arc::clone(&self.analysis_queue),
341 previous_build_results: Arc::clone(&self.previous_build_results),
342 file_to_crates: Arc::clone(&self.file_to_crates),
343 project_path: project_path.to_owned(),
344 show_warnings: config.show_warnings,
345 related_information_support: self.client_capabilities.related_information_support,
346 shown_cargo_error: Arc::clone(&self.shown_cargo_error),
347 active_build_count: Arc::clone(&self.active_build_count),
348 crate_blacklist: config.crate_blacklist.as_ref().clone(),
349 notifier: Box::new(BuildDiagnosticsNotifier::new(out.clone())),
350 blocked_threads: vec![],
351 _token: token,
352 }
353 };
354
355 let notifier = Box::new(BuildProgressNotifier::new(out.clone()));
356
357 self.active_build_count.fetch_add(1, Ordering::SeqCst);
358 self.build_queue.request_build(project_path, priority, notifier, pbh);
359 }
360
build_current_project<O: Output>(&self, priority: BuildPriority, out: &O)361 fn build_current_project<O: Output>(&self, priority: BuildPriority, out: &O) {
362 self.build(&self.current_project, priority, out);
363 }
364
add_job(&self, job: ConcurrentJob)365 pub fn add_job(&self, job: ConcurrentJob) {
366 self.jobs.lock().unwrap().add(job);
367 }
368
wait_for_concurrent_jobs(&self)369 pub fn wait_for_concurrent_jobs(&self) {
370 self.jobs.lock().unwrap().wait_for_all();
371 }
372
373 /// Block until any builds and analysis tasks are complete.
block_on_build(&self)374 pub fn block_on_build(&self) {
375 self.build_queue.block_on_build();
376 }
377
378 /// Returns `true` if there are no builds pending or in progress.
build_ready(&self) -> bool379 fn build_ready(&self) -> bool {
380 self.build_queue.build_ready()
381 }
382
383 /// Returns `true` if there are no builds or post-build (analysis) tasks pending
384 /// or in progress.
analysis_ready(&self) -> bool385 fn analysis_ready(&self) -> bool {
386 self.active_build_count.load(Ordering::SeqCst) == 0
387 }
388
389 /// See docs on VersionOrdering
check_change_version(&self, file_path: &Path, version_num: u64) -> VersionOrdering390 fn check_change_version(&self, file_path: &Path, version_num: u64) -> VersionOrdering {
391 let file_path = file_path.to_owned();
392 let mut prev_changes = self.prev_changes.lock().unwrap();
393
394 if prev_changes.contains_key(&file_path) {
395 let prev_version = prev_changes[&file_path];
396 if version_num <= prev_version {
397 debug!(
398 "Out of order or duplicate change {:?}, prev: {}, current: {}",
399 file_path, prev_version, version_num,
400 );
401
402 if version_num == prev_version {
403 return VersionOrdering::Duplicate;
404 } else {
405 return VersionOrdering::OutOfOrder;
406 }
407 }
408 }
409
410 prev_changes.insert(file_path, version_num);
411 VersionOrdering::Ok
412 }
413
reset_change_version(&self, file_path: &Path)414 fn reset_change_version(&self, file_path: &Path) {
415 let file_path = file_path.to_owned();
416 let mut prev_changes = self.prev_changes.lock().unwrap();
417 prev_changes.remove(&file_path);
418 }
419
convert_pos_to_span(&self, file_path: PathBuf, pos: Position) -> Span420 fn convert_pos_to_span(&self, file_path: PathBuf, pos: Position) -> Span {
421 trace!("convert_pos_to_span: {:?} {:?}", file_path, pos);
422
423 let pos = ls_util::position_to_rls(pos);
424 let line = self.vfs.load_line(&file_path, pos.row).unwrap();
425 trace!("line: `{}`", line);
426
427 let (start, end) = find_word_at_pos(&line, pos.col);
428 trace!("start: {}, end: {}", start.0, end.0);
429
430 Span::from_positions(
431 span::Position::new(pos.row, start),
432 span::Position::new(pos.row, end),
433 file_path,
434 )
435 }
436 }
437
438 /// Read package edition from the Cargo manifest
edition_from_manifest<P: AsRef<Path>>(manifest_path: P) -> Option<Edition>439 fn edition_from_manifest<P: AsRef<Path>>(manifest_path: P) -> Option<Edition> {
440 #[derive(Debug, serde::Deserialize)]
441 struct Manifest {
442 package: Package,
443 }
444 #[derive(Debug, serde::Deserialize)]
445 struct Package {
446 edition: Option<String>,
447 }
448
449 let manifest: Manifest = toml::from_str(&std::fs::read_to_string(manifest_path).ok()?).ok()?;
450 match manifest.package.edition {
451 Some(edition) => Edition::try_from(edition.as_str()).ok(),
452 None => Some(Edition::default()),
453 }
454 }
455
456 /// Some notifications come with sequence numbers, we check that these are in
457 /// order. However, clients might be buggy about sequence numbers so we do cope
458 /// with them being wrong.
459 ///
460 /// This enum defines the state of sequence numbers.
461 #[derive(Eq, PartialEq, Debug, Clone, Copy)]
462 pub enum VersionOrdering {
463 /// Sequence number is in order (note that we don't currently check that
464 /// sequence numbers are sequential, but we probably should).
465 Ok,
466 /// This indicates the client sent us multiple copies of the same notification
467 /// and some should be ignored.
468 Duplicate,
469 /// Just plain wrong sequence number. No obvious way for us to recover.
470 OutOfOrder,
471 }
472
473 /// Represents a text cursor between characters, pointing at the next character
474 /// in the buffer.
475 type Column = span::Column<span::ZeroIndexed>;
476
477 /// Returns a text cursor range for a found word inside `line` at which `pos`
478 /// text cursor points to. Resulting type represents a (`start`, `end`) range
479 /// between `start` and `end` cursors.
480 /// For example (4, 4) means an empty selection starting after first 4 characters.
find_word_at_pos(line: &str, pos: Column) -> (Column, Column)481 fn find_word_at_pos(line: &str, pos: Column) -> (Column, Column) {
482 let col = pos.0 as usize;
483 let is_ident_char = |c: char| c.is_alphanumeric() || c == '_';
484
485 let start = line
486 .chars()
487 .enumerate()
488 .take(col)
489 .filter(|&(_, c)| !is_ident_char(c))
490 .last()
491 .map(|(i, _)| i + 1)
492 .unwrap_or(0) as u32;
493
494 #[allow(clippy::filter_next)]
495 let end = line
496 .chars()
497 .enumerate()
498 .skip(col)
499 .filter(|&(_, c)| !is_ident_char(c))
500 .next()
501 .map(|(i, _)| i)
502 .unwrap_or(col) as u32;
503
504 (span::Column::new_zero_indexed(start), span::Column::new_zero_indexed(end))
505 }
506
507 /// Client file-watching request / filtering logic
508 /// We want to watch workspace 'Cargo.toml', root 'Cargo.lock' & the root 'target' dir
509 pub struct FileWatch {
510 project_path: PathBuf,
511 project_uri: String,
512 }
513
514 impl FileWatch {
515 /// Construct a new `FileWatch`.
new(ctx: &InitActionContext) -> Self516 pub fn new(ctx: &InitActionContext) -> Self {
517 Self::from_project_root(ctx.current_project.clone())
518 }
519
from_project_root(root: PathBuf) -> Self520 pub fn from_project_root(root: PathBuf) -> Self {
521 Self { project_uri: Url::from_file_path(&root).unwrap().into(), project_path: root }
522 }
523
524 /// Returns json config for desired file watches
watchers_config(&self) -> serde_json::Value525 pub fn watchers_config(&self) -> serde_json::Value {
526 fn watcher(pat: String) -> FileSystemWatcher {
527 FileSystemWatcher { glob_pattern: pat, kind: None }
528 }
529 fn watcher_with_kind(pat: String, kind: WatchKind) -> FileSystemWatcher {
530 FileSystemWatcher { glob_pattern: pat, kind: Some(kind) }
531 }
532
533 let project_str = self.project_path.to_str().unwrap();
534
535 let mut watchers = vec![
536 watcher(format!("{}/Cargo.lock", project_str)),
537 // For target, we only watch if it gets deleted.
538 watcher_with_kind(format!("{}/target", project_str), WatchKind::Delete),
539 ];
540
541 // Find any Cargo.tomls in the project
542 for entry in WalkDir::new(project_str)
543 .into_iter()
544 .filter_map(Result::ok)
545 .filter(|e| e.file_name() == "Cargo.toml")
546 {
547 watchers.push(watcher(entry.path().display().to_string()));
548 }
549
550 json!({ "watchers": watchers })
551 }
552
553 /// Returns if a file change is relevant to the files we actually wanted to watch
554 // Implementation note: This is expected to be called a large number of times in a loop
555 // so should be fast / avoid allocation.
556 #[inline]
relevant_change_kind(&self, change_uri: &Url, kind: FileChangeType) -> bool557 fn relevant_change_kind(&self, change_uri: &Url, kind: FileChangeType) -> bool {
558 let path = change_uri.as_str();
559
560 // Prefix-matching file URLs on Windows require special attention -
561 // - either file:c/... and file:///c:/ works
562 // - drive letters are case-insensitive
563 // - also protects against naive scheme-independent parsing
564 // (https://github.com/Microsoft/vscode-languageserver-node/issues/105)
565 if cfg!(windows) {
566 let changed_path = match change_uri.to_file_path() {
567 Ok(path) => path,
568 Err(_) => return false,
569 };
570 if !changed_path.starts_with(&self.project_path) {
571 return false;
572 }
573 } else if !path.starts_with(&self.project_uri) {
574 return false;
575 }
576
577 if path.ends_with("/Cargo.toml") {
578 return true;
579 }
580
581 let local = &path[self.project_uri.len()..];
582 local == "/Cargo.lock" || (local == "/target" && kind == FileChangeType::Deleted)
583 }
584
585 #[inline]
is_relevant(&self, change: &FileEvent) -> bool586 pub fn is_relevant(&self, change: &FileEvent) -> bool {
587 self.relevant_change_kind(&change.uri, change.typ)
588 }
589
590 #[inline]
is_relevant_save_doc(&self, did_save: &DidSaveTextDocumentParams) -> bool591 pub fn is_relevant_save_doc(&self, did_save: &DidSaveTextDocumentParams) -> bool {
592 self.relevant_change_kind(&did_save.text_document.uri, FileChangeType::Changed)
593 }
594 }
595
596 #[cfg(test)]
597 mod test {
598 use super::*;
599
600 #[test]
test_find_word_at_pos()601 fn test_find_word_at_pos() {
602 fn assert_range(test_str: &'static str, range: (u32, u32)) {
603 assert!(test_str.chars().filter(|c| *c == '|').count() == 1);
604 let col = test_str.find('|').unwrap() as u32;
605 let line = test_str.replace('|', "");
606 let (start, end) = find_word_at_pos(&line, Column::new_zero_indexed(col));
607 let actual = (start.0, end.0);
608 assert_eq!(range, actual, "Assertion failed for {:?}", test_str);
609 }
610
611 assert_range("|struct Def {", (0, 6));
612 assert_range("stru|ct Def {", (0, 6));
613 assert_range("struct| Def {", (0, 6));
614
615 assert_range("struct |Def {", (7, 10));
616 assert_range("struct De|f {", (7, 10));
617 assert_range("struct Def| {", (7, 10));
618
619 assert_range("struct Def |{", (11, 11));
620
621 assert_range("|span::Position<T>", (0, 4));
622 assert_range(" |span::Position<T>", (1, 5));
623 assert_range("sp|an::Position<T>", (0, 4));
624 assert_range("span|::Position<T>", (0, 4));
625 assert_range("span::|Position<T>", (6, 14));
626 assert_range("span::Position|<T>", (6, 14));
627 assert_range("span::Position<|T>", (15, 16));
628 assert_range("span::Position<T|>", (15, 16));
629 }
630
change(url: &str) -> FileEvent631 fn change(url: &str) -> FileEvent {
632 FileEvent::new(Url::parse(url).unwrap(), FileChangeType::Changed)
633 }
634
did_save(url: &str) -> DidSaveTextDocumentParams635 fn did_save(url: &str) -> DidSaveTextDocumentParams {
636 DidSaveTextDocumentParams {
637 text_document: TextDocumentIdentifier::new(Url::parse(url).unwrap()),
638 }
639 }
640
641 #[cfg(not(windows))]
642 #[test]
file_watch_relevant_files()643 fn file_watch_relevant_files() {
644 let watch = FileWatch::from_project_root("/some/dir".into());
645
646 assert!(watch.is_relevant(&change("file://localhost/some/dir/Cargo.toml")));
647 assert!(watch.is_relevant(&change("file:///some/dir/Cargo.toml")));
648
649 assert!(watch.is_relevant(&change("file:///some/dir/Cargo.lock")));
650 assert!(watch.is_relevant(&change("file:///some/dir/inner/Cargo.toml")));
651
652 assert!(!watch.is_relevant(&change("file:///some/dir/inner/Cargo.lock")));
653 assert!(!watch.is_relevant(&change("file:///Cargo.toml")));
654 }
655
656 #[cfg(not(windows))]
657 #[test]
did_save_relevant_files()658 fn did_save_relevant_files() {
659 let watch = FileWatch::from_project_root("/some/dir".into());
660
661 assert!(watch.is_relevant_save_doc(&did_save("file:///some/dir/Cargo.lock")));
662 assert!(watch.is_relevant_save_doc(&did_save("file:///some/dir/inner/Cargo.toml")));
663 assert!(!watch.is_relevant_save_doc(&did_save("file:///some/dir/inner/Cargo.lock")));
664 assert!(!watch.is_relevant_save_doc(&did_save("file:///Cargo.toml")));
665 }
666
667 #[cfg(windows)]
668 #[test]
file_watch_relevant_files()669 fn file_watch_relevant_files() {
670 let watch = FileWatch::from_project_root("C:/some/dir".into());
671
672 assert!(watch.is_relevant(&change("file:c:/some/dir/Cargo.toml")));
673 assert!(watch.is_relevant(&change("file:///c:/some/dir/Cargo.toml")));
674 assert!(watch.is_relevant(&change("file:///C:/some/dir/Cargo.toml")));
675 assert!(watch.is_relevant(&change("file:///c%3A/some/dir/Cargo.toml")));
676
677 assert!(watch.is_relevant(&change("file:///c:/some/dir/Cargo.lock")));
678 assert!(watch.is_relevant(&change("file:///c:/some/dir/inner/Cargo.toml")));
679
680 assert!(!watch.is_relevant(&change("file:///c:/some/dir/inner/Cargo.lock")));
681 assert!(!watch.is_relevant(&change("file:///c:/Cargo.toml")));
682 }
683
684 #[cfg(windows)]
685 #[test]
did_save_relevant_files()686 fn did_save_relevant_files() {
687 let watch = FileWatch::from_project_root("C:/some/dir".into());
688
689 assert!(watch.is_relevant_save_doc(&did_save("file:///c:/some/dir/Cargo.lock")));
690 assert!(watch.is_relevant_save_doc(&did_save("file:///c:/some/dir/inner/Cargo.toml")));
691 assert!(!watch.is_relevant_save_doc(&did_save("file:///c:/some/dir/inner/Cargo.lock")));
692 assert!(!watch.is_relevant_save_doc(&did_save("file:///c:/Cargo.toml")));
693 }
694
695 #[test]
explicit_edition_from_manifest() -> Result<(), std::io::Error>696 fn explicit_edition_from_manifest() -> Result<(), std::io::Error> {
697 use std::{fs::File, io::Write};
698
699 let dir = tempfile::tempdir()?;
700
701 let manifest_path = {
702 let path = dir.path().join("Cargo.toml");
703 let mut m = File::create(&path)?;
704 writeln!(
705 m,
706 "[package]\n\
707 name = \"foo\"\n\
708 version = \"1.0.0\"\n\
709 edition = \"2018\""
710 )?;
711 path
712 };
713
714 assert_eq!(edition_from_manifest(manifest_path), Some(Edition::Edition2018));
715
716 Ok(())
717 }
718 }
719