diff --git a/Cargo.lock b/Cargo.lock index 87e32ed97d..94e7af6766 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 c74a76ccce..63882573ab 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 0000000000..60ed0e6633 --- /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 0000000000..d48a50ce93 --- /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 8060c2af11..a7c4861e02 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 b9e3ed550b..655e3968cf 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);