move method icon definition to documentation tag (#7123)

This commit is contained in:
Paweł Grabarz 2023-06-29 16:48:55 +02:00 committed by GitHub
parent 26bd95cf3d
commit cb9d4c4607
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 95 additions and 82 deletions

3
Cargo.lock generated
View File

@ -1603,8 +1603,11 @@ dependencies = [
name = "debug-scene-icons"
version = "0.1.0"
dependencies = [
"convert_case 0.6.0",
"ensogl",
"ensogl-hardcoded-theme",
"ensogl-text-msdf",
"ensogl-tooltip",
"ide-view-component-list-panel-grid",
"ide-view-component-list-panel-icons",
"ide-view-graph-editor",

View File

@ -80,12 +80,6 @@ pub mod constants {
/// The "void" atom returned by function meant to not return any argument.
pub const NOTHING: &str = "Nothing";
}
/// The tag name of documentation sections marked as "PRIVATE"
pub const PRIVATE_DOC_SECTION_TAG_NAME: &str = "PRIVATE";
/// The tag name of a documentation section with a method alias.
pub const ALIAS_DOC_SECTION_TAG_NAME: &str = "ALIAS";
}
pub use crumbs::Crumb;

View File

@ -12,6 +12,7 @@ use convert_case::Case;
use convert_case::Casing;
use double_representation::name::QualifiedName;
use enso_doc_parser::DocSection;
use enso_doc_parser::Tag;
use ordered_float::OrderedFloat;
@ -237,11 +238,10 @@ impl Component {
/// Check whether the component contains the "PRIVATE" tag.
pub fn is_private(&self) -> bool {
match &self.data {
Data::FromDatabase { entry, .. } => entry.documentation.iter().any(|doc| match doc {
DocSection::Tag { name, .. } =>
name == &ast::constants::PRIVATE_DOC_SECTION_TAG_NAME,
_ => false,
}),
Data::FromDatabase { entry, .. } => entry
.documentation
.iter()
.any(|doc| matches!(doc, DocSection::Tag { tag: Tag::Private, .. })),
_ => false,
}
}
@ -252,8 +252,7 @@ impl Component {
let aliases = match &self.data {
Data::FromDatabase { entry, .. } => {
let aliases = entry.documentation.iter().filter_map(|doc| match doc {
DocSection::Tag { name, body }
if name == &ast::constants::ALIAS_DOC_SECTION_TAG_NAME =>
DocSection::Tag { tag: Tag::Alias, body } =>
Some(body.as_str().split(',').map(|s| s.trim())),
_ => None,
});

View File

@ -627,8 +627,7 @@ mod tests {
/// excluded from the list.
#[test]
fn building_component_list_with_private_component() {
use ast::constants::PRIVATE_DOC_SECTION_TAG_NAME as PRIVATE_TAG;
let private_doc_section = enso_suggestion_database::doc_section!(@ PRIVATE_TAG, "");
let private_doc_section = enso_suggestion_database::doc_section!(@ Private, "");
let suggestion_db = enso_suggestion_database::mock_suggestion_database! {
test.Test {
mod LocalModule {

View File

@ -723,7 +723,7 @@ impl FilteredDocSections {
let mut examples = Vec::new();
for section in doc_sections {
match section {
DocSection::Tag { name, body } => tags.push(Tag::new(name, body)),
DocSection::Tag { tag, body } => tags.push(Tag::new(tag.to_str(), body)),
DocSection::Marked { mark: Mark::Example, .. } => examples.push(section.clone()),
section => synopsis.push(section.clone()),
}
@ -838,21 +838,21 @@ mod tests {
Bar (b);
#[with_doc_section(doc_section!("Documentation of method A.baz."))]
#[with_doc_section(doc_section!(@ "Tag", "Tag body."))]
#[with_doc_section(doc_section!(@ Deprecated, "Tag body."))]
fn baz() -> Standard.Base.B;
}
#[with_doc_section(doc_section!("Documentation of type B."))]
type B {
#[with_doc_section(doc_section!("Documentation of constructor B.New."))]
#[with_doc_section(doc_section!(@ "AnotherTag", "Tag body."))]
#[with_doc_section(doc_section!(@ Alias, "Tag body."))]
#[with_doc_section(doc_section!(! "Important", "Important note."))]
#[with_doc_section(doc_section!(> "Example", "Example of constructor B.New usage."))]
New;
}
#[with_doc_section(doc_section!("Documentation of module method."))]
#[with_doc_section(doc_section!(@ "Deprecated", ""))]
#[with_doc_section(doc_section!(@ Deprecated, ""))]
#[with_doc_section(doc_section!(> "Example", "Example of module method usage."))]
fn module_method() -> Standard.Base.A;
}
@ -875,7 +875,7 @@ mod tests {
name: QualifiedName::from_text("Standard.Base.module_method").unwrap().into(),
tags: Tags {
list: SortedVec::new([Tag {
name: "Deprecated".to_im_string(),
name: "DEPRECATED".to_im_string(),
body: "".to_im_string(),
}]),
},
@ -936,7 +936,7 @@ mod tests {
fn a_baz_method() -> Function {
Function {
name: QualifiedName::from_text("Standard.Base.A.baz").unwrap().into(),
tags: Tags { list: vec![Tag::new("Tag", "Tag body.")].into() },
tags: Tags { list: vec![Tag::new("DEPRECATED", "Tag body.")].into() },
arguments: default(),
synopsis: Synopsis::from_doc_sections([doc_section!(
"Documentation of method A.baz."
@ -960,7 +960,7 @@ mod tests {
fn b_new_constructor() -> Function {
Function {
name: QualifiedName::from_text("Standard.Base.B.New").unwrap().into(),
tags: Tags { list: vec![Tag::new("AnotherTag", "Tag body.")].into() },
tags: Tags { list: vec![Tag::new("ALIAS", "Tag body.")].into() },
arguments: default(),
synopsis: Synopsis::from_doc_sections([
doc_section!("Documentation of constructor B.New."),

View File

@ -16,6 +16,7 @@ use engine_protocol::language_server;
use engine_protocol::language_server::FieldUpdate;
use engine_protocol::language_server::SuggestionsDatabaseModification;
use enso_doc_parser::DocSection;
use enso_doc_parser::Tag;
use enso_text::Location;
use language_server::types::FieldAction;
@ -30,16 +31,6 @@ 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 ===
// ==============
@ -91,25 +82,23 @@ pub struct ModuleSpan {
// ================
/// Name of an icon. The name is composed of words with unspecified casing.
///
/// In order to make icon definitions more readable for non-programmer users, the builtin icon name
/// is allowed to be formatted in arbitrary casing. Either `SNAKE_case`,`camelCase`, `Pascal_Case`
/// etc. is allowed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IconName {
/// Internally the name is kept in PascalCase to optimize converting into
/// [`component_group_view::icon::Id`].
/// The name is kept in `PascalCase` to allow easy conversion into builtin 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();
/// Parse arbitrary tag body with any casing into an `PascalCase` icon name.
pub fn from_tag_body(s: &str) -> Self {
let pascal_cased = s.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()
@ -977,19 +966,7 @@ where
fn find_icon_name_in_doc_sections<'a, I>(doc_sections: I) -> Option<IconName>
where I: IntoIterator<Item = &'a 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."
);
warn!("{msg}");
}
Some(icon_name)
}
DocSection::Tag { tag: Tag::Icon, body } => Some(IconName::from_tag_body(body)),
_ => None,
})
}
@ -1274,8 +1251,8 @@ mod test {
use enso_doc_parser::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::Tag { tag: Tag::Advanced, body: "example_not_icon_body".into() },
DocSection::Tag { tag: Tag::Icon, body: "ExampleIconName".into() },
DocSection::Paragraph { body: "Another paragraph.".into() },
];
let icon_name = find_icon_name_in_doc_sections(&doc_sections).unwrap();
@ -1286,8 +1263,8 @@ mod test {
/// 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");
let name_from_small_snake_case = IconName::from_tag_body("an_example_name");
let name_from_mixed_snake_case = IconName::from_tag_body("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);

View File

@ -52,8 +52,7 @@ pub const DEFAULT_TYPE: &str = "Standard.Base.Any";
///
/// let mut builder = Builder::new();
/// builder.add_and_enter_module("local.Project", |e| e);
/// builder
/// .add_and_enter_type("Type", vec![], |e| e.with_icon(IconName::from_snake_case("an_icon")));
/// builder.add_and_enter_type("Type", vec![], |e| e.with_icon(IconName::from_tag_body("an_icon")));
/// builder.add_constructor("Constructor", vec![], |e| e);
/// builder.leave();
/// builder.add_method("module_method", vec![], "local.Project.Type", true, |e| e);
@ -320,7 +319,7 @@ macro_rules! mock_suggestion_database_entries {
/// static fn static_method(x) -> Standard.Base.Number;
/// }
///
/// #[with_icon(entry::IconName::from_snake_case("TestIcon"))]
/// #[with_icon(entry::IconName::from_tag_body("TestIcon"))]
/// static fn module_method() -> local.Project.Submodule.TestType;
/// }
/// }
@ -405,7 +404,7 @@ macro_rules! doc_section_mark {
/// ### [`DocSection::Tag`]
/// ```
/// # use enso_suggestion_database::doc_section;
/// doc_section!(@ "Tag name", "Tag body.");
/// doc_section!(@ Deprecated, "Tag body.");
/// ```
///
/// ### [`DocSection::Keyed`]
@ -426,8 +425,11 @@ macro_rules! doc_section_mark {
/// ```
#[macro_export]
macro_rules! doc_section {
(@ $tag:expr, $body:expr) => {
$crate::mock::enso_doc_parser::DocSection::Tag { name: $tag.into(), body: $body.into() }
(@ $tag:ident, $body:expr) => {
$crate::mock::enso_doc_parser::DocSection::Tag {
tag: $crate::mock::enso_doc_parser::Tag::$tag,
body: $body.into(),
}
};
($mark:tt $body:expr) => {
$crate::mock::enso_doc_parser::DocSection::Marked {
@ -478,7 +480,7 @@ pub fn standard_db_mock() -> SuggestionDatabase {
static fn static_method(x) -> Standard.Base.Number;
}
#[with_icon(entry::IconName::from_snake_case("TestIcon"))]
#[with_icon(entry::IconName::from_tag_body("TestIcon"))]
static fn module_method() -> local.Project.Submodule.TestType;
}
}

View File

@ -12,6 +12,7 @@ use ensogl::prelude::*;
use enso_doc_parser::DocSection;
use enso_doc_parser::Mark;
use enso_doc_parser::Tag;
use enso_suggestion_database as suggestion_database;
use enso_suggestion_database::doc_section;
use enso_suggestion_database::documentation_ir::EntryDocumentation;
@ -94,7 +95,7 @@ fn database() -> SuggestionDatabase {
#[with_doc_section(doc_section!(? "Info", "Info sections provide some insights."))]
Standard.Base {
#[with_doc_section(doc_section!("Maybe type."))]
#[with_doc_section(doc_section!(@ "Annotated", ""))]
#[with_doc_section(doc_section!(@ Advanced, ""))]
type Delimited_Format (a) {
#[with_doc_section(doc_section!("Some constructor."))]
#[with_doc_section(doc_section!(> "Example", "Some 1"))]
@ -135,7 +136,7 @@ fn database() -> SuggestionDatabase {
builder.add_function("bar", args, "Standard.Base.Boolean", scope.clone(), |e| {
e.with_doc_sections(vec![
DocSection::Paragraph { body: "Documentation for the bar function.".into() },
DocSection::Tag { name: "DEPRECATED", body: default() },
DocSection::Tag { tag: Tag::Deprecated, body: default() },
DocSection::Marked {
mark: Mark::Example,
header: None,
@ -147,7 +148,7 @@ fn database() -> SuggestionDatabase {
builder.add_local("local1", "Standard.Base.Boolean", scope, |e| {
e.with_doc_sections(vec![
DocSection::Paragraph { body: "Documentation for the local1 variable.".into() },
DocSection::Tag { name: "SOMETAG", body: default() },
DocSection::Tag { tag: Tag::Advanced, body: default() },
])
});

View File

@ -8,8 +8,11 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
convert_case = { workspace = true }
ensogl = { path = "../../../../../lib/rust/ensogl" }
ensogl-hardcoded-theme = { path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" }
ensogl-text-msdf = { path = "../../../../../lib/rust/ensogl/component/text/src/font/msdf" }
ensogl-tooltip = { path = "../../../../../lib/rust/ensogl/component/tooltip/" }
ide-view-component-list-panel-grid = { path = "../../component-browser/component-list-panel/grid" }
ide-view-component-list-panel-icons = { path = "../../component-browser/component-list-panel/icons" }
ide-view-graph-editor = { path = "../../graph-editor" }

View File

@ -8,13 +8,19 @@ use ensogl::system::web::traits::*;
use ide_view_component_list_panel_grid::prelude::*;
use wasm_bindgen::prelude::*;
use convert_case::Case;
use convert_case::Casing;
use ensogl::application::Application;
use ensogl::control::io::mouse;
use ensogl::data::color;
use ensogl::display::navigation::navigator::Navigator;
use ensogl::display::object::Object;
use ensogl::display::shape::Rectangle;
use ensogl::display::world::World;
use ensogl::display::DomSymbol;
use ensogl::system::web;
use ensogl_text_msdf::run_once_initialized;
use ensogl_tooltip::Tooltip;
use ide_view_component_list_panel_grid::entry::icon;
use ide_view_component_list_panel_icons::SHRINK_AMOUNT;
use ide_view_component_list_panel_icons::SIZE;
@ -52,9 +58,15 @@ mod frame {
#[wasm_bindgen]
#[allow(dead_code)]
pub fn entry_point_icons() {
run_once_initialized(init);
}
fn init() {
let app = Application::new("root");
let tooltip = Tooltip::new(&app);
let network = app.frp.network();
let world = app.display.clone();
mem::forget(app);
let scene = &world.default_scene;
mem::forget(Navigator::new(scene, &scene.camera()));
@ -68,10 +80,25 @@ pub fn entry_point_icons() {
let shape = ide_view_component_list_panel_icons::any::View::new();
shape.icon.set(id.any_cached_shape_location());
shape.r_component.set(dark_green.into());
place_icon(&world, shape, x, 0.0);
let hover_target = place_icon(&world, shape, x, 0.0);
x += 20.0;
let enter = hover_target.on_event::<mouse::Enter>();
let leave = hover_target.on_event::<mouse::Leave>();
let snake_case_name = id.as_str().to_case(Case::Snake);
let style = ensogl::application::tooltip::Style::set_label(snake_case_name);
frp::extend! { network
trace enter;
trace leave;
tooltip.frp.set_style <+ enter.constant(style);
tooltip.frp.set_style <+ leave.constant(default());
}
mem::forget(hover_target);
});
scene.add_child(&tooltip);
mem::forget(tooltip);
// === Action Bar Icons ===
@ -101,6 +128,8 @@ pub fn entry_point_icons() {
let enable_output_context_icon = action_bar::icon::enable_output_context::View::new();
enable_output_context_icon.color_rgba.set(dark_green.into());
place_icon(&world, enable_output_context_icon, 60.0, y);
mem::forget(app);
}
/// Create a grid with pixel squares to help development of icons.
@ -124,9 +153,15 @@ fn create_grid(world: &World, x: f32, y: f32) {
}
/// Place the given icon in the world at the right coordinates, in a dark green shade.
fn place_icon(world: &World, icon: impl Object, x: f32, y: f32) {
fn place_icon(world: &World, icon: impl Object, x: f32, y: f32) -> Rectangle {
let hover_target = Rectangle();
hover_target.set_color(ensogl::display::shape::INVISIBLE_HOVER_COLOR);
hover_target.set_xy((x - SIZE / 2.0, y - SIZE / 2.0));
hover_target.set_size((SIZE, SIZE));
icon.set_xy((x, y));
icon.set_size((SIZE, SIZE));
world.add_child(&hover_target);
world.add_child(&icon);
mem::forget(icon);
hover_target
}

View File

@ -137,7 +137,8 @@ type Table
column_count : Integer
column_count self = self.internal_columns.length
## Returns a new table with a chosen subset of columns, as specified by the
## ICON select_column
Returns a new table with a chosen subset of columns, as specified by the
`columns`, from the input table. Any unmatched input columns will be
dropped from the output.
@ -183,8 +184,6 @@ type Table
Select the first two columns and the last column, moving the last one to front.
table.select_columns [-1, 0, 1] reorder=True
Icon: select_column
@columns Widget_Helpers.make_column_name_vector_selector
select_columns : Vector (Integer | Text | Column_Selector) | Text | Integer -> Boolean -> Boolean -> Problem_Behavior -> Table ! No_Output_Columns | Missing_Input_Columns
select_columns self (columns = [self.columns.first.name]) (reorder = False) (error_on_missing_columns = True) (on_problems = Report_Warning) =

View File

@ -262,7 +262,8 @@ type Table
column_count : Integer
column_count self = self.java_table.getColumns.length
## Returns a new table with a chosen subset of columns, as specified by the
## ICON select_column
Returns a new table with a chosen subset of columns, as specified by the
`columns`, from the input table. Any unmatched input columns will be
dropped from the output.
@ -309,8 +310,6 @@ type Table
Select the first two columns and the last column, moving the last one to front.
table.select_columns [-1, 0, 1] reorder=True
Icon: select_column
@columns Widget_Helpers.make_column_name_vector_selector
select_columns : Vector (Integer | Text | Column_Selector) | Text | Integer -> Boolean -> Boolean -> Problem_Behavior -> Table ! No_Output_Columns | Missing_Input_Columns
select_columns self columns=[self.columns.first.name] (reorder = False) (error_on_missing_columns = True) (on_problems = Report_Warning) =

View File

@ -69,7 +69,7 @@ pub enum DocSection {
/// The documentation tag.
Tag {
/// The tag name.
name: &'static str,
tag: Tag,
/// The tag text.
body: HtmlString,
},
@ -141,9 +141,8 @@ impl DocSectionCollector {
impl<L> TokenConsumer<L> for DocSectionCollector {
fn tag(&mut self, tag: Tag, description: Option<Span<'_, L>>) {
let name = tag.to_str();
let body = description.map(|description| description.to_string()).unwrap_or_default();
self.sections.push(DocSection::Tag { name, body });
self.sections.push(DocSection::Tag { tag, body });
}
fn enter_marked_section(&mut self, mark: Mark, header: Option<Span<'_, L>>) {

View File

@ -55,13 +55,14 @@ pub struct TagWithDescription<'a, L> {
}
/// Indicator placed at the beginning of a documentation section, e.g. `PRIVATE`.
#[derive(Debug, Copy, Clone)]
#[derive(Hash, Debug, Clone, Copy, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum Tag {
Added,
Advanced,
Alias,
Deprecated,
Icon,
Modified,
Private,
Removed,
@ -90,6 +91,7 @@ impl Tag {
"ADVANCED" => Some(Advanced),
"ALIAS" => Some(Alias),
"DEPRECATED" => Some(Deprecated),
"ICON" => Some(Icon),
"MODIFIED" => Some(Modified),
"PRIVATE" => Some(Private),
"REMOVED" => Some(Removed),
@ -107,6 +109,7 @@ impl Tag {
Tag::Advanced => "ADVANCED",
Tag::Alias => "ALIAS",
Tag::Deprecated => "DEPRECATED",
Tag::Icon => "ICON",
Tag::Modified => "MODIFIED",
Tag::Private => "PRIVATE",
Tag::Removed => "REMOVED",