) -> tree_sitter::Range {
tree_sitter::Range {
@@ -511,7 +518,7 @@ fn test_removing_injection_by_replacing_across_boundary() {
}
#[gpui::test]
-fn test_combined_injections() {
+fn test_combined_injections_simple() {
let (buffer, syntax_map) = test_edit_sequence(
"ERB",
&[
@@ -653,33 +660,78 @@ fn test_combined_injections_editing_after_last_injection() {
#[gpui::test]
fn test_combined_injections_inside_injections() {
- let (_buffer, _syntax_map) = test_edit_sequence(
+ let (buffer, syntax_map) = test_edit_sequence(
"Markdown",
&[
r#"
- here is some ERB code:
+ here is
+ some
+ ERB code:
```erb
<% people.each do |person| %>
- <%= person.name %>
+ - <%= person.age %>
<% end %>
```
"#,
r#"
- here is some ERB code:
+ here is
+ some
+ ERB code:
```erb
<% people«2».each do |person| %>
- <%= person.name %>
+ - <%= person.age %>
+ <% end %>
+
+ ```
+ "#,
+ // Inserting a comment character inside one code directive
+ // does not cause the other code directive to become a comment,
+ // because newlines are included in between each injection range.
+ r#"
+ here is
+ some
+ ERB code:
+
+ ```erb
+
+ <% people2.each do |person| %>
+ - <%= «# »person.name %>
+ - <%= person.age %>
<% end %>
```
"#,
],
);
+
+ // Check that the code directive below the ruby comment is
+ // not parsed as a comment.
+ assert_capture_ranges(
+ &syntax_map,
+ &buffer,
+ &["method"],
+ "
+ here is
+ some
+ ERB code:
+
+ ```erb
+
+ <% people2.«each» do |person| %>
+ - <%= # person.name %>
+ - <%= person.«age» %>
+ <% end %>
+
+ ```
+ ",
+ );
}
#[gpui::test]
@@ -711,11 +763,7 @@ fn test_empty_combined_injections_inside_injections() {
}
#[gpui::test(iterations = 50)]
-fn test_random_syntax_map_edits(mut rng: StdRng) {
- let operations = env::var("OPERATIONS")
- .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
- .unwrap_or(10);
-
+fn test_random_syntax_map_edits_rust_macros(rng: StdRng) {
let text = r#"
fn test_something() {
let vec = vec![5, 1, 3, 8];
@@ -736,68 +784,12 @@ fn test_random_syntax_map_edits(mut rng: StdRng) {
let registry = Arc::new(LanguageRegistry::test());
let language = Arc::new(rust_lang());
registry.add(language.clone());
- let mut buffer = Buffer::new(0, 0, text);
- let mut syntax_map = SyntaxMap::new();
- syntax_map.set_language_registry(registry.clone());
- syntax_map.reparse(language.clone(), &buffer);
-
- let mut reference_syntax_map = SyntaxMap::new();
- reference_syntax_map.set_language_registry(registry.clone());
-
- log::info!("initial text:\n{}", buffer.text());
-
- for _ in 0..operations {
- let prev_buffer = buffer.snapshot();
- let prev_syntax_map = syntax_map.snapshot();
-
- buffer.randomly_edit(&mut rng, 3);
- log::info!("text:\n{}", buffer.text());
-
- syntax_map.interpolate(&buffer);
- check_interpolation(&prev_syntax_map, &syntax_map, &prev_buffer, &buffer);
-
- syntax_map.reparse(language.clone(), &buffer);
-
- reference_syntax_map.clear();
- reference_syntax_map.reparse(language.clone(), &buffer);
- }
-
- for i in 0..operations {
- let i = operations - i - 1;
- buffer.undo();
- log::info!("undoing operation {}", i);
- log::info!("text:\n{}", buffer.text());
-
- syntax_map.interpolate(&buffer);
- syntax_map.reparse(language.clone(), &buffer);
-
- reference_syntax_map.clear();
- reference_syntax_map.reparse(language.clone(), &buffer);
- assert_eq!(
- syntax_map.layers(&buffer).len(),
- reference_syntax_map.layers(&buffer).len(),
- "wrong number of layers after undoing edit {i}"
- );
- }
-
- let layers = syntax_map.layers(&buffer);
- let reference_layers = reference_syntax_map.layers(&buffer);
- for (edited_layer, reference_layer) in layers.into_iter().zip(reference_layers.into_iter()) {
- assert_eq!(
- edited_layer.node().to_sexp(),
- reference_layer.node().to_sexp()
- );
- assert_eq!(edited_layer.node().range(), reference_layer.node().range());
- }
+ test_random_edits(text, registry, language, rng);
}
#[gpui::test(iterations = 50)]
-fn test_random_syntax_map_edits_with_combined_injections(mut rng: StdRng) {
- let operations = env::var("OPERATIONS")
- .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
- .unwrap_or(10);
-
+fn test_random_syntax_map_edits_with_erb(rng: StdRng) {
let text = r#"
<% if one?(:two) %>
@@ -814,13 +806,60 @@ fn test_random_syntax_map_edits_with_combined_injections(mut rng: StdRng) {
"#
.unindent()
- .repeat(8);
+ .repeat(5);
let registry = Arc::new(LanguageRegistry::test());
let language = Arc::new(erb_lang());
registry.add(language.clone());
registry.add(Arc::new(ruby_lang()));
registry.add(Arc::new(html_lang()));
+
+ test_random_edits(text, registry, language, rng);
+}
+
+#[gpui::test(iterations = 50)]
+fn test_random_syntax_map_edits_with_heex(rng: StdRng) {
+ let text = r#"
+ defmodule TheModule do
+ def the_method(assigns) do
+ ~H"""
+ <%= if @empty do %>
+
+ <% else %>
+
+ <% end %>
+ """
+ end
+ end
+ "#
+ .unindent()
+ .repeat(3);
+
+ let registry = Arc::new(LanguageRegistry::test());
+ let language = Arc::new(elixir_lang());
+ registry.add(language.clone());
+ registry.add(Arc::new(heex_lang()));
+ registry.add(Arc::new(html_lang()));
+
+ test_random_edits(text, registry, language, rng);
+}
+
+fn test_random_edits(
+ text: String,
+ registry: Arc,
+ language: Arc,
+ mut rng: StdRng,
+) {
+ let operations = env::var("OPERATIONS")
+ .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+ .unwrap_or(10);
+
let mut buffer = Buffer::new(0, 0, text);
let mut syntax_map = SyntaxMap::new();
@@ -984,11 +1023,14 @@ fn check_interpolation(
fn test_edit_sequence(language_name: &str, steps: &[&str]) -> (Buffer, SyntaxMap) {
let registry = Arc::new(LanguageRegistry::test());
+ registry.add(Arc::new(elixir_lang()));
+ registry.add(Arc::new(heex_lang()));
registry.add(Arc::new(rust_lang()));
registry.add(Arc::new(ruby_lang()));
registry.add(Arc::new(html_lang()));
registry.add(Arc::new(erb_lang()));
registry.add(Arc::new(markdown_lang()));
+
let language = registry
.language_for_name(language_name)
.now_or_never()
@@ -1074,6 +1116,7 @@ fn ruby_lang() -> Language {
r#"
["if" "do" "else" "end"] @keyword
(instance_variable) @ivar
+ (call method: (identifier) @method)
"#,
)
.unwrap()
@@ -1158,6 +1201,52 @@ fn markdown_lang() -> Language {
.unwrap()
}
+fn elixir_lang() -> Language {
+ Language::new(
+ LanguageConfig {
+ name: "Elixir".into(),
+ path_suffixes: vec!["ex".into()],
+ ..Default::default()
+ },
+ Some(tree_sitter_elixir::language()),
+ )
+ .with_highlights_query(
+ r#"
+
+ "#,
+ )
+ .unwrap()
+}
+
+fn heex_lang() -> Language {
+ Language::new(
+ LanguageConfig {
+ name: "HEEx".into(),
+ path_suffixes: vec!["heex".into()],
+ ..Default::default()
+ },
+ Some(tree_sitter_heex::language()),
+ )
+ .with_injection_query(
+ r#"
+ (
+ (directive
+ [
+ (partial_expression_value)
+ (expression_value)
+ (ending_expression_value)
+ ] @content)
+ (#set! language "elixir")
+ (#set! combined)
+ )
+
+ ((expression (expression_value) @content)
+ (#set! language "elixir"))
+ "#,
+ )
+ .unwrap()
+}
+
fn range_for_text(buffer: &Buffer, text: &str) -> Range {
let start = buffer.as_rope().to_string().find(text).unwrap();
start..start + text.len()
diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs
index 6362b8247d..b5336d5b3b 100644
--- a/crates/language_selector/src/language_selector.rs
+++ b/crates/language_selector/src/language_selector.rs
@@ -93,7 +93,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
self.matches.len()
}
- fn confirm(&mut self, cx: &mut ViewContext>) {
+ fn confirm(&mut self, _: bool, cx: &mut ViewContext>) {
if let Some(mat) = self.matches.get(self.selected_index) {
let language_name = &self.candidates[mat.candidate_id].string;
let language = self.language_registry.language_for_name(language_name);
diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs
index 12d8c6b34d..0dc594a13f 100644
--- a/crates/language_tools/src/lsp_log.rs
+++ b/crates/language_tools/src/lsp_log.rs
@@ -467,8 +467,13 @@ impl Item for LspLogView {
impl SearchableItem for LspLogView {
type Match = ::Match;
- fn to_search_event(event: &Self::Event) -> Option {
- Editor::to_search_event(event)
+ fn to_search_event(
+ &mut self,
+ event: &Self::Event,
+ cx: &mut ViewContext,
+ ) -> Option {
+ self.editor
+ .update(cx, |editor, cx| editor.to_search_event(event, cx))
}
fn clear_matches(&mut self, cx: &mut ViewContext) {
@@ -494,6 +499,11 @@ impl SearchableItem for LspLogView {
.update(cx, |e, cx| e.activate_match(index, matches, cx))
}
+ fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) {
+ self.editor
+ .update(cx, |e, cx| e.select_matches(matches, cx))
+ }
+
fn find_matches(
&mut self,
query: project::search::SearchQuery,
diff --git a/crates/menu/src/menu.rs b/crates/menu/src/menu.rs
index 81716028b9..b0f1a9c6c8 100644
--- a/crates/menu/src/menu.rs
+++ b/crates/menu/src/menu.rs
@@ -3,6 +3,7 @@ gpui::actions!(
[
Cancel,
Confirm,
+ SecondaryConfirm,
SelectPrev,
SelectNext,
SelectFirst,
diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs
index f93fa10052..18e10678fa 100644
--- a/crates/outline/src/outline.rs
+++ b/crates/outline/src/outline.rs
@@ -177,7 +177,7 @@ impl PickerDelegate for OutlineViewDelegate {
Task::ready(())
}
- fn confirm(&mut self, cx: &mut ViewContext) {
+ fn confirm(&mut self, _: bool, cx: &mut ViewContext) {
self.prev_scroll_position.take();
self.active_editor.update(cx, |active_editor, cx| {
if let Some(rows) = active_editor.highlighted_rows() {
diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs
index d09de5320c..6efa33e961 100644
--- a/crates/picker/src/picker.rs
+++ b/crates/picker/src/picker.rs
@@ -7,7 +7,7 @@ use gpui::{
AnyElement, AnyViewHandle, AppContext, Axis, Entity, MouseState, Task, View, ViewContext,
ViewHandle,
};
-use menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
+use menu::{Cancel, Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
use parking_lot::Mutex;
use std::{cmp, sync::Arc};
use util::ResultExt;
@@ -34,7 +34,7 @@ pub trait PickerDelegate: Sized + 'static {
fn selected_index(&self) -> usize;
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>);
fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()>;
- fn confirm(&mut self, cx: &mut ViewContext>);
+ fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>);
fn dismissed(&mut self, cx: &mut ViewContext>);
fn render_match(
&self,
@@ -118,8 +118,8 @@ impl View for Picker {
// Capture mouse events
.on_down(MouseButton::Left, |_, _, _| {})
.on_up(MouseButton::Left, |_, _, _| {})
- .on_click(MouseButton::Left, move |_, picker, cx| {
- picker.select_index(ix, cx);
+ .on_click(MouseButton::Left, move |click, picker, cx| {
+ picker.select_index(ix, click.cmd, cx);
})
.with_cursor_style(CursorStyle::PointingHand)
.into_any()
@@ -175,6 +175,7 @@ impl Picker {
cx.add_action(Self::select_next);
cx.add_action(Self::select_prev);
cx.add_action(Self::confirm);
+ cx.add_action(Self::secondary_confirm);
cx.add_action(Self::cancel);
}
@@ -288,11 +289,11 @@ impl Picker {
cx.notify();
}
- pub fn select_index(&mut self, index: usize, cx: &mut ViewContext) {
+ pub fn select_index(&mut self, index: usize, cmd: bool, cx: &mut ViewContext) {
if self.delegate.match_count() > 0 {
self.confirmed = true;
self.delegate.set_selected_index(index, cx);
- self.delegate.confirm(cx);
+ self.delegate.confirm(cmd, cx);
}
}
@@ -330,7 +331,12 @@ impl Picker {
pub fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) {
self.confirmed = true;
- self.delegate.confirm(cx);
+ self.delegate.confirm(false, cx);
+ }
+
+ pub fn secondary_confirm(&mut self, _: &SecondaryConfirm, cx: &mut ViewContext) {
+ self.confirmed = true;
+ self.delegate.confirm(true, cx);
}
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) {
diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs
index eec64beb5a..08261b64f1 100644
--- a/crates/project/src/lsp_command.rs
+++ b/crates/project/src/lsp_command.rs
@@ -1358,16 +1358,6 @@ impl LspCommand for GetCompletions {
completions
.into_iter()
.filter_map(move |mut lsp_completion| {
- // For now, we can only handle additional edits if they are returned
- // when resolving the completion, not if they are present initially.
- if lsp_completion
- .additional_text_edits
- .as_ref()
- .map_or(false, |edits| !edits.is_empty())
- {
- return None;
- }
-
let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() {
// If the language server provides a range to overwrite, then
// check that the range is valid.
diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs
index 81db0c7ed7..fded9ec309 100644
--- a/crates/project/src/project.rs
+++ b/crates/project/src/project.rs
@@ -50,7 +50,7 @@ use lsp::{
};
use lsp_command::*;
use postage::watch;
-use project_settings::ProjectSettings;
+use project_settings::{LspSettings, ProjectSettings};
use rand::prelude::*;
use search::SearchQuery;
use serde::Serialize;
@@ -149,6 +149,7 @@ pub struct Project {
_maintain_workspace_config: Task<()>,
terminals: Terminals,
copilot_enabled: bool,
+ current_lsp_settings: HashMap, LspSettings>,
}
struct DelayedDebounced {
@@ -260,6 +261,7 @@ pub enum Event {
ActiveEntryChanged(Option),
WorktreeAdded,
WorktreeRemoved(WorktreeId),
+ WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
DiskBasedDiagnosticsStarted {
language_server_id: LanguageServerId,
},
@@ -614,6 +616,7 @@ impl Project {
local_handles: Vec::new(),
},
copilot_enabled: Copilot::global(cx).is_some(),
+ current_lsp_settings: settings::get::(cx).lsp.clone(),
}
})
}
@@ -706,6 +709,7 @@ impl Project {
local_handles: Vec::new(),
},
copilot_enabled: Copilot::global(cx).is_some(),
+ current_lsp_settings: settings::get::(cx).lsp.clone(),
};
for worktree in worktrees {
let _ = this.add_worktree(&worktree, cx);
@@ -779,7 +783,9 @@ 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::(cx).clone();
+
+ let new_lsp_settings = settings::get::(cx).lsp.clone();
+ let current_lsp_settings = &self.current_lsp_settings;
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
let language = languages.iter().find_map(|l| {
let adapter = l
@@ -796,16 +802,25 @@ impl Project {
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)));
+ let server_name = &adapter.name.0;
+ match (
+ current_lsp_settings.get(server_name),
+ new_lsp_settings.get(server_name),
+ ) {
+ (None, None) => {}
+ (Some(_), None) | (None, Some(_)) => {
+ language_servers_to_restart.push((worktree, Arc::clone(language)));
+ }
+ (Some(current_lsp_settings), Some(new_lsp_settings)) => {
+ if current_lsp_settings != new_lsp_settings {
+ language_servers_to_restart.push((worktree, Arc::clone(language)));
+ }
+ }
}
}
}
}
+ self.current_lsp_settings = new_lsp_settings;
// Stop all newly-disabled language servers.
for (worktree_id, adapter_name) in language_servers_to_stop {
@@ -3030,6 +3045,8 @@ impl Project {
) -> Task<(Option, Vec)> {
let key = (worktree_id, adapter_name);
if let Some(server_id) = self.language_server_ids.remove(&key) {
+ log::info!("stopping language server {}", key.1 .0);
+
// Remove other entries for this language server as well
let mut orphaned_worktrees = vec![worktree_id];
let other_keys = self.language_server_ids.keys().cloned().collect::>();
@@ -4432,11 +4449,11 @@ impl Project {
};
cx.spawn(|this, mut cx| async move {
- let resolved_completion = lang_server
+ let additional_text_edits = lang_server
.request::(completion.lsp_completion)
- .await?;
-
- if let Some(edits) = resolved_completion.additional_text_edits {
+ .await?
+ .additional_text_edits;
+ if let Some(edits) = additional_text_edits {
let edits = this
.update(&mut cx, |this, cx| {
this.edits_from_lsp(
@@ -5389,6 +5406,10 @@ impl Project {
this.update_local_worktree_buffers(&worktree, changes, cx);
this.update_local_worktree_language_servers(&worktree, changes, cx);
this.update_local_worktree_settings(&worktree, changes, cx);
+ cx.emit(Event::WorktreeUpdatedEntries(
+ worktree.read(cx).id(),
+ changes.clone(),
+ ));
}
worktree::Event::UpdatedGitRepositories(updated_repos) => {
this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs
index 2c3c9d5304..a1730fd365 100644
--- a/crates/project/src/worktree.rs
+++ b/crates/project/src/worktree.rs
@@ -397,6 +397,7 @@ impl Worktree {
}))
}
+ // abcdefghi
pub fn remote(
project_remote_id: u64,
replica_id: ReplicaId,
@@ -2022,6 +2023,9 @@ impl LocalSnapshot {
) -> Vec> {
let mut changes = vec![];
let mut edits = vec![];
+
+ let statuses = repo_ptr.statuses();
+
for mut entry in self
.descendent_entries(false, false, &work_directory.0)
.cloned()
@@ -2029,10 +2033,8 @@ impl LocalSnapshot {
let Ok(repo_path) = entry.path.strip_prefix(&work_directory.0) else {
continue;
};
- let git_file_status = repo_ptr
- .status(&RepoPath(repo_path.into()))
- .log_err()
- .flatten();
+ let repo_path = RepoPath(repo_path.to_path_buf());
+ let git_file_status = statuses.as_ref().and_then(|s| s.get(&repo_path).copied());
if entry.git_status != git_file_status {
entry.git_status = git_file_status;
changes.push(entry.path.clone());
diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs
index c329ae4e51..5442a8be74 100644
--- a/crates/project_panel/src/project_panel.rs
+++ b/crates/project_panel/src/project_panel.rs
@@ -159,6 +159,9 @@ pub enum Event {
entry_id: ProjectEntryId,
focus_opened_item: bool,
},
+ SplitEntry {
+ entry_id: ProjectEntryId,
+ },
DockPositionChanged,
Focus,
}
@@ -290,6 +293,21 @@ impl ProjectPanel {
}
}
}
+ &Event::SplitEntry { entry_id } => {
+ if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
+ if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
+ workspace
+ .split_path(
+ ProjectPath {
+ worktree_id: worktree.read(cx).id(),
+ path: entry.path.clone(),
+ },
+ cx,
+ )
+ .detach_and_log_err(cx);
+ }
+ }
+ }
_ => {}
}
})
@@ -620,6 +638,10 @@ impl ProjectPanel {
});
}
+ fn split_entry(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext) {
+ cx.emit(Event::SplitEntry { entry_id });
+ }
+
fn new_file(&mut self, _: &NewFile, cx: &mut ViewContext) {
self.add_entry(false, cx)
}
@@ -1333,7 +1355,11 @@ impl ProjectPanel {
if kind.is_dir() {
this.toggle_expanded(entry_id, cx);
} else {
- this.open_entry(entry_id, event.click_count > 1, cx);
+ if event.cmd {
+ this.split_entry(entry_id, cx);
+ } else if !event.cmd {
+ this.open_entry(entry_id, event.click_count > 1, cx);
+ }
}
}
})
diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs
index fc17b57c6d..cbf914230d 100644
--- a/crates/project_symbols/src/project_symbols.rs
+++ b/crates/project_symbols/src/project_symbols.rs
@@ -104,7 +104,7 @@ impl PickerDelegate for ProjectSymbolsDelegate {
"Search project symbols...".into()
}
- fn confirm(&mut self, cx: &mut ViewContext) {
+ fn confirm(&mut self, secondary: bool, cx: &mut ViewContext) {
if let Some(symbol) = self
.matches
.get(self.selected_match_index)
@@ -122,7 +122,12 @@ impl PickerDelegate for ProjectSymbolsDelegate {
.read(cx)
.clip_point_utf16(symbol.range.start, Bias::Left);
- let editor = workspace.open_project_item::(buffer, cx);
+ let editor = if secondary {
+ workspace.split_project_item::(buffer, cx)
+ } else {
+ workspace.open_project_item::(buffer, cx)
+ };
+
editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
s.select_ranges([position..position])
diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs
index 4ba6103167..5bf9ba6ccf 100644
--- a/crates/recent_projects/src/recent_projects.rs
+++ b/crates/recent_projects/src/recent_projects.rs
@@ -134,7 +134,7 @@ impl PickerDelegate for RecentProjectsDelegate {
let combined_string = location
.paths()
.iter()
- .map(|path| path.to_string_lossy().to_owned())
+ .map(|path| util::paths::compact(&path).to_string_lossy().into_owned())
.collect::>()
.join("");
StringMatchCandidate::new(id, combined_string)
@@ -161,7 +161,7 @@ impl PickerDelegate for RecentProjectsDelegate {
Task::ready(())
}
- fn confirm(&mut self, cx: &mut ViewContext) {
+ fn confirm(&mut self, _: bool, cx: &mut ViewContext) {
if let Some((selected_match, workspace)) = self
.matches
.get(self.selected_index())
diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs
index 2bd765f8bb..7d50794108 100644
--- a/crates/search/src/buffer_search.rs
+++ b/crates/search/src/buffer_search.rs
@@ -1,6 +1,6 @@
use crate::{
- SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
- ToggleWholeWord,
+ SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive,
+ ToggleRegex, ToggleWholeWord,
};
use collections::HashMap;
use editor::Editor;
@@ -41,8 +41,10 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(BufferSearchBar::focus_editor);
cx.add_action(BufferSearchBar::select_next_match);
cx.add_action(BufferSearchBar::select_prev_match);
+ cx.add_action(BufferSearchBar::select_all_matches);
cx.add_action(BufferSearchBar::select_next_match_on_pane);
cx.add_action(BufferSearchBar::select_prev_match_on_pane);
+ cx.add_action(BufferSearchBar::select_all_matches_on_pane);
cx.add_action(BufferSearchBar::handle_editor_cancel);
add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx);
add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx);
@@ -67,7 +69,7 @@ pub struct BufferSearchBar {
active_searchable_item: Option>,
active_match_index: Option,
active_searchable_item_subscription: Option,
- seachable_items_with_matches:
+ searchable_items_with_matches:
HashMap, Vec>>,
pending_search: Option>,
search_options: SearchOptions,
@@ -118,7 +120,7 @@ impl View for BufferSearchBar {
.with_children(self.active_searchable_item.as_ref().and_then(
|searchable_item| {
let matches = self
- .seachable_items_with_matches
+ .searchable_items_with_matches
.get(&searchable_item.downgrade())?;
let message = if let Some(match_ix) = self.active_match_index {
format!("{}/{}", match_ix + 1, matches.len())
@@ -146,6 +148,7 @@ impl View for BufferSearchBar {
Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx))
+ .with_child(self.render_action_button("Select All", cx))
.aligned(),
)
.with_child(
@@ -249,7 +252,7 @@ impl BufferSearchBar {
active_searchable_item: None,
active_searchable_item_subscription: None,
active_match_index: None,
- seachable_items_with_matches: Default::default(),
+ searchable_items_with_matches: Default::default(),
default_options: SearchOptions::NONE,
search_options: SearchOptions::NONE,
pending_search: None,
@@ -264,7 +267,7 @@ impl BufferSearchBar {
pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) {
self.dismissed = true;
- for searchable_item in self.seachable_items_with_matches.keys() {
+ for searchable_item in self.searchable_items_with_matches.keys() {
if let Some(searchable_item) =
WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
{
@@ -306,7 +309,7 @@ impl BufferSearchBar {
if let Some(match_ix) = self.active_match_index {
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
if let Some(matches) = self
- .seachable_items_with_matches
+ .searchable_items_with_matches
.get(&active_searchable_item.downgrade())
{
active_searchable_item.activate_match(match_ix, matches, cx)
@@ -438,6 +441,37 @@ impl BufferSearchBar {
.into_any()
}
+ fn render_action_button(
+ &self,
+ icon: &'static str,
+ cx: &mut ViewContext,
+ ) -> AnyElement {
+ let tooltip = "Select All Matches";
+ let tooltip_style = theme::current(cx).tooltip.clone();
+ let action_type_id = 0_usize;
+
+ enum ActionButton {}
+ MouseEventHandler::::new(action_type_id, cx, |state, cx| {
+ let theme = theme::current(cx);
+ let style = theme.search.action_button.style_for(state);
+ Label::new(icon, style.text.clone())
+ .contained()
+ .with_style(style.container)
+ })
+ .on_click(MouseButton::Left, move |_, this, cx| {
+ this.select_all_matches(&SelectAllMatches, cx)
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .with_tooltip::(
+ action_type_id,
+ tooltip.to_string(),
+ Some(Box::new(SelectAllMatches)),
+ tooltip_style,
+ cx,
+ )
+ .into_any()
+ }
+
fn render_close_button(
&self,
theme: &theme::Search,
@@ -533,6 +567,20 @@ impl BufferSearchBar {
self.select_match(Direction::Prev, None, cx);
}
+ fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext) {
+ if !self.dismissed {
+ if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+ if let Some(matches) = self
+ .searchable_items_with_matches
+ .get(&searchable_item.downgrade())
+ {
+ searchable_item.select_matches(matches, cx);
+ self.focus_editor(&FocusEditor, cx);
+ }
+ }
+ }
+ }
+
pub fn select_match(
&mut self,
direction: Direction,
@@ -542,7 +590,7 @@ impl BufferSearchBar {
if let Some(index) = self.active_match_index {
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
if let Some(matches) = self
- .seachable_items_with_matches
+ .searchable_items_with_matches
.get(&searchable_item.downgrade())
{
let new_match_index = searchable_item
@@ -574,6 +622,16 @@ impl BufferSearchBar {
}
}
+ fn select_all_matches_on_pane(
+ pane: &mut Pane,
+ action: &SelectAllMatches,
+ cx: &mut ViewContext,
+ ) {
+ if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() {
+ search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx));
+ }
+ }
+
fn on_query_editor_event(
&mut self,
_: ViewHandle,
@@ -603,7 +661,7 @@ impl BufferSearchBar {
fn clear_matches(&mut self, cx: &mut ViewContext) {
let mut active_item_matches = None;
- for (searchable_item, matches) in self.seachable_items_with_matches.drain() {
+ for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
if let Some(searchable_item) =
WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
{
@@ -615,7 +673,7 @@ impl BufferSearchBar {
}
}
- self.seachable_items_with_matches
+ self.searchable_items_with_matches
.extend(active_item_matches);
}
@@ -663,13 +721,13 @@ impl BufferSearchBar {
if let Some(active_searchable_item) =
WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
{
- this.seachable_items_with_matches
+ this.searchable_items_with_matches
.insert(active_searchable_item.downgrade(), matches);
this.update_match_index(cx);
if !this.dismissed {
let matches = this
- .seachable_items_with_matches
+ .searchable_items_with_matches
.get(&active_searchable_item.downgrade())
.unwrap();
active_searchable_item.update_matches(matches, cx);
@@ -691,7 +749,7 @@ impl BufferSearchBar {
.as_ref()
.and_then(|searchable_item| {
let matches = self
- .seachable_items_with_matches
+ .searchable_items_with_matches
.get(&searchable_item.downgrade())?;
searchable_item.active_match_index(matches, cx)
});
@@ -1027,7 +1085,6 @@ mod tests {
});
}
- /*
#[gpui::test]
async fn test_search_with_options(cx: &mut TestAppContext) {
let (editor, search_bar) = init_test(cx);
@@ -1122,5 +1179,133 @@ mod tests {
);
});
}
- */
+
+ #[gpui::test]
+ async fn test_search_select_all_matches(cx: &mut TestAppContext) {
+ crate::project_search::tests::init_test(cx);
+
+ let buffer_text = r#"
+ A regular expression (shortened as regex or regexp;[1] also referred to as
+ rational expression[2][3]) is a sequence of characters that specifies a search
+ pattern in text. Usually such patterns are used by string-searching algorithms
+ for "find" or "find and replace" operations on strings, or for input validation.
+ "#
+ .unindent();
+ let expected_query_matches_count = buffer_text
+ .chars()
+ .filter(|c| c.to_ascii_lowercase() == 'a')
+ .count();
+ assert!(
+ expected_query_matches_count > 1,
+ "Should pick a query with multiple results"
+ );
+ let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
+ let (window_id, _root_view) = cx.add_window(|_| EmptyView);
+
+ let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
+
+ let search_bar = cx.add_view(window_id, |cx| {
+ let mut search_bar = BufferSearchBar::new(cx);
+ search_bar.set_active_pane_item(Some(&editor), cx);
+ search_bar.show(false, true, cx);
+ search_bar
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.set_query("a", cx);
+ });
+
+ editor.next_notification(cx).await;
+ let initial_selections = editor.update(cx, |editor, cx| {
+ let initial_selections = editor.selections.display_ranges(cx);
+ assert_eq!(
+ initial_selections.len(), 1,
+ "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
+ );
+ initial_selections
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_all_matches(&SelectAllMatches, cx);
+ let all_selections =
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+ assert_eq!(
+ all_selections.len(),
+ expected_query_matches_count,
+ "Should select all `a` characters in the buffer, but got: {all_selections:?}"
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(
+ search_bar.active_match_index,
+ Some(0),
+ "Match index should not change after selecting all matches"
+ );
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_next_match(&SelectNextMatch, cx);
+ let all_selections =
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+ assert_eq!(
+ all_selections.len(),
+ 1,
+ "On next match, should deselect items and select the next match"
+ );
+ assert_ne!(
+ all_selections, initial_selections,
+ "Next match should be different from the first selection"
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(
+ search_bar.active_match_index,
+ Some(1),
+ "Match index should be updated to the next one"
+ );
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_all_matches(&SelectAllMatches, cx);
+ let all_selections =
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+ assert_eq!(
+ all_selections.len(),
+ expected_query_matches_count,
+ "Should select all `a` characters in the buffer, but got: {all_selections:?}"
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(
+ search_bar.active_match_index,
+ Some(1),
+ "Match index should not change after selecting all matches"
+ );
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
+ let all_selections =
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+ assert_eq!(
+ all_selections.len(),
+ 1,
+ "On previous match, should deselect items and select the previous item"
+ );
+ assert_eq!(
+ all_selections, initial_selections,
+ "Previous match should be the same as the first selection"
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(
+ search_bar.active_match_index,
+ Some(0),
+ "Match index should be updated to the previous one"
+ );
+ });
+ }
}
diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs
index 76350f1812..5bdcf72a97 100644
--- a/crates/search/src/project_search.rs
+++ b/crates/search/src/project_search.rs
@@ -667,6 +667,9 @@ impl ProjectSearchView {
if match_ranges.is_empty() {
self.active_match_index = None;
} else {
+ self.active_match_index = Some(0);
+ self.select_match(Direction::Next, cx);
+ self.update_match_index(cx);
let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id);
let is_new_search = self.search_id != prev_search_id;
self.results_editor.update(cx, |editor, cx| {
diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs
index efec4c2516..1303f81e2c 100644
--- a/crates/search/src/search.rs
+++ b/crates/search/src/search.rs
@@ -19,7 +19,8 @@ actions!(
ToggleCaseSensitive,
ToggleRegex,
SelectNextMatch,
- SelectPrevMatch
+ SelectPrevMatch,
+ SelectAllMatches,
]
);
diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs
index 576719526d..39e77b590b 100644
--- a/crates/terminal/src/terminal.rs
+++ b/crates/terminal/src/terminal.rs
@@ -198,7 +198,7 @@ impl TerminalLineHeight {
match self {
TerminalLineHeight::Comfortable => 1.618,
TerminalLineHeight::Standard => 1.3,
- TerminalLineHeight::Custom(line_height) => *line_height,
+ TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
}
}
}
@@ -908,6 +908,21 @@ impl Terminal {
}
}
+ pub fn select_matches(&mut self, matches: Vec>) {
+ let matches_to_select = self
+ .matches
+ .iter()
+ .filter(|self_match| matches.contains(self_match))
+ .cloned()
+ .collect::>();
+ for match_to_select in matches_to_select {
+ self.set_selection(Some((
+ make_selection(&match_to_select),
+ *match_to_select.end(),
+ )));
+ }
+ }
+
fn set_selection(&mut self, selection: Option<(Selection, Point)>) {
self.events
.push_back(InternalEvent::SetSelection(selection));
diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs
index 11f8f7abde..ad61903a9d 100644
--- a/crates/terminal_view/src/terminal_panel.rs
+++ b/crates/terminal_view/src/terminal_panel.rs
@@ -221,6 +221,14 @@ impl TerminalPanel {
pane::Event::ZoomIn => cx.emit(Event::ZoomIn),
pane::Event::ZoomOut => cx.emit(Event::ZoomOut),
pane::Event::Focus => cx.emit(Event::Focus),
+
+ pane::Event::AddItem { item } => {
+ if let Some(workspace) = self.workspace.upgrade(cx) {
+ let pane = self.pane.clone();
+ workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx))
+ }
+ }
+
_ => {}
}
}
diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs
index c40a1a7ccd..3dd401e392 100644
--- a/crates/terminal_view/src/terminal_view.rs
+++ b/crates/terminal_view/src/terminal_view.rs
@@ -275,7 +275,7 @@ impl TerminalView {
cx.spawn(|this, mut cx| async move {
Timer::after(CURSOR_BLINK_INTERVAL).await;
this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
- .log_err();
+ .ok();
})
.detach();
}
@@ -647,7 +647,11 @@ impl SearchableItem for TerminalView {
}
/// Convert events raised by this item into search-relevant events (if applicable)
- fn to_search_event(event: &Self::Event) -> Option {
+ fn to_search_event(
+ &mut self,
+ event: &Self::Event,
+ _: &mut ViewContext,
+ ) -> Option {
match event {
Event::Wakeup => Some(SearchEvent::MatchesInvalidated),
Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged),
@@ -682,6 +686,13 @@ impl SearchableItem for TerminalView {
cx.notify();
}
+ /// Add selections for all matches given.
+ fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) {
+ self.terminal()
+ .update(cx, |term, _| term.select_matches(matches));
+ cx.notify();
+ }
+
/// Get all of the matches for this query, should be done on the background
fn find_matches(
&mut self,
@@ -907,6 +918,7 @@ mod tests {
let params = cx.update(AppState::test);
cx.update(|cx| {
theme::init((), cx);
+ Project::init_settings(cx);
language::init(cx);
});
diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs
index 1949a5d9bb..cdf3cadf59 100644
--- a/crates/theme/src/theme.rs
+++ b/crates/theme/src/theme.rs
@@ -379,6 +379,7 @@ pub struct Search {
pub invalid_include_exclude_editor: ContainerStyle,
pub include_exclude_inputs: ContainedText,
pub option_button: Toggleable>,
+ pub action_button: Interactive,
pub match_background: Color,
pub match_index: ContainedText,
pub results_status: TextStyle,
@@ -586,7 +587,7 @@ pub struct Picker {
pub no_matches: ContainedLabel,
pub item: Toggleable>,
pub header: ContainedLabel,
- pub footer: ContainedLabel,
+ pub footer: Interactive,
}
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
@@ -1030,6 +1031,7 @@ pub struct AssistantStyle {
pub system_sender: Interactive,
pub model: Interactive,
pub remaining_tokens: ContainedText,
+ pub low_remaining_tokens: ContainedText,
pub no_remaining_tokens: ContainedText,
pub error_icon: Icon,
pub api_key_editor: FieldEditor,
diff --git a/crates/theme/src/theme_registry.rs b/crates/theme/src/theme_registry.rs
index 8565bc3b56..617667bc9f 100644
--- a/crates/theme/src/theme_registry.rs
+++ b/crates/theme/src/theme_registry.rs
@@ -5,6 +5,7 @@ use parking_lot::Mutex;
use serde::Deserialize;
use serde_json::Value;
use std::{
+ borrow::Cow,
collections::HashMap,
sync::{
atomic::{AtomicUsize, Ordering::SeqCst},
@@ -43,7 +44,7 @@ impl ThemeRegistry {
this
}
- pub fn list(&self, staff: bool) -> impl Iterator- + '_ {
+ pub fn list_names(&self, staff: bool) -> impl Iterator
- > + '_ {
let mut dirs = self.assets.list("themes/");
if !staff {
@@ -53,10 +54,21 @@ impl ThemeRegistry {
.collect()
}
- dirs.into_iter().filter_map(|path| {
- let filename = path.strip_prefix("themes/")?;
- let theme_name = filename.strip_suffix(".json")?;
- self.get(theme_name).ok().map(|theme| theme.meta.clone())
+ fn get_name(path: &str) -> Option<&str> {
+ path.strip_prefix("themes/")?.strip_suffix(".json")
+ }
+
+ dirs.into_iter().filter_map(|path| match path {
+ Cow::Borrowed(path) => Some(Cow::Borrowed(get_name(path)?)),
+ Cow::Owned(path) => Some(Cow::Owned(get_name(&path)?.to_string())),
+ })
+ }
+
+ pub fn list(&self, staff: bool) -> impl Iterator
- + '_ {
+ self.list_names(staff).filter_map(|theme_name| {
+ self.get(theme_name.as_ref())
+ .ok()
+ .map(|theme| theme.meta.clone())
})
}
diff --git a/crates/theme/src/theme_settings.rs b/crates/theme/src/theme_settings.rs
index b9e6f7a133..b576391e14 100644
--- a/crates/theme/src/theme_settings.rs
+++ b/crates/theme/src/theme_settings.rs
@@ -13,6 +13,7 @@ use std::sync::Arc;
use util::ResultExt as _;
const MIN_FONT_SIZE: f32 = 6.0;
+const MIN_LINE_HEIGHT: f32 = 1.0;
#[derive(Clone, JsonSchema)]
pub struct ThemeSettings {
@@ -20,6 +21,7 @@ pub struct ThemeSettings {
pub buffer_font_features: fonts::Features,
pub buffer_font_family: FamilyId,
pub(crate) buffer_font_size: f32,
+ pub(crate) buffer_line_height: BufferLineHeight,
#[serde(skip)]
pub theme: Arc,
}
@@ -33,11 +35,32 @@ pub struct ThemeSettingsContent {
#[serde(default)]
pub buffer_font_size: Option,
#[serde(default)]
+ pub buffer_line_height: Option,
+ #[serde(default)]
pub buffer_font_features: Option,
#[serde(default)]
pub theme: Option,
}
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum BufferLineHeight {
+ #[default]
+ Comfortable,
+ Standard,
+ Custom(f32),
+}
+
+impl BufferLineHeight {
+ pub fn value(&self) -> f32 {
+ match self {
+ BufferLineHeight::Comfortable => 1.618,
+ BufferLineHeight::Standard => 1.3,
+ BufferLineHeight::Custom(line_height) => *line_height,
+ }
+ }
+}
+
impl ThemeSettings {
pub fn buffer_font_size(&self, cx: &AppContext) -> f32 {
if cx.has_global::() {
@@ -47,6 +70,10 @@ impl ThemeSettings {
}
.max(MIN_FONT_SIZE)
}
+
+ pub fn line_height(&self) -> f32 {
+ f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT)
+ }
}
pub fn adjusted_font_size(size: f32, cx: &AppContext) -> f32 {
@@ -106,6 +133,7 @@ impl settings::Setting for ThemeSettings {
buffer_font_family_name: defaults.buffer_font_family.clone().unwrap(),
buffer_font_features,
buffer_font_size: defaults.buffer_font_size.unwrap(),
+ buffer_line_height: defaults.buffer_line_height.unwrap(),
theme: themes.get(defaults.theme.as_ref().unwrap()).unwrap(),
};
@@ -136,6 +164,7 @@ impl settings::Setting for ThemeSettings {
}
merge(&mut this.buffer_font_size, value.buffer_font_size);
+ merge(&mut this.buffer_line_height, value.buffer_line_height);
}
Ok(this)
@@ -149,8 +178,8 @@ impl settings::Setting for ThemeSettings {
let mut root_schema = generator.root_schema_for::();
let theme_names = cx
.global::>()
- .list(params.staff_mode)
- .map(|theme| Value::String(theme.name.clone()))
+ .list_names(params.staff_mode)
+ .map(|theme_name| Value::String(theme_name.to_string()))
.collect();
let theme_name_schema = SchemaObject {
diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs
index 5775f1b3e7..5510005733 100644
--- a/crates/theme_selector/src/theme_selector.rs
+++ b/crates/theme_selector/src/theme_selector.rs
@@ -120,7 +120,7 @@ impl PickerDelegate for ThemeSelectorDelegate {
self.matches.len()
}
- fn confirm(&mut self, cx: &mut ViewContext) {
+ fn confirm(&mut self, _: bool, cx: &mut ViewContext) {
self.selection_completed = true;
let theme_name = theme::current(cx).meta.name.clone();
diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs
index 7ef55a9918..5df0ed12e9 100644
--- a/crates/util/src/paths.rs
+++ b/crates/util/src/paths.rs
@@ -6,6 +6,7 @@ lazy_static::lazy_static! {
pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory");
pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed");
pub static ref CONVERSATIONS_DIR: PathBuf = HOME.join(".config/zed/conversations");
+ pub static ref EMBEDDINGS_DIR: PathBuf = HOME.join(".config/zed/embeddings");
pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed");
pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed");
pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages");
diff --git a/crates/vcs_menu/Cargo.toml b/crates/vcs_menu/Cargo.toml
new file mode 100644
index 0000000000..4ddf1214d0
--- /dev/null
+++ b/crates/vcs_menu/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "vcs_menu"
+version = "0.1.0"
+edition = "2021"
+publish = false
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+fuzzy = {path = "../fuzzy"}
+gpui = {path = "../gpui"}
+picker = {path = "../picker"}
+util = {path = "../util"}
+theme = {path = "../theme"}
+workspace = {path = "../workspace"}
+
+anyhow.workspace = true
diff --git a/crates/collab_ui/src/branch_list.rs b/crates/vcs_menu/src/lib.rs
similarity index 50%
rename from crates/collab_ui/src/branch_list.rs
rename to crates/vcs_menu/src/lib.rs
index 16fefbd2eb..384b622469 100644
--- a/crates/collab_ui/src/branch_list.rs
+++ b/crates/vcs_menu/src/lib.rs
@@ -1,15 +1,22 @@
-use anyhow::{anyhow, bail};
+use anyhow::{anyhow, bail, Result};
use fuzzy::{StringMatch, StringMatchCandidate};
-use gpui::{elements::*, AppContext, MouseState, Task, ViewContext, ViewHandle};
+use gpui::{
+ actions,
+ elements::*,
+ platform::{CursorStyle, MouseButton},
+ AppContext, MouseState, Task, ViewContext, ViewHandle,
+};
use picker::{Picker, PickerDelegate, PickerEvent};
use std::{ops::Not, sync::Arc};
use util::ResultExt;
use workspace::{Toast, Workspace};
+actions!(branches, [OpenRecent]);
+
pub fn init(cx: &mut AppContext) {
Picker::::init(cx);
+ cx.add_async_action(toggle);
}
-
pub type BranchList = Picker;
pub fn build_branch_list(
@@ -22,19 +29,60 @@ pub fn build_branch_list(
workspace,
selected_index: 0,
last_query: String::default(),
+ branch_name_trailoff_after: 29,
},
cx,
)
.with_theme(|theme| theme.picker.clone())
}
+fn toggle(
+ _: &mut Workspace,
+ _: &OpenRecent,
+ cx: &mut ViewContext,
+) -> Option>> {
+ Some(cx.spawn(|workspace, mut cx| async move {
+ workspace.update(&mut cx, |workspace, cx| {
+ workspace.toggle_modal(cx, |_, cx| {
+ let workspace = cx.handle();
+ cx.add_view(|cx| {
+ Picker::new(
+ BranchListDelegate {
+ matches: vec![],
+ workspace,
+ selected_index: 0,
+ last_query: String::default(),
+ /// Modal branch picker has a longer trailoff than a popover one.
+ branch_name_trailoff_after: 70,
+ },
+ cx,
+ )
+ .with_theme(|theme| theme.picker.clone())
+ .with_max_size(800., 1200.)
+ })
+ });
+ })?;
+ Ok(())
+ }))
+}
+
pub struct BranchListDelegate {
matches: Vec,
workspace: ViewHandle,
selected_index: usize,
last_query: String,
+ /// Max length of branch name before we truncate it and add a trailing `...`.
+ branch_name_trailoff_after: usize,
}
+impl BranchListDelegate {
+ fn display_error_toast(&self, message: String, cx: &mut ViewContext) {
+ const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
+ self.workspace.update(cx, |model, ctx| {
+ model.show_toast(Toast::new(GIT_CHECKOUT_FAILURE_ID, message), ctx)
+ });
+ }
+}
impl PickerDelegate for BranchListDelegate {
fn placeholder_text(&self) -> Arc {
"Select branch...".into()
@@ -58,12 +106,14 @@ impl PickerDelegate for BranchListDelegate {
.read_with(&mut cx, |view, cx| {
let delegate = view.delegate();
let project = delegate.workspace.read(cx).project().read(&cx);
- let mut cwd =
- project
+
+ let Some(worktree) = project
.visible_worktrees(cx)
.next()
- .unwrap()
- .read(cx)
+ else {
+ bail!("Cannot update branch list as there are no visible worktrees")
+ };
+ let mut cwd = worktree .read(cx)
.abs_path()
.to_path_buf();
cwd.push(".git");
@@ -132,44 +182,45 @@ impl PickerDelegate for BranchListDelegate {
})
}
- fn confirm(&mut self, cx: &mut ViewContext>) {
+ fn confirm(&mut self, _: bool, cx: &mut ViewContext>) {
let current_pick = self.selected_index();
- let current_pick = self.matches[current_pick].string.clone();
+ let Some(current_pick) = self.matches.get(current_pick).map(|pick| pick.string.clone()) else {
+ return;
+ };
cx.spawn(|picker, mut cx| async move {
- picker.update(&mut cx, |this, cx| {
- let project = this.delegate().workspace.read(cx).project().read(cx);
- let mut cwd = project
- .visible_worktrees(cx)
- .next()
- .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
- .read(cx)
- .abs_path()
- .to_path_buf();
- cwd.push(".git");
- let status = project
- .fs()
- .open_repo(&cwd)
- .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?
- .lock()
- .change_branch(¤t_pick);
- if status.is_err() {
- const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
- this.delegate().workspace.update(cx, |model, ctx| {
- model.show_toast(
- Toast::new(
- GIT_CHECKOUT_FAILURE_ID,
- format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"),
- ),
- ctx,
- )
- });
- status?;
- }
- cx.emit(PickerEvent::Dismiss);
+ picker
+ .update(&mut cx, |this, cx| {
+ let project = this.delegate().workspace.read(cx).project().read(cx);
+ let mut cwd = project
+ .visible_worktrees(cx)
+ .next()
+ .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
+ .read(cx)
+ .abs_path()
+ .to_path_buf();
+ cwd.push(".git");
+ let status = project
+ .fs()
+ .open_repo(&cwd)
+ .ok_or_else(|| {
+ anyhow!(
+ "Could not open repository at path `{}`",
+ cwd.as_os_str().to_string_lossy()
+ )
+ })?
+ .lock()
+ .change_branch(¤t_pick);
+ if status.is_err() {
+ this.delegate().display_error_toast(format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"), cx);
+ status?;
+ }
+ cx.emit(PickerEvent::Dismiss);
- Ok::<(), anyhow::Error>(())
- }).log_err();
- }).detach();
+ Ok::<(), anyhow::Error>(())
+ })
+ .log_err();
+ })
+ .detach();
}
fn dismissed(&mut self, cx: &mut ViewContext>) {
@@ -183,15 +234,15 @@ impl PickerDelegate for BranchListDelegate {
selected: bool,
cx: &gpui::AppContext,
) -> AnyElement> {
- const DISPLAYED_MATCH_LEN: usize = 29;
let theme = &theme::current(cx);
let hit = &self.matches[ix];
- let shortened_branch_name = util::truncate_and_trailoff(&hit.string, DISPLAYED_MATCH_LEN);
+ let shortened_branch_name =
+ util::truncate_and_trailoff(&hit.string, self.branch_name_trailoff_after);
let highlights = hit
.positions
.iter()
.copied()
- .filter(|index| index < &DISPLAYED_MATCH_LEN)
+ .filter(|index| index < &self.branch_name_trailoff_after)
.collect();
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
Flex::row()
@@ -235,4 +286,61 @@ impl PickerDelegate for BranchListDelegate {
};
Some(label.into_any())
}
+ fn render_footer(
+ &self,
+ cx: &mut ViewContext>,
+ ) -> Option>> {
+ if !self.last_query.is_empty() {
+ let theme = &theme::current(cx);
+ let style = theme.picker.footer.clone();
+ enum BranchCreateButton {}
+ Some(
+ Flex::row().with_child(MouseEventHandler::::new(0, cx, |state, _| {
+ let style = style.style_for(state);
+ Label::new("Create branch", style.label.clone())
+ .contained()
+ .with_style(style.container)
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_down(MouseButton::Left, |_, _, cx| {
+ cx.spawn(|picker, mut cx| async move {
+ picker.update(&mut cx, |this, cx| {
+ let project = this.delegate().workspace.read(cx).project().read(cx);
+ let current_pick = &this.delegate().last_query;
+ let mut cwd = project
+ .visible_worktrees(cx)
+ .next()
+ .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
+ .read(cx)
+ .abs_path()
+ .to_path_buf();
+ cwd.push(".git");
+ let repo = project
+ .fs()
+ .open_repo(&cwd)
+ .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?;
+ let repo = repo
+ .lock();
+ let status = repo
+ .create_branch(¤t_pick);
+ if status.is_err() {
+ this.delegate().display_error_toast(format!("Failed to create branch '{current_pick}', check for conflicts or unstashed files"), cx);
+ status?;
+ }
+ let status = repo.change_branch(¤t_pick);
+ if status.is_err() {
+ this.delegate().display_error_toast(format!("Failed to chec branch '{current_pick}', check for conflicts or unstashed files"), cx);
+ status?;
+ }
+ cx.emit(PickerEvent::Dismiss);
+ Ok::<(), anyhow::Error>(())
+ })
+ }).detach();
+ })).aligned().right()
+ .into_any(),
+ )
+ } else {
+ None
+ }
+ }
}
diff --git a/crates/vector_store/Cargo.toml b/crates/vector_store/Cargo.toml
new file mode 100644
index 0000000000..40bff8b95c
--- /dev/null
+++ b/crates/vector_store/Cargo.toml
@@ -0,0 +1,49 @@
+[package]
+name = "vector_store"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/vector_store.rs"
+doctest = false
+
+[dependencies]
+gpui = { path = "../gpui" }
+language = { path = "../language" }
+project = { path = "../project" }
+workspace = { path = "../workspace" }
+util = { path = "../util" }
+picker = { path = "../picker" }
+theme = { path = "../theme" }
+editor = { path = "../editor" }
+rpc = { path = "../rpc" }
+settings = { path = "../settings" }
+anyhow.workspace = true
+futures.workspace = true
+smol.workspace = true
+rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] }
+isahc.workspace = true
+log.workspace = true
+tree-sitter.workspace = true
+lazy_static.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+async-trait.workspace = true
+bincode = "1.3.3"
+matrixmultiply = "0.3.7"
+tiktoken-rs = "0.5.0"
+rand.workspace = true
+schemars.workspace = true
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }
+language = { path = "../language", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }
+rpc = { path = "../rpc", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"]}
+tree-sitter-rust = "*"
+rand.workspace = true
+unindent.workspace = true
+tempdir.workspace = true
diff --git a/crates/vector_store/README.md b/crates/vector_store/README.md
new file mode 100644
index 0000000000..86e68dc414
--- /dev/null
+++ b/crates/vector_store/README.md
@@ -0,0 +1,31 @@
+
+WIP: Sample SQL Queries
+/*
+
+create table "files" (
+"id" INTEGER PRIMARY KEY,
+"path" VARCHAR,
+"sha1" VARCHAR,
+);
+
+create table symbols (
+"file_id" INTEGER REFERENCES("files", "id") ON CASCADE DELETE,
+"offset" INTEGER,
+"embedding" VECTOR,
+);
+
+insert into "files" ("path", "sha1") values ("src/main.rs", "sha1") return id;
+insert into symbols (
+"file_id",
+"start",
+"end",
+"embedding"
+) values (
+(id,),
+(id,),
+(id,),
+(id,),
+)
+
+
+*/
diff --git a/crates/vector_store/src/db.rs b/crates/vector_store/src/db.rs
new file mode 100644
index 0000000000..a91a1872b5
--- /dev/null
+++ b/crates/vector_store/src/db.rs
@@ -0,0 +1,325 @@
+use std::{
+ cmp::Ordering,
+ collections::HashMap,
+ path::{Path, PathBuf},
+ rc::Rc,
+ time::SystemTime,
+};
+
+use anyhow::{anyhow, Result};
+
+use crate::parsing::ParsedFile;
+use crate::VECTOR_STORE_VERSION;
+use rpc::proto::Timestamp;
+use rusqlite::{
+ params,
+ types::{FromSql, FromSqlResult, ValueRef},
+};
+
+#[derive(Debug)]
+pub struct FileRecord {
+ pub id: usize,
+ pub relative_path: String,
+ pub mtime: Timestamp,
+}
+
+#[derive(Debug)]
+struct Embedding(pub Vec);
+
+impl FromSql for Embedding {
+ fn column_result(value: ValueRef) -> FromSqlResult {
+ let bytes = value.as_blob()?;
+ let embedding: Result, Box> = bincode::deserialize(bytes);
+ if embedding.is_err() {
+ return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
+ }
+ return Ok(Embedding(embedding.unwrap()));
+ }
+}
+
+pub struct VectorDatabase {
+ db: rusqlite::Connection,
+}
+
+impl VectorDatabase {
+ pub fn new(path: String) -> Result {
+ let this = Self {
+ db: rusqlite::Connection::open(path)?,
+ };
+ this.initialize_database()?;
+ Ok(this)
+ }
+
+ fn initialize_database(&self) -> Result<()> {
+ rusqlite::vtab::array::load_module(&self.db)?;
+
+ // This will create the database if it doesnt exist
+
+ // Initialize Vector Databasing Tables
+ self.db.execute(
+ "CREATE TABLE IF NOT EXISTS worktrees (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ absolute_path VARCHAR NOT NULL
+ );
+ CREATE UNIQUE INDEX IF NOT EXISTS worktrees_absolute_path ON worktrees (absolute_path);
+ ",
+ [],
+ )?;
+
+ self.db.execute(
+ "CREATE TABLE IF NOT EXISTS files (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ worktree_id INTEGER NOT NULL,
+ relative_path VARCHAR NOT NULL,
+ mtime_seconds INTEGER NOT NULL,
+ mtime_nanos INTEGER NOT NULL,
+ vector_store_version INTEGER NOT NULL,
+ FOREIGN KEY(worktree_id) REFERENCES worktrees(id) ON DELETE CASCADE
+ )",
+ [],
+ )?;
+
+ self.db.execute(
+ "CREATE TABLE IF NOT EXISTS documents (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ file_id INTEGER NOT NULL,
+ offset INTEGER NOT NULL,
+ name VARCHAR NOT NULL,
+ embedding BLOB NOT NULL,
+ FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE
+ )",
+ [],
+ )?;
+
+ Ok(())
+ }
+
+ pub fn delete_file(&self, worktree_id: i64, delete_path: PathBuf) -> Result<()> {
+ self.db.execute(
+ "DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2",
+ params![worktree_id, delete_path.to_str()],
+ )?;
+ Ok(())
+ }
+
+ pub fn insert_file(&self, worktree_id: i64, indexed_file: ParsedFile) -> Result<()> {
+ // Write to files table, and return generated id.
+ self.db.execute(
+ "
+ DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2;
+ ",
+ params![worktree_id, indexed_file.path.to_str()],
+ )?;
+ let mtime = Timestamp::from(indexed_file.mtime);
+ self.db.execute(
+ "
+ INSERT INTO files
+ (worktree_id, relative_path, mtime_seconds, mtime_nanos, vector_store_version)
+ VALUES
+ (?1, ?2, $3, $4, $5);
+ ",
+ params![
+ worktree_id,
+ indexed_file.path.to_str(),
+ mtime.seconds,
+ mtime.nanos,
+ VECTOR_STORE_VERSION
+ ],
+ )?;
+
+ let file_id = self.db.last_insert_rowid();
+
+ // Currently inserting at approximately 3400 documents a second
+ // I imagine we can speed this up with a bulk insert of some kind.
+ for document in indexed_file.documents {
+ let embedding_blob = bincode::serialize(&document.embedding)?;
+
+ self.db.execute(
+ "INSERT INTO documents (file_id, offset, name, embedding) VALUES (?1, ?2, ?3, ?4)",
+ params![
+ file_id,
+ document.offset.to_string(),
+ document.name,
+ embedding_blob
+ ],
+ )?;
+ }
+
+ Ok(())
+ }
+
+ pub fn find_or_create_worktree(&self, worktree_root_path: &Path) -> Result