use std::{ path::Path, time::{Duration, Instant}, }; use fs::FakeFs; use futures::StreamExt; use gpui::TestAppContext; use language::{CodeLabel, FakeLspAdapter, HighlightId, rust_lang}; use lsp::Uri; use project::{ Project, lsp_store::{log_store::TestRpcRequestTracker, *}, }; use serde_json::json; use util::path; use crate::init_test; #[gpui::test] async fn test_removing_invisible_worktree_cleans_reused_lsp_bookkeeping(cx: &mut TestAppContext) { init_test(cx); cx.executor().allow_parking(); let fs = FakeFs::new(cx.executor()); fs.insert_tree(path!("/the-root"), json!({ "main.rs": "fn main() {}" })) .await; fs.insert_tree( path!("/the-registry"), json!({ "dep": { "src": { "dep.rs": "pub fn dep() {}" } } }), ) .await; let project = Project::test(fs, [path!("/the-root").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default()); let (_visible_buffer, _visible_handle) = project .update(cx, |project, cx| { project.open_local_buffer_with_lsp(path!("/the-root/main.rs"), cx) }) .await .unwrap(); fake_servers.next().await.unwrap(); cx.run_until_parked(); let server_id = project.read_with(cx, |project, cx| { project .lsp_store() .read(cx) .language_server_statuses() .next() .unwrap() .0 }); let external_buffer = project .update(cx, |project, cx| { project.open_local_buffer_via_lsp( Uri::from_file_path(path!("/the-registry/dep/src/dep.rs")).unwrap(), server_id, cx, ) }) .await .unwrap(); cx.run_until_parked(); let invisible_worktree_id = external_buffer.read_with(cx, |buffer, cx| buffer.file().unwrap().worktree_id(cx)); project.read_with(cx, |project, cx| { let worktree = project.worktree_for_id(invisible_worktree_id, cx).unwrap(); assert!(!worktree.read(cx).is_visible()); assert!( project .lsp_store() .read(cx) .has_language_server_seed_for_worktree(invisible_worktree_id) ); }); project.update(cx, |project, cx| { project.remove_worktree(invisible_worktree_id, cx); }); cx.run_until_parked(); project.read_with(cx, |project, cx| { let lsp_store = project.lsp_store(); let lsp_store = lsp_store.read(cx); assert!( lsp_store .language_server_statuses() .any(|(status_server_id, _)| status_server_id == server_id) ); assert!(!lsp_store.has_language_server_seed_for_worktree(invisible_worktree_id)); }); } #[test] fn test_rpc_request_tracker_distinguishes_request_directions() { let mut tracker = TestRpcRequestTracker::new(); let started_at = Instant::now(); assert_eq!( tracker.observe( false, r#"{"jsonrpc":"2.0","id":1,"method":"textDocument/hover"}"#, started_at, ), None ); assert_eq!( tracker.observe( true, r#"{"jsonrpc":"2.0","id":1,"method":"workspace/configuration"}"#, started_at + Duration::from_millis(10), ), None ); assert_eq!( tracker.observe( false, r#"{"jsonrpc":"2.0","id":1,"result":[]}"#, started_at + Duration::from_millis(30), ), Some(Duration::from_millis(20)) ); assert_eq!( tracker.observe( true, r#"{"jsonrpc":"2.0","id":1,"result":null}"#, started_at + Duration::from_millis(50), ), Some(Duration::from_millis(50)) ); } #[test] fn test_rpc_request_tracker_decodes_ids_and_times_cancelled_requests() { let mut tracker = TestRpcRequestTracker::new(); let started_at = Instant::now(); tracker.observe( true, r#"{"jsonrpc":"2.0","id":"foo\u002fbar","method":"workspace/configuration"}"#, started_at, ); assert_eq!( tracker.observe( false, r#"{"jsonrpc":"2.0","id":"foo/bar","result":[]}"#, started_at + Duration::from_millis(25), ), Some(Duration::from_millis(25)) ); tracker.observe( false, r#"{"jsonrpc":"2.0","id":7,"method":"textDocument/hover"}"#, started_at, ); tracker.observe( false, r#"{"jsonrpc":"2.0","method":"$/cancelRequest","params":{"id":7}}"#, started_at + Duration::from_millis(1), ); assert_eq!(tracker.pending_request_count(), 1); assert_eq!( tracker.observe( true, r#"{"jsonrpc":"2.0","id":7,"error":{"code":-32800,"message":"Request was cancelled"}}"#, started_at + Duration::from_millis(10), ), Some(Duration::from_millis(10)) ); assert_eq!(tracker.pending_request_count(), 0); } #[test] fn test_rpc_request_tracker_bounds_unanswered_requests() { let mut tracker = TestRpcRequestTracker::new(); let started_at = Instant::now(); let max_pending_requests = TestRpcRequestTracker::max_pending_requests(); for id in 0..=max_pending_requests { tracker.observe( false, &format!(r#"{{"jsonrpc":"2.0","id":{id},"method":"textDocument/hover"}}"#), started_at + Duration::from_nanos(id as u64), ); } assert_eq!(tracker.pending_request_count(), max_pending_requests); assert_eq!( tracker.observe( true, r#"{"jsonrpc":"2.0","id":0,"result":null}"#, started_at + Duration::from_secs(1), ), None ); assert!( tracker .observe( true, r#"{"jsonrpc":"2.0","id":1,"result":null}"#, started_at + Duration::from_secs(1), ) .is_some() ); } #[test] fn test_rpc_log_duration_proto_roundtrip() { let log_type = LanguageServerLogType::Rpc { received: true, elapsed: Some(Duration::from_micros(1234)), }; assert_eq!( LanguageServerLogType::from_proto(log_type.to_proto()), log_type ); } #[test] fn test_glob_literal_prefix() { assert_eq!(glob_literal_prefix(Path::new("**/*.js")), Path::new("")); assert_eq!( glob_literal_prefix(Path::new("node_modules/**/*.js")), Path::new("node_modules") ); assert_eq!( glob_literal_prefix(Path::new("foo/{bar,baz}.js")), Path::new("foo") ); assert_eq!( glob_literal_prefix(Path::new("foo/bar/baz.js")), Path::new("foo/bar/baz.js") ); #[cfg(target_os = "windows")] { assert_eq!(glob_literal_prefix(Path::new("**\\*.js")), Path::new("")); assert_eq!( glob_literal_prefix(Path::new("node_modules\\**/*.js")), Path::new("node_modules") ); assert_eq!( glob_literal_prefix(Path::new("foo/{bar,baz}.js")), Path::new("foo") ); assert_eq!( glob_literal_prefix(Path::new("foo\\bar\\baz.js")), Path::new("foo/bar/baz.js") ); } } #[test] fn test_multi_len_chars_normalization() { let mut label = CodeLabel::new( "myElˇ (parameter) myElˇ: {\n foo: string;\n}".to_string(), 0..6, vec![(0..6, HighlightId::new(1))], ); ensure_uniform_list_compatible_label(&mut label); assert_eq!( label, CodeLabel::new( "myElˇ (parameter) myElˇ: { foo: string; }".to_string(), 0..6, vec![(0..6, HighlightId::new(1))], ) ); } #[test] fn test_trailing_newline_in_completion_documentation() { let doc = lsp::Documentation::String("Inappropriate argument value (of correct type).\n".to_string()); let completion_doc: CompletionDocumentation = doc.into(); assert!( matches!(completion_doc, CompletionDocumentation::SingleLine(s) if s == "Inappropriate argument value (of correct type).") ); let doc = lsp::Documentation::String(" some value \n".to_string()); let completion_doc: CompletionDocumentation = doc.into(); assert!(matches!( completion_doc, CompletionDocumentation::SingleLine(s) if s == "some value" )); }