merged main

This commit is contained in:
KCaverly 2023-07-07 14:24:29 -04:00
commit 3f5667b101
118 changed files with 4009 additions and 1557 deletions

View file

@ -1822,11 +1822,21 @@ impl LspCommand for InlayHints {
async fn response_from_lsp(
self,
message: Option<Vec<lsp::InlayHint>>,
_: ModelHandle<Project>,
project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
_: LanguageServerId,
cx: AsyncAppContext,
server_id: LanguageServerId,
mut cx: AsyncAppContext,
) -> Result<Vec<InlayHint>> {
let (lsp_adapter, _) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
// `typescript-language-server` adds padding to the left for type hints, turning
// `const foo: boolean` into `const foo : boolean` which looks odd.
// `rust-analyzer` does not have the padding for this case, and we have to accomodate both.
//
// We could trim the whole string, but being pessimistic on par with the situation above,
// there might be a hint with multiple whitespaces at the end(s) which we need to display properly.
// Hence let's use a heuristic first to handle the most awkward case and look for more.
let force_no_type_left_padding =
lsp_adapter.name.0.as_ref() == "typescript-language-server";
cx.read(|cx| {
let origin_buffer = buffer.read(cx);
Ok(message
@ -1840,6 +1850,12 @@ impl LspCommand for InlayHints {
});
let position = origin_buffer
.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left);
let padding_left =
if force_no_type_left_padding && kind == Some(InlayHintKind::Type) {
false
} else {
lsp_hint.padding_left.unwrap_or(false)
};
InlayHint {
buffer_id: origin_buffer.remote_id(),
position: if kind == Some(InlayHintKind::Parameter) {
@ -1847,7 +1863,7 @@ impl LspCommand for InlayHints {
} else {
origin_buffer.anchor_after(position)
},
padding_left: lsp_hint.padding_left.unwrap_or(false),
padding_left,
padding_right: lsp_hint.padding_right.unwrap_or(false),
label: match lsp_hint.label {
lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s),

View file

@ -778,20 +778,32 @@ impl Project {
}
let mut language_servers_to_stop = Vec::new();
let mut language_servers_to_restart = Vec::new();
let languages = self.languages.to_vec();
let project_settings = settings::get::<ProjectSettings>(cx).clone();
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
let language = languages.iter().find(|l| {
l.lsp_adapters()
let language = languages.iter().find_map(|l| {
let adapter = l
.lsp_adapters()
.iter()
.any(|adapter| &adapter.name == started_lsp_name)
.find(|adapter| &adapter.name == started_lsp_name)?;
Some((l, adapter))
});
if let Some(language) = language {
if let Some((language, adapter)) = language {
let worktree = self.worktree_for_id(*worktree_id, cx);
let file = worktree.and_then(|tree| {
let file = worktree.as_ref().and_then(|tree| {
tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _))
});
if !language_settings(Some(language), file.as_ref(), cx).enable_language_server {
language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
} else if let Some(worktree) = worktree {
let new_lsp_settings = project_settings
.lsp
.get(&adapter.name.0)
.and_then(|s| s.initialization_options.as_ref());
if adapter.initialization_options.as_ref() != new_lsp_settings {
language_servers_to_restart.push((worktree, Arc::clone(language)));
}
}
}
}
@ -808,6 +820,11 @@ impl Project {
self.start_language_servers(&worktree, worktree_path, language, cx);
}
// Restart all language servers with changed initialization options.
for (worktree, language) in language_servers_to_restart {
self.restart_language_servers(worktree, language, cx);
}
if !self.copilot_enabled && Copilot::global(cx).is_some() {
self.copilot_enabled = true;
for buffer in self.opened_buffers.values() {
@ -3398,6 +3415,7 @@ impl Project {
cx: &mut ModelContext<Self>,
) {
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
cx.emit(Event::RefreshInlays);
status.pending_work.remove(&token);
cx.notify();
}

View file

@ -981,6 +981,19 @@ impl LocalWorktree {
})
}
/// Find the lowest path in the worktree's datastructures that is an ancestor
fn lowest_ancestor(&self, path: &Path) -> PathBuf {
let mut lowest_ancestor = None;
for path in path.ancestors() {
if self.entry_for_path(path).is_some() {
lowest_ancestor = Some(path.to_path_buf());
break;
}
}
lowest_ancestor.unwrap_or_else(|| PathBuf::from(""))
}
pub fn create_entry(
&self,
path: impl Into<Arc<Path>>,
@ -988,6 +1001,7 @@ impl LocalWorktree {
cx: &mut ModelContext<Worktree>,
) -> Task<Result<Entry>> {
let path = path.into();
let lowest_ancestor = self.lowest_ancestor(&path);
let abs_path = self.absolutize(&path);
let fs = self.fs.clone();
let write = cx.background().spawn(async move {
@ -1001,10 +1015,31 @@ impl LocalWorktree {
cx.spawn(|this, mut cx| async move {
write.await?;
this.update(&mut cx, |this, cx| {
this.as_local_mut().unwrap().refresh_entry(path, None, cx)
})
.await
let (result, refreshes) = this.update(&mut cx, |this, cx| {
let mut refreshes = Vec::<Task<anyhow::Result<Entry>>>::new();
let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
for refresh_path in refresh_paths.ancestors() {
if refresh_path == Path::new("") {
continue;
}
let refresh_full_path = lowest_ancestor.join(refresh_path);
refreshes.push(this.as_local_mut().unwrap().refresh_entry(
refresh_full_path.into(),
None,
cx,
));
}
(
this.as_local_mut().unwrap().refresh_entry(path, None, cx),
refreshes,
)
});
for refresh in refreshes {
refresh.await.log_err();
}
result.await
})
}
@ -2140,6 +2175,7 @@ impl LocalSnapshot {
impl BackgroundScannerState {
fn should_scan_directory(&self, entry: &Entry) -> bool {
(!entry.is_external && !entry.is_ignored)
|| entry.path.file_name() == Some(&*DOT_GIT)
|| self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning
|| self
.paths_to_scan
@ -2319,6 +2355,7 @@ impl BackgroundScannerState {
.entry_for_id(entry_id)
.map(|entry| RepositoryWorkDirectory(entry.path.clone())) else { continue };
log::info!("reload git repository {:?}", dot_git_dir);
let repository = repository.repo_ptr.lock();
let branch = repository.branch_name();
repository.reload_index();
@ -2359,6 +2396,8 @@ impl BackgroundScannerState {
}
fn build_repository(&mut self, dot_git_path: Arc<Path>, fs: &dyn Fs) -> Option<()> {
log::info!("build git repository {:?}", dot_git_path);
let work_dir_path: Arc<Path> = dot_git_path.parent().unwrap().into();
// Guard against repositories inside the repository metadata
@ -3138,8 +3177,6 @@ impl BackgroundScanner {
}
async fn process_events(&mut self, mut abs_paths: Vec<PathBuf>) {
log::debug!("received fs events {:?}", abs_paths);
let root_path = self.state.lock().snapshot.abs_path.clone();
let root_canonical_path = match self.fs.canonicalize(&root_path).await {
Ok(path) => path,
@ -3150,7 +3187,6 @@ impl BackgroundScanner {
};
let mut relative_paths = Vec::with_capacity(abs_paths.len());
let mut unloaded_relative_paths = Vec::new();
abs_paths.sort_unstable();
abs_paths.dedup_by(|a, b| a.starts_with(&b));
abs_paths.retain(|abs_path| {
@ -3173,7 +3209,6 @@ impl BackgroundScanner {
});
if !parent_dir_is_loaded {
log::debug!("ignoring event {relative_path:?} within unloaded directory");
unloaded_relative_paths.push(relative_path);
return false;
}
@ -3182,27 +3217,30 @@ impl BackgroundScanner {
}
});
if !relative_paths.is_empty() {
let (scan_job_tx, scan_job_rx) = channel::unbounded();
self.reload_entries_for_paths(
root_path,
root_canonical_path,
&relative_paths,
abs_paths,
Some(scan_job_tx.clone()),
)
.await;
drop(scan_job_tx);
self.scan_dirs(false, scan_job_rx).await;
let (scan_job_tx, scan_job_rx) = channel::unbounded();
self.update_ignore_statuses(scan_job_tx).await;
self.scan_dirs(false, scan_job_rx).await;
if relative_paths.is_empty() {
return;
}
log::debug!("received fs events {:?}", relative_paths);
let (scan_job_tx, scan_job_rx) = channel::unbounded();
self.reload_entries_for_paths(
root_path,
root_canonical_path,
&relative_paths,
abs_paths,
Some(scan_job_tx.clone()),
)
.await;
drop(scan_job_tx);
self.scan_dirs(false, scan_job_rx).await;
let (scan_job_tx, scan_job_rx) = channel::unbounded();
self.update_ignore_statuses(scan_job_tx).await;
self.scan_dirs(false, scan_job_rx).await;
{
let mut state = self.state.lock();
relative_paths.extend(unloaded_relative_paths);
state.reload_repositories(&relative_paths, self.fs.as_ref());
state.snapshot.completed_scan_id = state.snapshot.scan_id;
for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
@ -3610,23 +3648,28 @@ impl BackgroundScanner {
}
}
let fs_entry = state.insert_entry(fs_entry, self.fs.as_ref());
if let Some(scan_queue_tx) = &scan_queue_tx {
let mut ancestor_inodes = state.snapshot.ancestor_inodes_for_path(&path);
if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) {
ancestor_inodes.insert(metadata.inode);
smol::block_on(scan_queue_tx.send(ScanJob {
abs_path,
path: path.clone(),
ignore_stack,
ancestor_inodes,
is_external: fs_entry.is_external,
scan_queue: scan_queue_tx.clone(),
}))
.unwrap();
if let (Some(scan_queue_tx), true) = (&scan_queue_tx, fs_entry.is_dir()) {
if state.should_scan_directory(&fs_entry) {
let mut ancestor_inodes =
state.snapshot.ancestor_inodes_for_path(&path);
if !ancestor_inodes.contains(&metadata.inode) {
ancestor_inodes.insert(metadata.inode);
smol::block_on(scan_queue_tx.send(ScanJob {
abs_path,
path: path.clone(),
ignore_stack,
ancestor_inodes,
is_external: fs_entry.is_external,
scan_queue: scan_queue_tx.clone(),
}))
.unwrap();
}
} else {
fs_entry.kind = EntryKind::UnloadedDir;
}
}
state.insert_entry(fs_entry, self.fs.as_ref());
}
Ok(None) => {
self.remove_repo_path(&path, &mut state.snapshot);

View file

@ -936,6 +936,119 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
);
}
#[gpui::test]
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let fs_fake = FakeFs::new(cx.background());
fs_fake
.insert_tree(
"/root",
json!({
"a": {},
}),
)
.await;
let tree_fake = Worktree::local(
client_fake,
"/root".as_ref(),
true,
fs_fake,
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
let entry = tree_fake
.update(cx, |tree, cx| {
tree.as_local_mut()
.unwrap()
.create_entry("a/b/c/d.txt".as_ref(), false, cx)
})
.await
.unwrap();
assert!(entry.is_file());
cx.foreground().run_until_parked();
tree_fake.read_with(cx, |tree, _| {
assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
});
let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let fs_real = Arc::new(RealFs);
let temp_root = temp_tree(json!({
"a": {}
}));
let tree_real = Worktree::local(
client_real,
temp_root.path(),
true,
fs_real,
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
let entry = tree_real
.update(cx, |tree, cx| {
tree.as_local_mut()
.unwrap()
.create_entry("a/b/c/d.txt".as_ref(), false, cx)
})
.await
.unwrap();
assert!(entry.is_file());
cx.foreground().run_until_parked();
tree_real.read_with(cx, |tree, _| {
assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
});
// Test smallest change
let entry = tree_real
.update(cx, |tree, cx| {
tree.as_local_mut()
.unwrap()
.create_entry("a/b/c/e.txt".as_ref(), false, cx)
})
.await
.unwrap();
assert!(entry.is_file());
cx.foreground().run_until_parked();
tree_real.read_with(cx, |tree, _| {
assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
});
// Test largest change
let entry = tree_real
.update(cx, |tree, cx| {
tree.as_local_mut()
.unwrap()
.create_entry("d/e/f/g.txt".as_ref(), false, cx)
})
.await
.unwrap();
assert!(entry.is_file());
cx.foreground().run_until_parked();
tree_real.read_with(cx, |tree, _| {
assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
assert!(tree.entry_for_path("d/").unwrap().is_dir());
});
}
#[gpui::test(iterations = 100)]
async fn test_random_worktree_operations_during_initial_scan(
cx: &mut TestAppContext,
@ -1654,6 +1767,23 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
}));
const A_TXT: &'static str = "a.txt";
const B_TXT: &'static str = "b.txt";
const E_TXT: &'static str = "c/d/e.txt";
const F_TXT: &'static str = "f.txt";
const DOTGITIGNORE: &'static str = ".gitignore";
const BUILD_FILE: &'static str = "target/build_file";
let project_path = Path::new("project");
// Set up git repository before creating the worktree.
let work_dir = root.path().join("project");
let mut repo = git_init(work_dir.as_path());
repo.add_ignore_rule(IGNORE_RULE).unwrap();
git_add(A_TXT, &repo);
git_add(E_TXT, &repo);
git_add(DOTGITIGNORE, &repo);
git_commit("Initial commit", &repo);
let tree = Worktree::local(
build_client(cx),
root.path(),
@ -1665,26 +1795,9 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
.await
.unwrap();
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
const A_TXT: &'static str = "a.txt";
const B_TXT: &'static str = "b.txt";
const E_TXT: &'static str = "c/d/e.txt";
const F_TXT: &'static str = "f.txt";
const DOTGITIGNORE: &'static str = ".gitignore";
const BUILD_FILE: &'static str = "target/build_file";
let project_path: &Path = &Path::new("project");
let work_dir = root.path().join("project");
let mut repo = git_init(work_dir.as_path());
repo.add_ignore_rule(IGNORE_RULE).unwrap();
git_add(Path::new(A_TXT), &repo);
git_add(Path::new(E_TXT), &repo);
git_add(Path::new(DOTGITIGNORE), &repo);
git_commit("Initial commit", &repo);
tree.flush_fs_events(cx).await;
deterministic.run_until_parked();
// Check that the right git state is observed on startup
@ -1704,39 +1817,39 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
);
});
// Modify a file in the working copy.
std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
tree.flush_fs_events(cx).await;
deterministic.run_until_parked();
// The worktree detects that the file's git status has changed.
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
assert_eq!(
snapshot.status_for_file(project_path.join(A_TXT)),
Some(GitFileStatus::Modified)
);
});
git_add(Path::new(A_TXT), &repo);
git_add(Path::new(B_TXT), &repo);
// Create a commit in the git repository.
git_add(A_TXT, &repo);
git_add(B_TXT, &repo);
git_commit("Committing modified and added", &repo);
tree.flush_fs_events(cx).await;
deterministic.run_until_parked();
// Check that repo only changes are tracked
// The worktree detects that the files' git status have changed.
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
assert_eq!(
snapshot.status_for_file(project_path.join(F_TXT)),
Some(GitFileStatus::Added)
);
assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
});
// Modify files in the working copy and perform git operations on other files.
git_reset(0, &repo);
git_remove_index(Path::new(B_TXT), &repo);
git_stash(&mut repo);