Add language icons to the language selector (#21298)

Closes https://github.com/zed-industries/zed/issues/21290

This is a first attempt to show the language icons to the selector.
Ideally, I wouldn't like to have yet another place mapping extensions to
icons, as we already have the `file_types.json` file doing that, but I'm
not so sure how to pull from it yet. Maybe in a future pass we'll
improve this and make it more solid.

<img width="700" alt="Screenshot 2024-11-28 at 16 10 27"
src="https://github.com/user-attachments/assets/683c3bef-5389-470f-a41e-3d510b927b61">

Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
This commit is contained in:
Danilo Leal 2024-12-02 15:01:09 -03:00 committed by GitHub
parent 995b40f149
commit f795ce9623
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 119 additions and 18 deletions

3
Cargo.lock generated
View file

@ -6709,11 +6709,14 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"editor", "editor",
"file_finder",
"file_icons",
"fuzzy", "fuzzy",
"gpui", "gpui",
"language", "language",
"picker", "picker",
"project", "project",
"settings",
"ui", "ui",
"util", "util",
"workspace", "workspace",

View file

@ -0,0 +1,5 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 3L8.5 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 6.5H12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 13H12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 415 B

View file

@ -34,6 +34,7 @@
"dat": "storage", "dat": "storage",
"db": "storage", "db": "storage",
"dbf": "storage", "dbf": "storage",
"diff": "diff",
"dll": "storage", "dll": "storage",
"doc": "document", "doc": "document",
"docx": "document", "docx": "document",
@ -112,6 +113,7 @@
"mkv": "video", "mkv": "video",
"ml": "ocaml", "ml": "ocaml",
"mli": "ocaml", "mli": "ocaml",
"mod": "go",
"mov": "video", "mov": "video",
"mp3": "audio", "mp3": "audio",
"mp4": "video", "mp4": "video",
@ -185,6 +187,7 @@
"wmv": "video", "wmv": "video",
"woff": "font", "woff": "font",
"woff2": "font", "woff2": "font",
"work": "go",
"wv": "audio", "wv": "audio",
"xls": "document", "xls": "document",
"xlsx": "document", "xlsx": "document",
@ -239,6 +242,9 @@
"default": { "default": {
"icon": "icons/file_icons/file.svg" "icon": "icons/file_icons/file.svg"
}, },
"diff": {
"icon": "icons/file_icons/diff.svg"
},
"docker": { "docker": {
"icon": "icons/file_icons/docker.svg" "icon": "icons/file_icons/docker.svg"
}, },

View file

@ -159,6 +159,7 @@ pub trait ExtensionLanguageProxy: Send + Sync + 'static {
language: LanguageName, language: LanguageName,
grammar: Option<Arc<str>>, grammar: Option<Arc<str>>,
matcher: LanguageMatcher, matcher: LanguageMatcher,
hidden: bool,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>, load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
); );
@ -175,13 +176,14 @@ impl ExtensionLanguageProxy for ExtensionHostProxy {
language: LanguageName, language: LanguageName,
grammar: Option<Arc<str>>, grammar: Option<Arc<str>>,
matcher: LanguageMatcher, matcher: LanguageMatcher,
hidden: bool,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>, load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
) { ) {
let Some(proxy) = self.language_proxy.read().clone() else { let Some(proxy) = self.language_proxy.read().clone() else {
return; return;
}; };
proxy.register_language(language, grammar, matcher, load) proxy.register_language(language, grammar, matcher, hidden, load)
} }
fn remove_languages( fn remove_languages(

View file

@ -162,6 +162,7 @@ pub struct ExtensionIndexLanguageEntry {
pub extension: Arc<str>, pub extension: Arc<str>,
pub path: PathBuf, pub path: PathBuf,
pub matcher: LanguageMatcher, pub matcher: LanguageMatcher,
pub hidden: bool,
pub grammar: Option<Arc<str>>, pub grammar: Option<Arc<str>>,
} }
@ -1097,6 +1098,7 @@ impl ExtensionStore {
language_name.clone(), language_name.clone(),
language.grammar.clone(), language.grammar.clone(),
language.matcher.clone(), language.matcher.clone(),
language.hidden,
Arc::new(move || { Arc::new(move || {
let config = std::fs::read_to_string(language_path.join("config.toml"))?; let config = std::fs::read_to_string(language_path.join("config.toml"))?;
let config: LanguageConfig = ::toml::from_str(&config)?; let config: LanguageConfig = ::toml::from_str(&config)?;
@ -1324,6 +1326,7 @@ impl ExtensionStore {
extension: extension_id.clone(), extension: extension_id.clone(),
path: relative_path, path: relative_path,
matcher: config.matcher, matcher: config.matcher,
hidden: config.hidden,
grammar: config.grammar, grammar: config.grammar,
}, },
); );

View file

@ -203,6 +203,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
extension: "zed-ruby".into(), extension: "zed-ruby".into(),
path: "languages/erb".into(), path: "languages/erb".into(),
grammar: Some("embedded_template".into()), grammar: Some("embedded_template".into()),
hidden: false,
matcher: LanguageMatcher { matcher: LanguageMatcher {
path_suffixes: vec!["erb".into()], path_suffixes: vec!["erb".into()],
first_line_pattern: None, first_line_pattern: None,
@ -215,6 +216,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
extension: "zed-ruby".into(), extension: "zed-ruby".into(),
path: "languages/ruby".into(), path: "languages/ruby".into(),
grammar: Some("ruby".into()), grammar: Some("ruby".into()),
hidden: false,
matcher: LanguageMatcher { matcher: LanguageMatcher {
path_suffixes: vec!["rb".into()], path_suffixes: vec!["rb".into()],
first_line_pattern: None, first_line_pattern: None,

View file

@ -156,6 +156,7 @@ impl HeadlessExtensionStore {
config.name.clone(), config.name.clone(),
None, None,
config.matcher.clone(), config.matcher.clone(),
config.hidden,
Arc::new(move || { Arc::new(move || {
Ok(LoadedLanguage { Ok(LoadedLanguage {
config: config.clone(), config: config.clone(),

View file

@ -1,7 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod file_finder_tests; mod file_finder_tests;
mod file_finder_settings; pub mod file_finder_settings;
mod new_path_prompt; mod new_path_prompt;
mod open_path_prompt; mod open_path_prompt;

View file

@ -129,6 +129,10 @@ pub static PLAIN_TEXT: LazyLock<Arc<Language>> = LazyLock::new(|| {
LanguageConfig { LanguageConfig {
name: "Plain Text".into(), name: "Plain Text".into(),
soft_wrap: Some(SoftWrap::EditorWidth), soft_wrap: Some(SoftWrap::EditorWidth),
matcher: LanguageMatcher {
path_suffixes: vec!["txt".to_owned()],
first_line_pattern: None,
},
..Default::default() ..Default::default()
}, },
None, None,
@ -1418,6 +1422,10 @@ impl Language {
pub fn prettier_parser_name(&self) -> Option<&str> { pub fn prettier_parser_name(&self) -> Option<&str> {
self.config.prettier_parser_name.as_deref() self.config.prettier_parser_name.as_deref()
} }
pub fn config(&self) -> &LanguageConfig {
&self.config
}
} }
impl LanguageScope { impl LanguageScope {

View file

@ -130,6 +130,7 @@ pub struct AvailableLanguage {
name: LanguageName, name: LanguageName,
grammar: Option<Arc<str>>, grammar: Option<Arc<str>>,
matcher: LanguageMatcher, matcher: LanguageMatcher,
hidden: bool,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>, load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
loaded: bool, loaded: bool,
} }
@ -142,6 +143,9 @@ impl AvailableLanguage {
pub fn matcher(&self) -> &LanguageMatcher { pub fn matcher(&self) -> &LanguageMatcher {
&self.matcher &self.matcher
} }
pub fn hidden(&self) -> bool {
self.hidden
}
} }
enum AvailableGrammar { enum AvailableGrammar {
@ -288,6 +292,7 @@ impl LanguageRegistry {
config.name.clone(), config.name.clone(),
config.grammar.clone(), config.grammar.clone(),
config.matcher.clone(), config.matcher.clone(),
config.hidden,
Arc::new(move || { Arc::new(move || {
Ok(LoadedLanguage { Ok(LoadedLanguage {
config: config.clone(), config: config.clone(),
@ -436,6 +441,7 @@ impl LanguageRegistry {
name: LanguageName, name: LanguageName,
grammar_name: Option<Arc<str>>, grammar_name: Option<Arc<str>>,
matcher: LanguageMatcher, matcher: LanguageMatcher,
hidden: bool,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>, load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
) { ) {
let state = &mut *self.state.write(); let state = &mut *self.state.write();
@ -455,6 +461,7 @@ impl LanguageRegistry {
grammar: grammar_name, grammar: grammar_name,
matcher, matcher,
load, load,
hidden,
loaded: false, loaded: false,
}); });
state.version += 1; state.version += 1;
@ -522,6 +529,7 @@ impl LanguageRegistry {
name: language.name(), name: language.name(),
grammar: language.config.grammar.clone(), grammar: language.config.grammar.clone(),
matcher: language.config.matcher.clone(), matcher: language.config.matcher.clone(),
hidden: language.config.hidden,
load: Arc::new(|| Err(anyhow!("already loaded"))), load: Arc::new(|| Err(anyhow!("already loaded"))),
loaded: true, loaded: true,
}); });
@ -590,15 +598,12 @@ impl LanguageRegistry {
async move { rx.await? } async move { rx.await? }
} }
pub fn available_language_for_name( pub fn available_language_for_name(self: &Arc<Self>, name: &str) -> Option<AvailableLanguage> {
self: &Arc<Self>,
name: &LanguageName,
) -> Option<AvailableLanguage> {
let state = self.state.read(); let state = self.state.read();
state state
.available_languages .available_languages
.iter() .iter()
.find(|l| &l.name == name) .find(|l| l.name.0.as_ref() == name)
.cloned() .cloned()
} }

View file

@ -34,10 +34,11 @@ impl ExtensionLanguageProxy for LanguageServerRegistryProxy {
language: LanguageName, language: LanguageName,
grammar: Option<Arc<str>>, grammar: Option<Arc<str>>,
matcher: LanguageMatcher, matcher: LanguageMatcher,
hidden: bool,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>, load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
) { ) {
self.language_registry self.language_registry
.register_language(language, grammar, matcher, load); .register_language(language, grammar, matcher, hidden, load);
} }
fn remove_languages( fn remove_languages(

View file

@ -15,11 +15,14 @@ doctest = false
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
editor.workspace = true editor.workspace = true
file_finder.workspace = true
file_icons.workspace = true
fuzzy.workspace = true fuzzy.workspace = true
gpui.workspace = true gpui.workspace = true
language.workspace = true language.workspace = true
picker.workspace = true picker.workspace = true
project.workspace = true project.workspace = true
settings.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true
workspace.workspace = true workspace.workspace = true

View file

@ -3,15 +3,18 @@ mod active_buffer_language;
pub use active_buffer_language::ActiveBufferLanguage; pub use active_buffer_language::ActiveBufferLanguage;
use anyhow::anyhow; use anyhow::anyhow;
use editor::Editor; use editor::Editor;
use file_finder::file_finder_settings::FileFinderSettings;
use file_icons::FileIcons;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
actions, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, actions, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
}; };
use language::{Buffer, LanguageRegistry}; use language::{Buffer, LanguageMatcher, LanguageName, LanguageRegistry};
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use project::Project; use project::Project;
use std::sync::Arc; use settings::Settings;
use std::{ops::Not as _, path::Path, sync::Arc};
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt; use util::ResultExt;
use workspace::{ModalView, Workspace}; use workspace::{ModalView, Workspace};
@ -102,7 +105,13 @@ impl LanguageSelectorDelegate {
.language_names() .language_names()
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name)) .filter_map(|(candidate_id, name)| {
language_registry
.available_language_for_name(&name)?
.hidden()
.not()
.then(|| StringMatchCandidate::new(candidate_id, name))
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Self { Self {
@ -115,13 +124,64 @@ impl LanguageSelectorDelegate {
selected_index: 0, selected_index: 0,
} }
} }
fn language_data_for_match(
&self,
mat: &StringMatch,
cx: &AppContext,
) -> (String, Option<Icon>) {
let mut label = mat.string.clone();
let buffer_language = self.buffer.read(cx).language();
let need_icon = FileFinderSettings::get_global(cx).file_icons;
if let Some(buffer_language) = buffer_language {
let buffer_language_name = buffer_language.name();
if buffer_language_name.0.as_ref() == mat.string.as_str() {
label.push_str(" (current)");
let icon = need_icon
.then(|| self.language_icon(&buffer_language.config().matcher, cx))
.flatten();
return (label, icon);
}
}
if need_icon {
let language_name = LanguageName::new(mat.string.as_str());
match self
.language_registry
.available_language_for_name(&language_name.0)
{
Some(available_language) => {
let icon = self.language_icon(available_language.matcher(), cx);
(label, icon)
}
None => (label, None),
}
} else {
(label, None)
}
}
fn language_icon(&self, matcher: &LanguageMatcher, cx: &AppContext) -> Option<Icon> {
matcher
.path_suffixes
.iter()
.find_map(|extension| {
if extension.contains('.') {
None
} else {
FileIcons::get_icon(Path::new(&format!("file.{extension}")), cx)
}
})
.map(Icon::from_path)
.map(|icon| icon.color(Color::Muted))
}
} }
impl PickerDelegate for LanguageSelectorDelegate { impl PickerDelegate for LanguageSelectorDelegate {
type ListItem = ListItem; type ListItem = ListItem;
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> { fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Select a language...".into() "Select a language".into()
} }
fn match_count(&self) -> usize { fn match_count(&self) -> usize {
@ -215,17 +275,13 @@ impl PickerDelegate for LanguageSelectorDelegate {
cx: &mut ViewContext<Picker<Self>>, cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> { ) -> Option<Self::ListItem> {
let mat = &self.matches[ix]; let mat = &self.matches[ix];
let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name()); let (label, language_icon) = self.language_data_for_match(mat, cx);
let mut label = mat.string.clone();
if buffer_language_name.map(|n| n.0).as_deref() == Some(mat.string.as_str()) {
label.push_str(" (current)");
}
Some( Some(
ListItem::new(ix) ListItem::new(ix)
.inset(true) .inset(true)
.spacing(ListItemSpacing::Sparse) .spacing(ListItemSpacing::Sparse)
.selected(selected) .selected(selected)
.start_slot::<Icon>(language_icon)
.child(HighlightedLabel::new(label, mat.positions.clone())), .child(HighlightedLabel::new(label, mat.positions.clone())),
) )
} }

View file

@ -5,3 +5,4 @@ brackets = [
{ start = "{", end = "}", close = true, newline = false }, { start = "{", end = "}", close = true, newline = false },
{ start = "[", end = "]", close = true, newline = false }, { start = "[", end = "]", close = true, newline = false },
] ]
hidden = true

View file

@ -62,6 +62,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
config.name.clone(), config.name.clone(),
config.grammar.clone(), config.grammar.clone(),
config.matcher.clone(), config.matcher.clone(),
config.hidden,
Arc::new(move || { Arc::new(move || {
Ok(LoadedLanguage { Ok(LoadedLanguage {
config: config.clone(), config: config.clone(),
@ -83,6 +84,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
config.name.clone(), config.name.clone(),
config.grammar.clone(), config.grammar.clone(),
config.matcher.clone(), config.matcher.clone(),
config.hidden,
Arc::new(move || { Arc::new(move || {
Ok(LoadedLanguage { Ok(LoadedLanguage {
config: config.clone(), config: config.clone(),
@ -104,6 +106,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
config.name.clone(), config.name.clone(),
config.grammar.clone(), config.grammar.clone(),
config.matcher.clone(), config.matcher.clone(),
config.hidden,
Arc::new(move || { Arc::new(move || {
Ok(LoadedLanguage { Ok(LoadedLanguage {
config: config.clone(), config: config.clone(),
@ -125,6 +128,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
config.name.clone(), config.name.clone(),
config.grammar.clone(), config.grammar.clone(),
config.matcher.clone(), config.matcher.clone(),
config.hidden,
Arc::new(move || { Arc::new(move || {
Ok(LoadedLanguage { Ok(LoadedLanguage {
config: config.clone(), config: config.clone(),

View file

@ -6,3 +6,4 @@ brackets = [
{ start = "{", end = "}", close = true, newline = false }, { start = "{", end = "}", close = true, newline = false },
{ start = "[", end = "]", close = true, newline = false }, { start = "[", end = "]", close = true, newline = false },
] ]
hidden = true