From b402f27d5042308548e05de6f8626a1e285f01e1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Feb 2023 15:57:32 +0100 Subject: [PATCH 1/4] Introduce a new language picker that displays available languages Right now this panics when trying to select a language, so that's what we're going to implement next. Co-Authored-By: Julia Risley --- Cargo.lock | 16 ++ Cargo.toml | 1 + crates/language_selector/Cargo.toml | 20 ++ .../src/language_selector.rs | 196 ++++++++++++++++++ crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + 6 files changed, 235 insertions(+) create mode 100644 crates/language_selector/Cargo.toml create mode 100644 crates/language_selector/src/language_selector.rs diff --git a/Cargo.lock b/Cargo.lock index 87e32ed97de3e024cb144ecaa5bec866857970d3..94e7af6766ea1fc397343eb4ad7bcab4953c2915 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3286,6 +3286,21 @@ dependencies = [ "util", ] +[[package]] +name = "language_selector" +version = "0.1.0" +dependencies = [ + "editor", + "fuzzy", + "gpui", + "language", + "picker", + "project", + "settings", + "theme", + "workspace", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -8399,6 +8414,7 @@ dependencies = [ "isahc", "journal", "language", + "language_selector", "lazy_static", "libc", "log", diff --git a/Cargo.toml b/Cargo.toml index c74a76cccefe6c6de610b30c264a81e74b2654df..63882573ab1e32cddee4b3d4efd6d7ede7032240 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ members = [ "crates/gpui_macros", "crates/journal", "crates/language", + "crates/language_selector", "crates/live_kit_client", "crates/live_kit_server", "crates/lsp", diff --git a/crates/language_selector/Cargo.toml b/crates/language_selector/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..60ed0e6633640c6ba5c4ff80e400203f450e2321 --- /dev/null +++ b/crates/language_selector/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "language_selector" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/language_selector.rs" +doctest = false + +[dependencies] +editor = { path = "../editor" } +fuzzy = { path = "../fuzzy" } +language = { path = "../language" } +gpui = { path = "../gpui" } +picker = { path = "../picker" } +project = { path = "../project" } +theme = { path = "../theme" } +settings = { path = "../settings" } +workspace = { path = "../workspace" } diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs new file mode 100644 index 0000000000000000000000000000000000000000..d48a50ce932abec0c6f3f9d98a8edbd450a28648 --- /dev/null +++ b/crates/language_selector/src/language_selector.rs @@ -0,0 +1,196 @@ +use std::sync::Arc; + +use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; +use gpui::{ + actions, elements::*, AnyViewHandle, AppContext, Entity, MouseState, MutableAppContext, + RenderContext, View, ViewContext, ViewHandle, +}; +use language::LanguageRegistry; +use picker::{Picker, PickerDelegate}; +use settings::Settings; +use workspace::{AppState, Workspace}; + +actions!(language_selector, [Toggle]); + +pub fn init(app_state: Arc, cx: &mut MutableAppContext) { + Picker::::init(cx); + cx.add_action({ + let language_registry = app_state.languages.clone(); + move |workspace, _: &Toggle, cx| { + LanguageSelector::toggle(workspace, language_registry.clone(), cx) + } + }); +} + +pub enum Event { + Dismissed, +} + +pub struct LanguageSelector { + language_registry: Arc, + matches: Vec, + picker: ViewHandle>, + selected_index: usize, +} + +impl LanguageSelector { + fn new(language_registry: Arc, cx: &mut ViewContext) -> Self { + let handle = cx.weak_handle(); + let picker = cx.add_view(|cx| Picker::new("Select Language...", handle, cx)); + + let mut matches = language_registry + .language_names() + .into_iter() + .enumerate() + .map(|(candidate_id, name)| StringMatch { + candidate_id, + score: 0.0, + positions: Default::default(), + string: name, + }) + .collect::>(); + matches.sort_unstable_by(|mat1, mat2| mat1.string.cmp(&mat2.string)); + + Self { + language_registry, + matches, + picker, + selected_index: 0, + } + } + + fn toggle( + workspace: &mut Workspace, + registry: Arc, + cx: &mut ViewContext, + ) { + workspace.toggle_modal(cx, |_, cx| { + let this = cx.add_view(|cx| Self::new(registry, cx)); + cx.subscribe(&this, Self::on_event).detach(); + this + }); + } + + fn on_event( + workspace: &mut Workspace, + _: ViewHandle, + event: &Event, + cx: &mut ViewContext, + ) { + match event { + Event::Dismissed => { + workspace.dismiss_modal(cx); + } + } + } +} + +impl Entity for LanguageSelector { + type Event = Event; +} + +impl View for LanguageSelector { + fn ui_name() -> &'static str { + "LanguageSelector" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + ChildView::new(self.picker.clone(), cx).boxed() + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(&self.picker); + } + } +} + +impl PickerDelegate for LanguageSelector { + fn match_count(&self) -> usize { + self.matches.len() + } + + fn confirm(&mut self, cx: &mut ViewContext) { + todo!(); + cx.emit(Event::Dismissed); + } + + fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Dismissed); + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext) { + self.selected_index = ix; + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> gpui::Task<()> { + let background = cx.background().clone(); + let candidates = self + .language_registry + .language_names() + .into_iter() + .enumerate() + .map(|(id, name)| StringMatchCandidate { + id, + char_bag: name.as_str().into(), + string: name.clone(), + }) + .collect::>(); + + cx.spawn(|this, mut cx| async move { + let matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + match_strings( + &candidates, + &query, + false, + 100, + &Default::default(), + background, + ) + .await + }; + + this.update(&mut cx, |this, cx| { + this.matches = matches; + this.selected_index = this + .selected_index + .min(this.matches.len().saturating_sub(1)); + cx.notify(); + }); + }) + } + + fn render_match( + &self, + ix: usize, + mouse_state: &mut MouseState, + selected: bool, + cx: &AppContext, + ) -> ElementBox { + let settings = cx.global::(); + let theme = &settings.theme; + let theme_match = &self.matches[ix]; + let style = theme.picker.item.style_for(mouse_state, selected); + + Label::new(theme_match.string.clone(), style.label.clone()) + .with_highlights(theme_match.positions.clone()) + .contained() + .with_style(style.container) + .boxed() + } +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 8060c2af115b0aa28b68a36f1d78f30ddfbbadad..a7c4861e023578c07e2f4ee7c7fd164c3e7116b9 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -40,6 +40,7 @@ go_to_line = { path = "../go_to_line" } gpui = { path = "../gpui" } journal = { path = "../journal" } language = { path = "../language" } +language_selector = { path = "../language_selector" } lsp = { path = "../lsp" } outline = { path = "../outline" } plugin_runtime = { path = "../plugin_runtime" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b9e3ed550beb434c7da92138308131f09f3c8ac2..655e3968cf615f54e8f3150a47bd697677ae0973 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -185,6 +185,7 @@ fn main() { workspace::init(app_state.clone(), cx); journal::init(app_state.clone(), cx); + language_selector::init(app_state.clone(), cx); theme_selector::init(app_state.clone(), cx); zed::init(&app_state, cx); collab_ui::init(app_state.clone(), cx); From 686f5439ad033d61d72dcbf22f14a19d3ad45057 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Feb 2023 16:28:56 +0100 Subject: [PATCH 2/4] Set buffer language when confirming selection in language selector Co-Authored-By: Julia Risley --- Cargo.lock | 1 + crates/editor/src/editor.rs | 9 ++ crates/language_selector/Cargo.toml | 1 + .../src/language_selector.rs | 83 ++++++++++++------- crates/project/src/project.rs | 36 +++++--- 5 files changed, 89 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94e7af6766ea1fc397343eb4ad7bcab4953c2915..0f19d4455f957463f61e8db34ff9fdb135ed4f3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3290,6 +3290,7 @@ dependencies = [ name = "language_selector" version = "0.1.0" dependencies = [ + "anyhow", "editor", "fuzzy", "gpui", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3e7a14d2aea29312bb6b6853f18957ca116eb5f6..7f60309f6ab39ad1839598fc612d465e2f5cf19d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1254,6 +1254,15 @@ impl Editor { self.buffer.read(cx).language_at(point, cx) } + pub fn active_excerpt( + &self, + cx: &AppContext, + ) -> Option<(ExcerptId, ModelHandle, Range)> { + self.buffer + .read(cx) + .excerpt_containing(self.selections.newest_anchor().head(), cx) + } + fn style(&self, cx: &AppContext) -> EditorStyle { build_style( cx.global::(), diff --git a/crates/language_selector/Cargo.toml b/crates/language_selector/Cargo.toml index 60ed0e6633640c6ba5c4ff80e400203f450e2321..14aa41a4651072cc95773d177ab771779b62849d 100644 --- a/crates/language_selector/Cargo.toml +++ b/crates/language_selector/Cargo.toml @@ -18,3 +18,4 @@ project = { path = "../project" } theme = { path = "../theme" } settings = { path = "../settings" } workspace = { path = "../workspace" } +anyhow = "1.0" diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index d48a50ce932abec0c6f3f9d98a8edbd450a28648..5a2918660e07297c9eac60bb2edc2fb924ddea78 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/crates/language_selector/src/language_selector.rs @@ -1,12 +1,14 @@ use std::sync::Arc; +use editor::Editor; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ - actions, elements::*, AnyViewHandle, AppContext, Entity, MouseState, MutableAppContext, - RenderContext, View, ViewContext, ViewHandle, + actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, + MutableAppContext, RenderContext, View, ViewContext, ViewHandle, }; -use language::LanguageRegistry; +use language::{Buffer, LanguageRegistry}; use picker::{Picker, PickerDelegate}; +use project::Project; use settings::Settings; use workspace::{AppState, Workspace}; @@ -27,32 +29,47 @@ pub enum Event { } pub struct LanguageSelector { + buffer: ModelHandle, + project: ModelHandle, language_registry: Arc, + candidates: Vec, matches: Vec, picker: ViewHandle>, selected_index: usize, } impl LanguageSelector { - fn new(language_registry: Arc, cx: &mut ViewContext) -> Self { + fn new( + buffer: ModelHandle, + project: ModelHandle, + language_registry: Arc, + cx: &mut ViewContext, + ) -> Self { let handle = cx.weak_handle(); let picker = cx.add_view(|cx| Picker::new("Select Language...", handle, cx)); - let mut matches = language_registry + let candidates = language_registry .language_names() .into_iter() .enumerate() - .map(|(candidate_id, name)| StringMatch { - candidate_id, - score: 0.0, + .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name)) + .collect::>(); + let mut matches = candidates + .iter() + .map(|candidate| StringMatch { + candidate_id: candidate.id, + score: 0., positions: Default::default(), - string: name, + string: candidate.string.clone(), }) .collect::>(); matches.sort_unstable_by(|mat1, mat2| mat1.string.cmp(&mat2.string)); Self { + buffer, + project, language_registry, + candidates, matches, picker, selected_index: 0, @@ -64,11 +81,18 @@ impl LanguageSelector { registry: Arc, cx: &mut ViewContext, ) { - workspace.toggle_modal(cx, |_, cx| { - let this = cx.add_view(|cx| Self::new(registry, cx)); - cx.subscribe(&this, Self::on_event).detach(); - this - }); + if let Some((_, buffer, _)) = workspace + .active_item(cx) + .and_then(|active_item| active_item.act_as::(cx)) + .and_then(|editor| editor.read(cx).active_excerpt(cx)) + { + workspace.toggle_modal(cx, |workspace, cx| { + let project = workspace.project().clone(); + let this = cx.add_view(|cx| Self::new(buffer, project, registry, cx)); + cx.subscribe(&this, Self::on_event).detach(); + this + }); + } } fn on_event( @@ -111,7 +135,21 @@ impl PickerDelegate for LanguageSelector { } fn confirm(&mut self, cx: &mut ViewContext) { - todo!(); + 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); + cx.spawn(|this, mut cx| async move { + let language = language.await?; + this.update(&mut cx, |this, cx| { + this.project.update(cx, |project, cx| { + project.set_language_for_buffer(&this.buffer, language, cx); + }); + }); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + cx.emit(Event::Dismissed); } @@ -123,24 +161,13 @@ impl PickerDelegate for LanguageSelector { self.selected_index } - fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext) { + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext) { self.selected_index = ix; } fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> gpui::Task<()> { let background = cx.background().clone(); - let candidates = self - .language_registry - .language_names() - .into_iter() - .enumerate() - .map(|(id, name)| StringMatchCandidate { - id, - char_bag: name.as_str().into(), - string: name.clone(), - }) - .collect::>(); - + let candidates = self.candidates.clone(); cx.spawn(|this, mut cx| async move { let matches = if query.is_empty() { candidates diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 003e4dd899afedafcd0eb780816b67c5108b9510..a164b5b885f927737efba79a5693ff22aa1500aa 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1464,7 +1464,7 @@ impl Project { }) .await?; this.update(&mut cx, |this, cx| { - this.assign_language_to_buffer(&buffer, cx); + this.detect_language_for_buffer(&buffer, cx); this.register_buffer_with_language_server(&buffer, cx); }); Ok(()) @@ -1531,7 +1531,7 @@ impl Project { }) .detach(); - self.assign_language_to_buffer(buffer, cx); + self.detect_language_for_buffer(buffer, cx); self.register_buffer_with_language_server(buffer, cx); cx.observe_release(buffer, |this, buffer, cx| { if let Some(file) = File::from_dyn(buffer.file()) { @@ -1818,7 +1818,7 @@ impl Project { } for buffer in plain_text_buffers { - project.assign_language_to_buffer(&buffer, cx); + project.detect_language_for_buffer(&buffer, cx); project.register_buffer_with_language_server(&buffer, cx); } @@ -1831,7 +1831,7 @@ impl Project { }) } - fn assign_language_to_buffer( + fn detect_language_for_buffer( &mut self, buffer: &ModelHandle, cx: &mut ModelContext, @@ -1843,6 +1843,16 @@ impl Project { .language_for_path(&full_path) .now_or_never()? .ok()?; + self.set_language_for_buffer(buffer, new_language, cx); + None + } + + pub fn set_language_for_buffer( + &mut self, + buffer: &ModelHandle, + new_language: Arc, + cx: &mut ModelContext, + ) { buffer.update(cx, |buffer, cx| { if buffer.language().map_or(true, |old_language| { !Arc::ptr_eq(old_language, &new_language) @@ -1851,13 +1861,13 @@ impl Project { } }); - let file = File::from_dyn(buffer.read(cx).file())?; - let worktree = file.worktree.read(cx).as_local()?; - let worktree_id = worktree.id(); - let worktree_abs_path = worktree.abs_path().clone(); - self.start_language_server(worktree_id, worktree_abs_path, new_language, cx); - - None + if let Some(file) = File::from_dyn(buffer.read(cx).file()) { + if let Some(worktree) = file.worktree.read(cx).as_local() { + let worktree_id = worktree.id(); + let worktree_abs_path = worktree.abs_path().clone(); + self.start_language_server(worktree_id, worktree_abs_path, new_language, cx); + } + } } fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) { @@ -4553,7 +4563,7 @@ impl Project { for (buffer, old_path) in renamed_buffers { self.unregister_buffer_from_language_server(&buffer, old_path, cx); - self.assign_language_to_buffer(&buffer, cx); + self.detect_language_for_buffer(&buffer, cx); self.register_buffer_with_language_server(&buffer, cx); } } @@ -5222,7 +5232,7 @@ impl Project { buffer.update(cx, |buffer, cx| { buffer.file_updated(Arc::new(file), cx).detach(); }); - this.assign_language_to_buffer(&buffer, cx); + this.detect_language_for_buffer(&buffer, cx); } Ok(()) }) From f28806d09ba0f79e27c4c32c10b1cc844b157c98 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 10 Mar 2023 15:48:39 +0100 Subject: [PATCH 3/4] Emphasize currently-selected language --- crates/language_selector/src/language_selector.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index 5a2918660e07297c9eac60bb2edc2fb924ddea78..786dd5abe0ba92a6425cc94e197bc4fc56bb2762 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/crates/language_selector/src/language_selector.rs @@ -211,11 +211,16 @@ impl PickerDelegate for LanguageSelector { ) -> ElementBox { let settings = cx.global::(); let theme = &settings.theme; - let theme_match = &self.matches[ix]; + let mat = &self.matches[ix]; let style = theme.picker.item.style_for(mouse_state, selected); + let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name()); + let mut label = mat.string.clone(); + if buffer_language_name.as_deref() == Some(mat.string.as_str()) { + label.push_str(" (current)"); + } - Label::new(theme_match.string.clone(), style.label.clone()) - .with_highlights(theme_match.positions.clone()) + Label::new(label, style.label.clone()) + .with_highlights(mat.positions.clone()) .contained() .with_style(style.container) .boxed() From ce828d55d5669eae01781aafa5d3a65cbd52d313 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 10 Mar 2023 16:32:18 +0100 Subject: [PATCH 4/4] Bind `language_selector::Toggle` to `cmd-k m` Co-Authored-By: Nathan Sobo --- assets/keymaps/default.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index cce65eda8abcadcb632d1b496c10b2dc283dc548..57f5075aca9a6678bb6a7231d8fdd5a3036c5aef 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -353,7 +353,8 @@ "cmd-shift-p": "command_palette::Toggle", "cmd-shift-m": "diagnostics::Deploy", "cmd-shift-e": "project_panel::ToggleFocus", - "cmd-alt-s": "workspace::SaveAll" + "cmd-alt-s": "workspace::SaveAll", + "cmd-k m": "language_selector::Toggle" } }, // Bindings from Sublime Text @@ -537,4 +538,4 @@ ] } } -] \ No newline at end of file +]