Detailed changes
@@ -3286,6 +3286,22 @@ dependencies = [
"util",
]
+[[package]]
+name = "language_selector"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "editor",
+ "fuzzy",
+ "gpui",
+ "language",
+ "picker",
+ "project",
+ "settings",
+ "theme",
+ "workspace",
+]
+
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -8399,6 +8415,7 @@ dependencies = [
"isahc",
"journal",
"language",
+ "language_selector",
"lazy_static",
"libc",
"log",
@@ -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",
@@ -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 @@
]
}
}
-]
+]
@@ -1254,6 +1254,15 @@ impl Editor {
self.buffer.read(cx).language_at(point, cx)
}
+ pub fn active_excerpt(
+ &self,
+ cx: &AppContext,
+ ) -> Option<(ExcerptId, ModelHandle<Buffer>, Range<text::Anchor>)> {
+ self.buffer
+ .read(cx)
+ .excerpt_containing(self.selections.newest_anchor().head(), cx)
+ }
+
fn style(&self, cx: &AppContext) -> EditorStyle {
build_style(
cx.global::<Settings>(),
@@ -0,0 +1,21 @@
+[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" }
+anyhow = "1.0"
@@ -0,0 +1,228 @@
+use std::sync::Arc;
+
+use editor::Editor;
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
+use gpui::{
+ actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState,
+ MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
+};
+use language::{Buffer, LanguageRegistry};
+use picker::{Picker, PickerDelegate};
+use project::Project;
+use settings::Settings;
+use workspace::{AppState, Workspace};
+
+actions!(language_selector, [Toggle]);
+
+pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
+ Picker::<LanguageSelector>::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 {
+ buffer: ModelHandle<Buffer>,
+ project: ModelHandle<Project>,
+ language_registry: Arc<LanguageRegistry>,
+ candidates: Vec<StringMatchCandidate>,
+ matches: Vec<StringMatch>,
+ picker: ViewHandle<Picker<Self>>,
+ selected_index: usize,
+}
+
+impl LanguageSelector {
+ fn new(
+ buffer: ModelHandle<Buffer>,
+ project: ModelHandle<Project>,
+ language_registry: Arc<LanguageRegistry>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ let handle = cx.weak_handle();
+ let picker = cx.add_view(|cx| Picker::new("Select Language...", handle, cx));
+
+ let candidates = language_registry
+ .language_names()
+ .into_iter()
+ .enumerate()
+ .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
+ .collect::<Vec<_>>();
+ let mut matches = candidates
+ .iter()
+ .map(|candidate| StringMatch {
+ candidate_id: candidate.id,
+ score: 0.,
+ positions: Default::default(),
+ string: candidate.string.clone(),
+ })
+ .collect::<Vec<_>>();
+ matches.sort_unstable_by(|mat1, mat2| mat1.string.cmp(&mat2.string));
+
+ Self {
+ buffer,
+ project,
+ language_registry,
+ candidates,
+ matches,
+ picker,
+ selected_index: 0,
+ }
+ }
+
+ fn toggle(
+ workspace: &mut Workspace,
+ registry: Arc<LanguageRegistry>,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ if let Some((_, buffer, _)) = workspace
+ .active_item(cx)
+ .and_then(|active_item| active_item.act_as::<Editor>(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(
+ workspace: &mut Workspace,
+ _: ViewHandle<LanguageSelector>,
+ event: &Event,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ 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<Self>) -> ElementBox {
+ ChildView::new(self.picker.clone(), cx).boxed()
+ }
+
+ fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+ 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<Self>) {
+ 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);
+ }
+
+ fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+ cx.emit(Event::Dismissed);
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
+ self.selected_index = ix;
+ }
+
+ fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> 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| {
+ 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::<Settings>();
+ let theme = &settings.theme;
+ 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(label, style.label.clone())
+ .with_highlights(mat.positions.clone())
+ .contained()
+ .with_style(style.container)
+ .boxed()
+ }
+}
@@ -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<Buffer>,
cx: &mut ModelContext<Self>,
@@ -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<Buffer>,
+ new_language: Arc<Language>,
+ cx: &mut ModelContext<Self>,
+ ) {
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(())
})
@@ -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" }
@@ -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);