mirror of
https://github.com/enso-org/enso.git
synced 2024-11-25 21:25:20 +03:00
Show custom icons in Component Browser (#3606)
Show custom icons in Component Browser for entries that have a non-empty `Icon` section in their docs with the section's body containing a name of a predefined icon. https://www.pivotaltracker.com/story/show/182584336 #### Visuals A screenshot of a couple custom icons in the Component Browser: <img width="346" alt="Screenshot 2022-07-27 at 15 55 33" src="https://user-images.githubusercontent.com/273837/181265249-d57f861f-8095-4933-9ef6-e62644e11da3.png"> # Important Notes - The PR assigns icon names to four items in the standard library, but only three of them are shown in the Component Browser because of [a parsing bug in the Engine](https://www.pivotaltracker.com/story/show/182781673). - Icon names are assigned only to four items in the standard library because only two currently predefined icons match entries in the currently defined Virtual Component Groups. Adjusting the definitions of icons and Virtual Component Groups is covered by [a different task](https://www.pivotaltracker.com/story/show/182584311). - A bug in the documentation of the Enso protocol message `DocSection` is fixed. A `text` field in the `Tag` interface is renamed to `body` (this is the field name used in Engine).
This commit is contained in:
parent
7f8190e663
commit
c6835d2de7
@ -776,6 +776,63 @@ pub enum RegisterOptions {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===================
|
||||
// === Doc Section ===
|
||||
// ===================
|
||||
|
||||
/// Text rendered as HTML (may contain HTML tags).
|
||||
pub type HtmlString = String;
|
||||
|
||||
/// Documentation section mark.
|
||||
#[derive(Hash, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Mark {
|
||||
Important,
|
||||
Info,
|
||||
Example,
|
||||
}
|
||||
|
||||
/// A single section of the documentation.
|
||||
#[derive(Hash, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[allow(missing_docs)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum DocSection {
|
||||
/// The documentation tag.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Tag {
|
||||
/// The tag name.
|
||||
name: String,
|
||||
/// The tag text.
|
||||
body: HtmlString,
|
||||
},
|
||||
/// The paragraph of the text.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Paragraph {
|
||||
/// The elements that make up this paragraph.
|
||||
body: HtmlString,
|
||||
},
|
||||
/// The section that starts with the key followed by the colon and the body.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Keyed {
|
||||
/// The section key.
|
||||
key: String,
|
||||
/// The elements that make up the body of the section.
|
||||
body: HtmlString,
|
||||
},
|
||||
/// The section that starts with the mark followed by the header and the body.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Marked {
|
||||
/// The section mark.
|
||||
mark: Mark,
|
||||
/// The section header.
|
||||
header: Option<String>,
|
||||
/// The elements that make up the body of the section.
|
||||
body: HtmlString,
|
||||
},
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// === Suggestion Database ===
|
||||
// ===========================
|
||||
@ -844,24 +901,28 @@ pub enum SuggestionEntryType {
|
||||
pub enum SuggestionEntry {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Atom {
|
||||
external_id: Option<Uuid>,
|
||||
name: String,
|
||||
module: String,
|
||||
arguments: Vec<SuggestionEntryArgument>,
|
||||
return_type: String,
|
||||
documentation: Option<String>,
|
||||
documentation_html: Option<String>,
|
||||
external_id: Option<Uuid>,
|
||||
name: String,
|
||||
module: String,
|
||||
arguments: Vec<SuggestionEntryArgument>,
|
||||
return_type: String,
|
||||
documentation: Option<String>,
|
||||
documentation_html: Option<String>,
|
||||
#[serde(default, deserialize_with = "enso_prelude::deserialize_null_as_default")]
|
||||
documentation_sections: Vec<DocSection>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Method {
|
||||
external_id: Option<Uuid>,
|
||||
name: String,
|
||||
module: String,
|
||||
arguments: Vec<SuggestionEntryArgument>,
|
||||
self_type: String,
|
||||
return_type: String,
|
||||
documentation: Option<String>,
|
||||
documentation_html: Option<String>,
|
||||
external_id: Option<Uuid>,
|
||||
name: String,
|
||||
module: String,
|
||||
arguments: Vec<SuggestionEntryArgument>,
|
||||
self_type: String,
|
||||
return_type: String,
|
||||
documentation: Option<String>,
|
||||
documentation_html: Option<String>,
|
||||
#[serde(default, deserialize_with = "enso_prelude::deserialize_null_as_default")]
|
||||
documentation_sections: Vec<DocSection>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Function {
|
||||
@ -882,10 +943,12 @@ pub enum SuggestionEntry {
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Module {
|
||||
module: String,
|
||||
documentation: Option<String>,
|
||||
documentation_html: Option<String>,
|
||||
reexport: Option<String>,
|
||||
module: String,
|
||||
documentation: Option<String>,
|
||||
documentation_html: Option<String>,
|
||||
reexport: Option<String>,
|
||||
#[serde(default, deserialize_with = "enso_prelude::deserialize_null_as_default")]
|
||||
documentation_sections: Vec<DocSection>,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -1150,6 +1150,7 @@ impl Searcher {
|
||||
documentation_html: None,
|
||||
self_type: Some(self_type.clone()),
|
||||
scope: model::suggestion_database::entry::Scope::Everywhere,
|
||||
icon_name: None,
|
||||
};
|
||||
let action = Action::Suggestion(action::Suggestion::FromDatabase(Rc::new(entry)));
|
||||
libraries_cat_builder.add_action(action);
|
||||
@ -1492,6 +1493,7 @@ pub mod test {
|
||||
documentation_html: default(),
|
||||
self_type: None,
|
||||
scope,
|
||||
icon_name: None,
|
||||
};
|
||||
let entry2 = model::suggestion_database::Entry {
|
||||
name: "TestVar1".to_string(),
|
||||
@ -1551,6 +1553,7 @@ pub mod test {
|
||||
documentation_html: None,
|
||||
self_type: None,
|
||||
scope: Scope::Everywhere,
|
||||
icon_name: None,
|
||||
};
|
||||
let entry9 = model::suggestion_database::Entry {
|
||||
name: "testFunction2".to_string(),
|
||||
|
@ -247,10 +247,11 @@ pub(crate) mod tests {
|
||||
|
||||
pub fn mock_module(name: &str) -> model::suggestion_database::Entry {
|
||||
let ls_entry = language_server::SuggestionEntry::Module {
|
||||
module: name.to_owned(),
|
||||
documentation: default(),
|
||||
documentation_html: default(),
|
||||
reexport: default(),
|
||||
module: name.to_owned(),
|
||||
documentation: default(),
|
||||
documentation_html: default(),
|
||||
documentation_sections: default(),
|
||||
reexport: default(),
|
||||
};
|
||||
model::suggestion_database::Entry::from_ls_entry(ls_entry).unwrap()
|
||||
}
|
||||
@ -268,6 +269,7 @@ pub(crate) mod tests {
|
||||
documentation_html: None,
|
||||
self_type: None,
|
||||
scope: model::suggestion_database::entry::Scope::Everywhere,
|
||||
icon_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -473,13 +473,14 @@ mod test {
|
||||
|
||||
// Non-empty db
|
||||
let entry = SuggestionEntry::Atom {
|
||||
name: "TextAtom".to_string(),
|
||||
module: "TestProject.TestModule".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "TestAtom".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
external_id: None,
|
||||
name: "TextAtom".to_string(),
|
||||
module: "TestProject.TestModule".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "TestAtom".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
external_id: None,
|
||||
};
|
||||
let db_entry = SuggestionsDatabaseEntry { id: 12, suggestion: entry };
|
||||
let response = language_server::response::GetSuggestionDatabase {
|
||||
@ -498,31 +499,34 @@ mod test {
|
||||
fn applying_update() {
|
||||
let mut fixture = TestWithLocalPoolExecutor::set_up();
|
||||
let entry1 = SuggestionEntry::Atom {
|
||||
name: "Entry1".to_owned(),
|
||||
module: "TestProject.TestModule".to_owned(),
|
||||
arguments: vec![],
|
||||
return_type: "TestAtom".to_owned(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
external_id: None,
|
||||
name: "Entry1".to_owned(),
|
||||
module: "TestProject.TestModule".to_owned(),
|
||||
arguments: vec![],
|
||||
return_type: "TestAtom".to_owned(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
external_id: None,
|
||||
};
|
||||
let entry2 = SuggestionEntry::Atom {
|
||||
name: "Entry2".to_owned(),
|
||||
module: "TestProject.TestModule".to_owned(),
|
||||
arguments: vec![],
|
||||
return_type: "TestAtom".to_owned(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
external_id: None,
|
||||
name: "Entry2".to_owned(),
|
||||
module: "TestProject.TestModule".to_owned(),
|
||||
arguments: vec![],
|
||||
return_type: "TestAtom".to_owned(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
external_id: None,
|
||||
};
|
||||
let new_entry2 = SuggestionEntry::Atom {
|
||||
name: "NewEntry2".to_owned(),
|
||||
module: "TestProject.TestModule".to_owned(),
|
||||
arguments: vec![],
|
||||
return_type: "TestAtom".to_owned(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
external_id: None,
|
||||
name: "NewEntry2".to_owned(),
|
||||
module: "TestProject.TestModule".to_owned(),
|
||||
arguments: vec![],
|
||||
return_type: "TestAtom".to_owned(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
external_id: None,
|
||||
};
|
||||
let arg1 = SuggestionEntryArgument {
|
||||
name: "Argument1".to_owned(),
|
||||
@ -791,29 +795,32 @@ mod test {
|
||||
fn lookup_by_fully_qualified_name_in_db_created_from_ls_response() {
|
||||
// Initialize a suggestion database with sample entries.
|
||||
let entry1 = SuggestionEntry::Atom {
|
||||
name: "TextAtom".to_string(),
|
||||
module: "TestProject.TestModule".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "TestAtom".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
external_id: None,
|
||||
name: "TextAtom".to_string(),
|
||||
module: "TestProject.TestModule".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "TestAtom".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
external_id: None,
|
||||
};
|
||||
let entry2 = SuggestionEntry::Method {
|
||||
name: "create_process".to_string(),
|
||||
module: "Standard.Builtins.Main".to_string(),
|
||||
self_type: "Standard.Builtins.Main.System".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "Standard.Builtins.Main.System_Process_Result".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
external_id: None,
|
||||
name: "create_process".to_string(),
|
||||
module: "Standard.Builtins.Main".to_string(),
|
||||
self_type: "Standard.Builtins.Main.System".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "Standard.Builtins.Main.System_Process_Result".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
external_id: None,
|
||||
};
|
||||
let entry3 = SuggestionEntry::Module {
|
||||
module: "local.Unnamed_6.Main".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
reexport: None,
|
||||
module: "local.Unnamed_6.Main".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
reexport: None,
|
||||
};
|
||||
let entry4 = SuggestionEntry::Local {
|
||||
module: "local.Unnamed_6.Main".to_string(),
|
||||
@ -873,13 +880,14 @@ mod test {
|
||||
fn initialize_database_with_invalid_entries() {
|
||||
// Prepare some nonsense inputs from the Engine.
|
||||
let entry_with_empty_name = SuggestionEntry::Atom {
|
||||
name: "".to_string(),
|
||||
module: "Empty.Entry".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
external_id: None,
|
||||
name: "".to_string(),
|
||||
module: "Empty.Entry".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
external_id: None,
|
||||
};
|
||||
let empty_entry = SuggestionEntry::Local {
|
||||
module: "".to_string(),
|
||||
@ -889,10 +897,11 @@ mod test {
|
||||
scope: (default()..=default()).into(),
|
||||
};
|
||||
let gibberish_entry = SuggestionEntry::Module {
|
||||
module: GIBBERISH_MODULE_NAME.to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
reexport: None,
|
||||
module: GIBBERISH_MODULE_NAME.to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
reexport: None,
|
||||
};
|
||||
|
||||
let ls_response = language_server::response::GetSuggestionDatabase {
|
||||
@ -913,23 +922,25 @@ mod test {
|
||||
fn lookup_by_fully_qualified_name_after_db_update() {
|
||||
// Initialize a suggestion database with a few sample entries.
|
||||
let entry1 = SuggestionEntry::Atom {
|
||||
name: "TextAtom".to_string(),
|
||||
module: "TestProject.TestModule".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "TestAtom".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
external_id: None,
|
||||
name: "TextAtom".to_string(),
|
||||
module: "TestProject.TestModule".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "TestAtom".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
external_id: None,
|
||||
};
|
||||
let entry2 = SuggestionEntry::Method {
|
||||
name: "create_process".to_string(),
|
||||
module: "Standard.Builtins.Main".to_string(),
|
||||
self_type: "Standard.Builtins.Main.System".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "Standard.Builtins.Main.System_Process_Result".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
external_id: None,
|
||||
name: "create_process".to_string(),
|
||||
module: "Standard.Builtins.Main".to_string(),
|
||||
self_type: "Standard.Builtins.Main.System".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "Standard.Builtins.Main.System_Process_Result".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
external_id: None,
|
||||
};
|
||||
fn db_entry(id: SuggestionId, suggestion: SuggestionEntry) -> SuggestionsDatabaseEntry {
|
||||
SuggestionsDatabaseEntry { id, suggestion }
|
||||
@ -953,10 +964,11 @@ mod test {
|
||||
scope: None,
|
||||
});
|
||||
let entry3 = SuggestionEntry::Module {
|
||||
module: "local.Unnamed_6.Main".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
reexport: None,
|
||||
module: "local.Unnamed_6.Main".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
reexport: None,
|
||||
};
|
||||
let update = SuggestionDatabaseUpdatesEvent {
|
||||
updates: vec![
|
||||
@ -987,13 +999,14 @@ mod test {
|
||||
fn lookup_by_fully_qualified_name_after_db_update_reuses_id() {
|
||||
// Initialize a suggestion database with a sample entry.
|
||||
let entry1 = SuggestionEntry::Atom {
|
||||
name: "TextAtom".to_string(),
|
||||
module: "TestProject.TestModule".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "TestAtom".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
external_id: None,
|
||||
name: "TextAtom".to_string(),
|
||||
module: "TestProject.TestModule".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "TestAtom".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
external_id: None,
|
||||
};
|
||||
let id = 1;
|
||||
let response = language_server::response::GetSuggestionDatabase {
|
||||
@ -1011,10 +1024,11 @@ mod test {
|
||||
|
||||
// Apply a DB update adding a different entry at the same `id`.
|
||||
let entry2 = SuggestionEntry::Module {
|
||||
module: "local.Unnamed_6.Main".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
reexport: None,
|
||||
module: "local.Unnamed_6.Main".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
reexport: None,
|
||||
};
|
||||
let update = SuggestionDatabaseUpdatesEvent {
|
||||
updates: vec![entry::Update::Add { id, suggestion: entry2 }],
|
||||
|
@ -5,6 +5,8 @@ use crate::prelude::*;
|
||||
use crate::model::module::MethodId;
|
||||
|
||||
use ast::constants::keywords;
|
||||
use convert_case::Case;
|
||||
use convert_case::Casing;
|
||||
use double_representation::module;
|
||||
use double_representation::tp;
|
||||
use engine_protocol::language_server;
|
||||
@ -25,6 +27,16 @@ pub use language_server::types::SuggestionsDatabaseUpdate as Update;
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/// Key of the keyed [`language_server::types::DocSection`] containing a name of an icon in its
|
||||
/// body.
|
||||
const ICON_DOC_SECTION_KEY: &str = "Icon";
|
||||
|
||||
|
||||
|
||||
// ==============
|
||||
// === Errors ===
|
||||
// ==============
|
||||
@ -127,6 +139,38 @@ impl<'a> IntoIterator for &'a QualifiedName {
|
||||
|
||||
|
||||
|
||||
// ================
|
||||
// === IconName ===
|
||||
// ================
|
||||
|
||||
/// Name of an icon. The name is composed of words with unspecified casing.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IconName {
|
||||
/// Internally the name is kept in PascalCase to optimize converting into
|
||||
/// [`component_group_view::icon::Id`].
|
||||
pascal_cased: ImString,
|
||||
}
|
||||
|
||||
impl IconName {
|
||||
/// Construct from a name formatted in snake_case.
|
||||
pub fn from_snake_case(s: impl AsRef<str>) -> Self {
|
||||
let pascal_cased = s.as_ref().from_case(Case::Snake).to_case(Case::Pascal).into();
|
||||
Self { pascal_cased }
|
||||
}
|
||||
|
||||
/// Convert to a name formatted in snake_case.
|
||||
pub fn to_snake_case(&self) -> ImString {
|
||||
self.pascal_cased.from_case(Case::Pascal).to_case(Case::Snake).into()
|
||||
}
|
||||
|
||||
/// Convert to a name formatted in PascalCase.
|
||||
pub fn to_pascal_case(&self) -> ImString {
|
||||
self.pascal_cased.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Entry ===
|
||||
// =============
|
||||
@ -188,6 +232,8 @@ pub struct Entry {
|
||||
pub self_type: Option<tp::QualifiedName>,
|
||||
/// A scope where this suggestion is visible.
|
||||
pub scope: Scope,
|
||||
/// A name of a custom icon to use when displaying the entry.
|
||||
pub icon_name: Option<IconName>,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
@ -351,6 +397,7 @@ impl Entry {
|
||||
return_type,
|
||||
documentation,
|
||||
documentation_html,
|
||||
documentation_sections,
|
||||
..
|
||||
} => Self {
|
||||
name,
|
||||
@ -361,6 +408,7 @@ impl Entry {
|
||||
self_type: None,
|
||||
kind: Kind::Atom,
|
||||
scope: Scope::Everywhere,
|
||||
icon_name: find_icon_name_in_doc_sections(&documentation_sections),
|
||||
},
|
||||
#[allow(unused)]
|
||||
Method {
|
||||
@ -371,6 +419,7 @@ impl Entry {
|
||||
return_type,
|
||||
documentation,
|
||||
documentation_html,
|
||||
documentation_sections,
|
||||
..
|
||||
} => Self {
|
||||
name,
|
||||
@ -381,6 +430,7 @@ impl Entry {
|
||||
self_type: Some(self_type.try_into()?),
|
||||
kind: Kind::Method,
|
||||
scope: Scope::Everywhere,
|
||||
icon_name: find_icon_name_in_doc_sections(&documentation_sections),
|
||||
},
|
||||
Function { name, module, arguments, return_type, scope, .. } => Self {
|
||||
name,
|
||||
@ -391,6 +441,7 @@ impl Entry {
|
||||
documentation_html: default(),
|
||||
kind: Kind::Function,
|
||||
scope: Scope::InModule { range: scope.into() },
|
||||
icon_name: None,
|
||||
},
|
||||
Local { name, module, return_type, scope, .. } => Self {
|
||||
name,
|
||||
@ -401,8 +452,11 @@ impl Entry {
|
||||
documentation_html: default(),
|
||||
kind: Kind::Local,
|
||||
scope: Scope::InModule { range: scope.into() },
|
||||
icon_name: None,
|
||||
},
|
||||
Module { module, documentation, documentation_html, .. } => {
|
||||
Module {
|
||||
module, documentation, documentation_html, documentation_sections, ..
|
||||
} => {
|
||||
let module_name: module::QualifiedName = module.clone().try_into()?;
|
||||
Self {
|
||||
documentation_html: Self::make_html_docs(documentation, documentation_html),
|
||||
@ -413,6 +467,7 @@ impl Entry {
|
||||
kind: Kind::Module,
|
||||
scope: Scope::Everywhere,
|
||||
return_type: module,
|
||||
icon_name: find_icon_name_in_doc_sections(&documentation_sections),
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -629,6 +684,27 @@ fn chain_iter_and_entry_name<'a>(
|
||||
iter.into_iter().chain(iter::once(entry.name.as_str()))
|
||||
}
|
||||
|
||||
fn find_icon_name_in_doc_sections<'a, I>(doc_sections: I) -> Option<IconName>
|
||||
where I: IntoIterator<Item = &'a language_server::types::DocSection> {
|
||||
use language_server::types::DocSection;
|
||||
doc_sections.into_iter().find_map(|section| match section {
|
||||
DocSection::Keyed { key, body } if key == ICON_DOC_SECTION_KEY => {
|
||||
let icon_name = IconName::from_snake_case(&body);
|
||||
let as_snake_case = icon_name.to_snake_case();
|
||||
if as_snake_case.as_str() != body.as_str() || !body.is_case(Case::Snake) {
|
||||
let msg = format!(
|
||||
"The icon name {body} used in the {ICON_DOC_SECTION_KEY} section of the \
|
||||
documentation of a component is not a valid, losslessly-convertible snake_case \
|
||||
identifier. The component may be displayed with a different icon than expected."
|
||||
);
|
||||
event!(WARN, "{msg}");
|
||||
}
|
||||
Some(icon_name)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
@ -665,6 +741,7 @@ mod test {
|
||||
documentation_html: None,
|
||||
self_type: None,
|
||||
scope: Scope::Everywhere,
|
||||
icon_name: None,
|
||||
};
|
||||
let method = Entry {
|
||||
name: "method".to_string(),
|
||||
@ -767,6 +844,7 @@ mod test {
|
||||
documentation_html: None,
|
||||
self_type: None,
|
||||
scope: Scope::Everywhere,
|
||||
icon_name: None,
|
||||
};
|
||||
let method = Entry {
|
||||
name: "method".to_string(),
|
||||
@ -795,31 +873,34 @@ mod test {
|
||||
assert_eq!(entry_qualified_name, expected_qualified_name);
|
||||
}
|
||||
let atom = language_server::SuggestionEntry::Atom {
|
||||
name: "TextAtom".to_string(),
|
||||
module: "TestProject.TestModule".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "TestAtom".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
external_id: None,
|
||||
name: "TextAtom".to_string(),
|
||||
module: "TestProject.TestModule".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "TestAtom".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
external_id: None,
|
||||
};
|
||||
expect(atom, "TestProject.TestModule.TextAtom");
|
||||
let method = language_server::SuggestionEntry::Method {
|
||||
name: "create_process".to_string(),
|
||||
module: "Standard.Builtins.Main".to_string(),
|
||||
self_type: "Standard.Builtins.Main.System".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "Standard.Builtins.Main.System_Process_Result".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
external_id: None,
|
||||
name: "create_process".to_string(),
|
||||
module: "Standard.Builtins.Main".to_string(),
|
||||
self_type: "Standard.Builtins.Main.System".to_string(),
|
||||
arguments: vec![],
|
||||
return_type: "Standard.Builtins.Main.System_Process_Result".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
external_id: None,
|
||||
};
|
||||
expect(method, "Standard.Builtins.Main.System.create_process");
|
||||
let module = language_server::SuggestionEntry::Module {
|
||||
module: "local.Unnamed_6.Main".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
reexport: None,
|
||||
module: "local.Unnamed_6.Main".to_string(),
|
||||
documentation: None,
|
||||
documentation_html: None,
|
||||
documentation_sections: default(),
|
||||
reexport: None,
|
||||
};
|
||||
expect(module, "local.Unnamed_6.Main");
|
||||
let local = language_server::SuggestionEntry::Local {
|
||||
@ -848,4 +929,31 @@ mod test {
|
||||
assert_eq!(qualified_name.to_string(), "Foo.Bar".to_string());
|
||||
assert_eq!(String::from(qualified_name), "Foo.Bar".to_string());
|
||||
}
|
||||
|
||||
/// Test [`find_icon_name_in_doc_sections`] function extracting a name of an icon from the body
|
||||
/// of a keyed [`DocSection`] which has its key equal to the `Icon` string.
|
||||
#[test]
|
||||
fn find_icon_name_in_doc_section_with_icon_key() {
|
||||
use language_server::types::DocSection;
|
||||
let doc_sections = [
|
||||
DocSection::Paragraph { body: "Some paragraph.".into() },
|
||||
DocSection::Keyed { key: "NotIcon".into(), body: "example_not_icon_body".into() },
|
||||
DocSection::Keyed { key: "Icon".into(), body: "example_icon_name".into() },
|
||||
DocSection::Paragraph { body: "Another paragraph.".into() },
|
||||
];
|
||||
let icon_name = find_icon_name_in_doc_sections(&doc_sections).unwrap();
|
||||
assert_eq!(icon_name.to_pascal_case(), "ExampleIconName");
|
||||
}
|
||||
|
||||
/// Test case-insensitive comparison of [`IconName`] values and case-insensitiveness when
|
||||
/// converting [`IconName`] values to PascalCase.
|
||||
#[test]
|
||||
fn icon_name_case_insensitiveness() {
|
||||
let name_from_small_snake_case = IconName::from_snake_case("an_example_name");
|
||||
let name_from_mixed_snake_case = IconName::from_snake_case("aN_EXAMPLE_name");
|
||||
const PASCAL_CASE_NAME: &str = "AnExampleName";
|
||||
assert_eq!(name_from_small_snake_case, name_from_mixed_snake_case);
|
||||
assert_eq!(name_from_small_snake_case.to_pascal_case(), PASCAL_CASE_NAME);
|
||||
assert_eq!(name_from_mixed_snake_case.to_pascal_case(), PASCAL_CASE_NAME);
|
||||
}
|
||||
}
|
||||
|
@ -185,8 +185,10 @@ impl list_view::entry::ModelProvider<component_group_view::Entry> for Component
|
||||
let label = component.label();
|
||||
let highlighted = bytes_of_matched_letters(&*match_info, &label);
|
||||
let kind = component.suggestion.kind;
|
||||
let icon_name = component.suggestion.icon_name.as_ref();
|
||||
let icon = icon_name.and_then(|n| n.to_pascal_case().parse().ok());
|
||||
Some(component_group_view::entry::Model {
|
||||
icon: for_each_kind_variant!(kind_to_icon(kind)),
|
||||
icon: icon.unwrap_or_else(|| for_each_kind_variant!(kind_to_icon(kind))),
|
||||
highlighted_text: list_view::entry::GlyphHighlightedLabelModel { label, highlighted },
|
||||
})
|
||||
}
|
||||
|
@ -108,6 +108,7 @@ pub mod mock {
|
||||
kind: suggestion_database::entry::Kind::Method,
|
||||
scope: suggestion_database::entry::Scope::Everywhere,
|
||||
documentation_html: None,
|
||||
icon_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,6 +124,7 @@ pub mod mock {
|
||||
kind: suggestion_database::entry::Kind::Method,
|
||||
scope: suggestion_database::entry::Scope::Everywhere,
|
||||
documentation_html: None,
|
||||
icon_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Options intended to be common for all developers.
|
||||
|
||||
wasm-size-limit: 5.22 MiB
|
||||
wasm-size-limit: 5.23 MiB
|
||||
|
||||
required-versions:
|
||||
cargo-watch: ^8.1.1
|
||||
|
@ -140,6 +140,8 @@ type Table
|
||||
Select columns with the same names as the ones provided.
|
||||
|
||||
table.select_columns (By_Column [column1, column2])
|
||||
|
||||
Icon: select_column
|
||||
select_columns : Column_Selector -> Boolean -> Problem_Behavior -> Table
|
||||
select_columns self (columns = By_Index [0]) (reorder = False) (on_problems = Report_Warning) =
|
||||
new_columns = Table_Helpers.select_columns internal_columns=self.internal_columns selector=columns reorder=reorder on_problems=on_problems
|
||||
@ -538,6 +540,8 @@ type Table
|
||||
The resulting table contains rows of `self` extended with rows of
|
||||
`other` with matching indexes. If the index in `other` is not unique,
|
||||
the corresponding rows of `self` will be duplicated in the result.
|
||||
|
||||
Icon: join
|
||||
join : Table | Column -> Nothing | Text | Column | Vector (Text | Column) -> Boolean -> Text -> Text -> Table
|
||||
join self other on=Nothing drop_unmatched=False left_suffix='_left' right_suffix='_right' = case other of
|
||||
Column _ _ _ _ _ -> self.join other.to_table on drop_unmatched left_suffix right_suffix
|
||||
|
@ -297,6 +297,8 @@ type Table
|
||||
Select columns with the same names as the ones provided.
|
||||
|
||||
table.select_columns (By_Column [column1, column2])
|
||||
|
||||
Icon: select_column
|
||||
select_columns : Column_Selector -> Boolean -> Problem_Behavior -> Table
|
||||
select_columns self (columns = By_Index [0]) (reorder = False) (on_problems = Report_Warning) =
|
||||
new_columns = Table_Helpers.select_columns internal_columns=self.columns selector=columns reorder=reorder on_problems=on_problems
|
||||
@ -812,6 +814,8 @@ type Table
|
||||
|
||||
example_join =
|
||||
Examples.inventory_table.join Examples.popularity_table
|
||||
|
||||
Icon: join
|
||||
join : Table | Column.Column -> Text | Nothing -> Boolean -> Text -> Text -> Table
|
||||
join self other on=Nothing drop_unmatched=False left_suffix='_left' right_suffix='_right' =
|
||||
case other of
|
||||
|
@ -610,7 +610,7 @@ interface Tag {
|
||||
name: string;
|
||||
|
||||
/** The tag text. */
|
||||
text: HTMLString;
|
||||
body: HTMLString;
|
||||
}
|
||||
|
||||
/** The paragraph of the text.
|
||||
|
@ -19,6 +19,38 @@ where
|
||||
serde_json::from_value(json_value).or_else(|_error| Ok(Ret::default()))
|
||||
}
|
||||
|
||||
/// Deserialize a JSON value that is either of `Ret` type or equals `null`. A `null` is converted
|
||||
/// to a default value of `Ret` type.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```
|
||||
/// # use serde::Deserialize;
|
||||
/// # use enso_prelude::deserialize_null_as_default;
|
||||
/// #[derive(Debug, Deserialize, PartialEq)]
|
||||
/// struct Foo {
|
||||
/// #[serde(default, deserialize_with = "deserialize_null_as_default")]
|
||||
/// blah: Vec<i32>,
|
||||
/// }
|
||||
/// fn check_deserialized_eq(code: &str, expected_deserialized: &Foo) {
|
||||
/// let deserialized = serde_json::from_str::<Foo>(code).unwrap();
|
||||
/// assert_eq!(&deserialized, expected_deserialized);
|
||||
/// }
|
||||
/// let empty_foo = Foo { blah: vec![] };
|
||||
/// check_deserialized_eq(r#"{"blah" : null }"#, &empty_foo);
|
||||
/// check_deserialized_eq(r#"{}"#, &empty_foo);
|
||||
/// check_deserialized_eq(r#"{"blah" : [] }"#, &empty_foo);
|
||||
/// check_deserialized_eq(r#"{"blah" : [1,2,3] }"#, &Foo { blah: vec![1, 2, 3] });
|
||||
/// ```
|
||||
#[cfg(feature = "serde_json")]
|
||||
pub fn deserialize_null_as_default<'d, Ret, D>(d: D) -> Result<Ret, D::Error>
|
||||
where
|
||||
for<'e> Ret: Default + Deserialize<'e>,
|
||||
D: serde::Deserializer<'d>, {
|
||||
let option_value = Option::deserialize(d)?;
|
||||
Ok(option_value.unwrap_or_default())
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
Loading…
Reference in New Issue
Block a user