diff --git a/Cargo.lock b/Cargo.lock index 7e56390089..5911848611 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4791,6 +4791,23 @@ dependencies = [ "workspace", ] +[[package]] +name = "language_selector2" +version = "0.1.0" +dependencies = [ + "anyhow", + "editor2", + "fuzzy2", + "gpui2", + "language2", + "picker2", + "project2", + "settings2", + "theme2", + "util", + "workspace2", +] + [[package]] name = "language_tools" version = "0.1.0" @@ -11772,6 +11789,7 @@ dependencies = [ "isahc", "journal2", "language2", + "language_selector2", "lazy_static", "libc", "log", diff --git a/Cargo.toml b/Cargo.toml index cc8264f697..0a7e4aa18f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ members = [ "crates/language", "crates/language2", "crates/language_selector", + "crates/language_selector2", "crates/language_tools", "crates/live_kit_client", "crates/live_kit_server", diff --git a/crates/language_selector2/Cargo.toml b/crates/language_selector2/Cargo.toml new file mode 100644 index 0000000000..a588dccde8 --- /dev/null +++ b/crates/language_selector2/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "language_selector2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/language_selector.rs" +doctest = false + +[dependencies] +editor2 = { package = "editor2", path = "../editor2" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +language = { package = "language2", path = "../language2" } +gpui = { package = "gpui2", path = "../gpui2" } +picker = { package = "picker2", path = "../picker2" } +project = { package = "project2", path = "../project2" } +theme = { package = "theme2", path = "../theme2" } +settings = { package = "settings2", path = "../settings2" } +util = { path = "../util" } +workspace = { package = "workspace2", path = "../workspace2" } +anyhow.workspace = true + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } diff --git a/crates/language_selector2/src/active_buffer_language.rs b/crates/language_selector2/src/active_buffer_language.rs new file mode 100644 index 0000000000..01333c1ffb --- /dev/null +++ b/crates/language_selector2/src/active_buffer_language.rs @@ -0,0 +1,96 @@ +use editor::Editor; +use gpui::{ + elements::*, + platform::{CursorStyle, MouseButton}, + Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, +}; +use std::sync::Arc; +use workspace::{item::ItemHandle, StatusItemView, Workspace}; + +pub struct ActiveBufferLanguage { + active_language: Option>>, + workspace: WeakViewHandle, + _observe_active_editor: Option, +} + +impl ActiveBufferLanguage { + pub fn new(workspace: &Workspace) -> Self { + Self { + active_language: None, + workspace: workspace.weak_handle(), + _observe_active_editor: None, + } + } + + fn update_language(&mut self, editor: ViewHandle, cx: &mut ViewContext) { + self.active_language = Some(None); + + let editor = editor.read(cx); + if let Some((_, buffer, _)) = editor.active_excerpt(cx) { + if let Some(language) = buffer.read(cx).language() { + self.active_language = Some(Some(language.name())); + } + } + + cx.notify(); + } +} + +impl Entity for ActiveBufferLanguage { + type Event = (); +} + +impl View for ActiveBufferLanguage { + fn ui_name() -> &'static str { + "ActiveBufferLanguage" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + if let Some(active_language) = self.active_language.as_ref() { + let active_language_text = if let Some(active_language_text) = active_language { + active_language_text.to_string() + } else { + "Unknown".to_string() + }; + let theme = theme::current(cx).clone(); + + MouseEventHandler::new::(0, cx, |state, cx| { + let theme = &theme::current(cx).workspace.status_bar; + let style = theme.active_language.style_for(state); + Label::new(active_language_text, style.text.clone()) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + crate::toggle(workspace, &Default::default(), cx) + }); + } + }) + .with_tooltip::(0, "Select Language", None, theme.tooltip.clone(), cx) + .into_any() + } else { + Empty::new().into_any() + } + } +} + +impl StatusItemView for ActiveBufferLanguage { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) { + if let Some(editor) = active_pane_item.and_then(|item| item.act_as::(cx)) { + self._observe_active_editor = Some(cx.observe(&editor, Self::update_language)); + self.update_language(editor, cx); + } else { + self.active_language = None; + self._observe_active_editor = None; + } + + cx.notify(); + } +} diff --git a/crates/language_selector2/src/language_selector.rs b/crates/language_selector2/src/language_selector.rs new file mode 100644 index 0000000000..00379919d5 --- /dev/null +++ b/crates/language_selector2/src/language_selector.rs @@ -0,0 +1,200 @@ +mod active_buffer_language; + +pub use active_buffer_language::ActiveBufferLanguage; +use anyhow::anyhow; +use editor::Editor; +use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; +use gpui::{actions, elements::*, AppContext, ModelHandle, MouseState, ViewContext}; +use language::{Buffer, LanguageRegistry}; +use picker::{Picker, PickerDelegate, PickerEvent}; +use project::Project; +use std::sync::Arc; +use util::ResultExt; +use workspace::Workspace; + +actions!(Toggle); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(LanguagePicker::register).detach(); +} + +pub fn init(cx: &mut AppContext) { + Picker::::init(cx); + cx.add_action(toggle); +} + +pub fn toggle( + workspace: &mut Workspace, + _: &Toggle, + cx: &mut ViewContext, +) -> Option<()> { + let (_, buffer, _) = workspace + .active_item(cx)? + .act_as::(cx)? + .read(cx) + .active_excerpt(cx)?; + workspace.toggle_modal(cx, |workspace, cx| { + let registry = workspace.app_state().languages.clone(); + cx.add_view(|cx| { + Picker::new( + LanguageSelectorDelegate::new(buffer, workspace.project().clone(), registry), + cx, + ) + }) + }); + Some(()) +} + +pub struct LanguageSelectorDelegate { + buffer: ModelHandle, + project: ModelHandle, + language_registry: Arc, + candidates: Vec, + matches: Vec, + selected_index: usize, +} + +impl LanguageSelectorDelegate { + fn new( + buffer: ModelHandle, + project: ModelHandle, + language_registry: Arc, + ) -> Self { + let candidates = language_registry + .language_names() + .into_iter() + .enumerate() + .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: candidate.string.clone(), + }) + .collect::>(); + matches.sort_unstable_by(|mat1, mat2| mat1.string.cmp(&mat2.string)); + + Self { + buffer, + project, + language_registry, + candidates, + matches, + selected_index: 0, + } + } +} + +impl PickerDelegate for LanguageSelectorDelegate { + fn placeholder_text(&self) -> Arc { + "Select a language...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + 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); + let project = self.project.downgrade(); + let buffer = self.buffer.downgrade(); + cx.spawn(|_, mut cx| async move { + let language = language.await?; + let project = project + .upgrade(&cx) + .ok_or_else(|| anyhow!("project was dropped"))?; + let buffer = buffer + .upgrade(&cx) + .ok_or_else(|| anyhow!("buffer was dropped"))?; + project.update(&mut cx, |project, cx| { + project.set_language_for_buffer(&buffer, language, cx); + }); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + cx.emit(PickerEvent::Dismiss); + } + + fn dismissed(&mut self, _cx: &mut ViewContext>) {} + + fn selected_index(&self) -> usize { + self.selected_index + } + + 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.candidates.clone(); + 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| { + let delegate = this.delegate_mut(); + delegate.matches = matches; + delegate.selected_index = delegate + .selected_index + .min(delegate.matches.len().saturating_sub(1)); + cx.notify(); + }) + .log_err(); + }) + } + + fn render_match( + &self, + ix: usize, + mouse_state: &mut MouseState, + selected: bool, + cx: &AppContext, + ) -> AnyElement> { + let theme = theme::current(cx); + let mat = &self.matches[ix]; + let style = theme.picker.item.in_state(selected).style_for(mouse_state); + 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(label, style.label.clone()) + .with_highlights(mat.positions.clone()) + .contained() + .with_style(style.container) + .into_any() + } +} diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 1c3c71a259..e72d4671ef 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -44,7 +44,7 @@ gpui = { package = "gpui2", path = "../gpui2" } install_cli = { package = "install_cli2", path = "../install_cli2" } journal = { package = "journal2", path = "../journal2" } language = { package = "language2", path = "../language2" } -# language_selector = { path = "../language_selector" } +language_selector = { package = "language_selector2", path = "../language_selector2" } lsp = { package = "lsp2", path = "../lsp2" } menu = { package = "menu2", path = "../menu2" } # language_tools = { path = "../language_tools" } diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index b897687489..63469a9132 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -142,8 +142,8 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { cx.build_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); let activity_indicator = activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx); - // let active_buffer_language = - // cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace)); + let active_buffer_language = + cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace)); // let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx)); // let feedback_button = cx.add_view(|_| { // feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace) @@ -155,7 +155,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { // status_bar.add_right_item(feedback_button, cx); // status_bar.add_right_item(copilot, cx); - // status_bar.add_right_item(active_buffer_language, cx); + status_bar.add_right_item(active_buffer_language, cx); // status_bar.add_right_item(vim_mode_indicator, cx); status_bar.add_right_item(cursor_position, cx); });