Documentation panel redesign (#7372)

* Support arguments list in the doc parser

* Support new doc sections in documentation panel

* Remove headers

* Remove outer dom and place breadcrumbs

* Fix methods icon

* Use unordered list class in css

* Improve tags styles

* Remove virtual component groups docs

* Cleanup top-level css styles

* Small adjustments to headers

* Add styles for emphasized text

* Add bold font for arguments

* Self-review

* Remove redundant placeholder struct.

* Update outdated doc.

* Avoid allocation when comparing strings.

* Avoid empty paragraph.

* Reduce allocations.

* Update test to remove empty paragraph.

* Fix rebase issues.

* Improve padding and size handling in UI themes

Added padding_x and padding_y to hardcoded theme's breadcrumb settings to ensure consistent padding. Also, these padding settings and breadcrumb_height are now used directly in the Style structure, eliminating hardcoded values in the view documentation.

* Adjusted breadcrumb background dimensions calculation.

* Add support for improper arguments formatting in documenation comments

* Do not include Icon tag into the docs

* Fix documentation panel resizing

* enso-formatter

---------

Co-authored-by: Michael Mauderer <michael.mauderer@enso.org>
This commit is contained in:
Ilya Bogdanov 2023-08-15 17:11:36 +04:00 committed by GitHub
parent 7f19b09d13
commit 02ba9a1a11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 325 additions and 248 deletions

1
Cargo.lock generated
View File

@ -2113,6 +2113,7 @@ dependencies = [
"enso-profiler",
"enso-reflect",
"lexpr",
"pretty_assertions",
]
[[package]]

View File

@ -29,10 +29,20 @@ use crate::SuggestionDatabase;
use double_representation::name::QualifiedName;
use enso_doc_parser::DocSection;
use enso_doc_parser::Mark;
use enso_doc_parser::Tag as DocSectionTag;
use std::cmp::Ordering;
// =================
// === Constants ===
// =================
/// A list of tags which are not included into generated documentation.
const IGNORED_TAGS: &[DocSectionTag] = &[DocSectionTag::Icon];
// ==============
// === Errors ===
// ==============
@ -54,14 +64,14 @@ pub struct NoParentModule(String);
#[derive(Debug, PartialEq, From, Clone, CloneRef)]
pub enum EntryDocumentation {
/// No documentation available.
Placeholder(Placeholder),
Placeholder,
/// Documentation for the entry.
Docs(Documentation),
}
impl Default for EntryDocumentation {
fn default() -> Self {
Placeholder::NoDocumentation.into()
EntryDocumentation::Placeholder
}
}
@ -99,7 +109,7 @@ impl EntryDocumentation {
},
Err(_) => {
error!("No entry found for id: {id:?}");
Self::Placeholder(Placeholder::NoDocumentation)
EntryDocumentation::Placeholder
}
};
Ok(result)
@ -194,7 +204,7 @@ impl EntryDocumentation {
Documentation::Local(_) => default(),
Documentation::Builtin(_) => default(),
},
EntryDocumentation::Placeholder(_) => default(),
EntryDocumentation::Placeholder => default(),
}
}
@ -232,7 +242,7 @@ impl EntryDocumentation {
Some(self_type) => self_type,
None => {
error!("Method without self type: {}.", entry.qualified_name());
return Ok(Placeholder::NoDocumentation.into());
return Ok(EntryDocumentation::Placeholder);
}
};
let self_type = db.lookup_by_qualified_name(self_type);
@ -251,12 +261,12 @@ impl EntryDocumentation {
}
_ => {
error!("Unexpected parent kind for method {}.", entry.qualified_name());
Ok(Placeholder::NoDocumentation.into())
Ok(EntryDocumentation::Placeholder)
}
},
Err(err) => {
error!("Parent entry for method {} not found: {}", entry.qualified_name(), err);
Ok(Self::Placeholder(Placeholder::NoDocumentation))
Ok(EntryDocumentation::Placeholder)
}
}
}
@ -277,23 +287,12 @@ impl EntryDocumentation {
}
Err(err) => {
error!("No return type found for constructor {}: {}", entry.qualified_name(), err);
Ok(Placeholder::NoDocumentation.into())
Ok(EntryDocumentation::Placeholder)
}
}
}
}
// === Placeholder ===
/// No documentation is available for the entry.
#[derive(Debug, Clone, CloneRef, PartialEq)]
#[allow(missing_docs)]
pub enum Placeholder {
/// Documentation is empty.
NoDocumentation,
/// Documentation for the Virtual Component group.
VirtualComponentGroup { name: ImString },
}
// === Documentation ===
@ -709,6 +708,8 @@ impl<'a, T: Clone + PartialOrd + Ord> From<&'a [T]> for SortedVec<T> {
/// Helper structure for splitting entry's [`DocSection`]s into [`Tags`], [`Synopsis`], and
/// [`Examples`].
///
/// Skips [`Tags`] from the [`IGNORED_TAGS`] list.
struct FilteredDocSections {
tags: Tags,
synopsis: Synopsis,
@ -723,7 +724,10 @@ impl FilteredDocSections {
let mut examples = Vec::new();
for section in doc_sections {
match section {
DocSection::Tag { tag, body } => tags.push(Tag::new(tag.to_str(), body)),
DocSection::Tag { tag, body } =>
if !IGNORED_TAGS.contains(tag) {
tags.push(Tag::new(tag.to_str(), body));
},
DocSection::Marked { mark: Mark::Example, .. } => examples.push(section.clone()),
section => synopsis.push(section.clone()),
}
@ -754,7 +758,7 @@ mod tests {
// Arbitrary non-existing entry id.
let entry_id = 10;
let docs = EntryDocumentation::new(&db, &entry_id).unwrap();
assert_eq!(docs, EntryDocumentation::Placeholder(Placeholder::NoDocumentation));
assert_eq!(docs, EntryDocumentation::Placeholder);
}
fn assert_docs(db: &SuggestionDatabase, name: Rc<QualifiedName>, expected: Documentation) {

View File

@ -425,6 +425,18 @@ macro_rules! doc_section_mark {
/// doc_section!(> "Marked as example");
/// doc_section!(> "Optional header", "Marked as example");
/// ```
///
/// ### [`DocSection::List`]
/// ```
/// # use enso_suggestion_database::doc_section;
/// doc_section!(- "Item 1"; - "Item 2"; -"Item 3");
/// ```
///
/// ### [`DocSection::Arguments`]
/// ```
/// # use enso_suggestion_database::doc_section;
/// doc_section!(- "arg_name", "Description"; - "arg_name2", "Description2");
/// ```
#[macro_export]
macro_rules! doc_section {
(@ $tag:ident, $body:expr) => {
@ -433,6 +445,16 @@ macro_rules! doc_section {
body: $body.into(),
}
};
($(- $body:expr);*) => {
$crate::mock::enso_doc_parser::DocSection::List { items: vec![
$($body.into()),*
]}
};
($(- $name:expr, $desc:expr);*) => {
$crate::mock::enso_doc_parser::DocSection::Arguments { args: vec![
$($crate::mock::enso_doc_parser::Argument { name: $name.into(), description: $desc.into() }),*
]}
};
($mark:tt $body:expr) => {
$crate::mock::enso_doc_parser::DocSection::Marked {
mark: $crate::doc_section_mark!($mark),

View File

@ -1,5 +1,4 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" fill="white"/>
<mask id="path-1-inside-1_7717_245722" fill="white">
<rect x="2" y="2" width="13" height="13" rx="2"/>
</mask>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,7 +0,0 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M8 16C3.58172 16 1.93129e-07 12.4183 0 8C-1.93129e-07 3.58172 3.58172 1.93129e-07 8 0H24C28.4183 -1.93129e-07 32 3.58172 32 8V24C32 28.4183 28.4183 32 24 32C19.5817 32 16 28.4183 16 24C16 19.5817 12.4183 16 8 16Z" fill="#9640DA" fill-opacity="0.3"/>
<ellipse cx="24" cy="24" rx="4" ry="4" transform="rotate(-90 24 24)" fill="#9640DA"/>
<ellipse cx="24" cy="8" rx="4" ry="4" transform="rotate(-90 24 8)" fill="#9640DA"/>
<ellipse cx="8" cy="8" rx="4" ry="4" transform="rotate(-90 8 8)" fill="#9640DA"/>
<circle cx="8" cy="24" r="5" transform="rotate(-90 8 24)" fill="#9640DA" fill-opacity="0.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 673 B

View File

@ -14,7 +14,8 @@
--enso-docs-example-background-color: #e6f1f8;
--enso-docs-background-color: #eaeaea;
--enso-docs-text-color: #434343;
--enso-docs-tag-background-color: #f5f5f5;
--enso-docs-tag-background-color: #dcd8d8;
--enso-docs-code-background-color: #dddcde;
--enso-docs-caption-background-color: #0077f6;
}
@ -23,32 +24,30 @@
font-size: 11.5px;
color: var(--enso-docs-text-color);
background-color: var(--enso-docs-background-color);
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 8px;
padding-right: 8px;
padding-bottom: 4px;
}
/* It's better to use `.unordered-list` class here instead of `ul`, but it will break the keyed lists that a provided by the engine.
* We need a separate way to represent keyed lists without embedded HTML tags. */
.enso-docs ul {
.enso-docs .unordered-list {
margin: 0;
padding: 0;
list-style-type: none;
list-style-position: inside;
}
.enso-docs ul li:before {
.enso-docs .unordered-list li:before {
content: "•";
font-size: 13px;
font-weight: 700;
margin-right: 3px;
}
.enso-docs ul li.type-item:before {
.enso-docs .unordered-list li.type-item:before {
color: var(--enso-docs-type-name-color);
}
.enso-docs ul li.method-item:before {
.enso-docs .unordered-list li.method-item:before {
color: var(--enso-docs-method-name-color);
}
@ -87,19 +86,17 @@
opacity: 0.34;
}
.enso-docs .argument {
font-weight: 600;
}
.enso-docs .section-content {
padding-left: 1.25rem;
padding-left: 8px;
padding-right: 8px;
}
/* Headers. */
.enso-docs .header-top {
font-size: 17px;
font-weight: 700;
color: var(--enso-docs-type-name-color);
margin: 0.5rem 0;
}
div.enso-docs .header-icon {
align-self: start;
display: flex;
@ -116,6 +113,8 @@ div.enso-docs .header-icon {
}
.enso-docs .header-container {
padding-left: 8px;
margin-top: 16px;
display: flex;
align-items: center;
}
@ -201,6 +200,14 @@ div.enso-docs .marked-icon-info {
margin: 0.05rem 0.1rem;
}
/* Code. The words emphasized with backticks. */
.enso-docs code {
background-color: var(--enso-docs-code-background-color);
border-radius: 4px;
padding: 2px;
}
/* Tags */
.enso-docs .tags-container {
@ -210,9 +217,13 @@ div.enso-docs .marked-icon-info {
}
.enso-docs .tag {
display: flex;
align-items: center;
justify-content: center;
height: 24px;
background-color: var(--enso-docs-tag-background-color);
border-radius: 0.5rem;
padding: 0.15rem 0.5rem;
margin-bottom: 0.25rem;
margin-right: 0.5rem;
border-radius: 4px;
padding: 1px 5px;
margin-bottom: 1px;
margin-right: 2px;
}

View File

@ -15,7 +15,6 @@ use enso_suggestion_database::documentation_ir::Examples;
use enso_suggestion_database::documentation_ir::Function;
use enso_suggestion_database::documentation_ir::LocalDocumentation;
use enso_suggestion_database::documentation_ir::ModuleDocumentation;
use enso_suggestion_database::documentation_ir::Placeholder;
use enso_suggestion_database::documentation_ir::Synopsis;
use enso_suggestion_database::documentation_ir::Tag;
use enso_suggestion_database::documentation_ir::TypeDocumentation;
@ -33,7 +32,6 @@ use horrorshow::owned_html;
/// We use SVG icons imported as text.
type Icon = &'static str;
const ICON_TYPE: &str = include_str!("../assets/icon-type.svg");
const ICON_METHODS: &str = include_str!("../assets/icon-methods.svg");
const ICON_EXAMPLES: &str = include_str!("../assets/icon-examples.svg");
const ICON_INFO: &str = include_str!("../assets/icon-info.svg");
@ -58,11 +56,7 @@ fn svg_icon(content: &'static str, class: &'static str) -> impl Render {
#[profile(Detail)]
pub fn render(docs: &EntryDocumentation) -> String {
let html = match docs {
EntryDocumentation::Placeholder(placeholder) => match placeholder {
Placeholder::NoDocumentation => String::from("No documentation available."),
Placeholder::VirtualComponentGroup { name } =>
render_virtual_component_group_docs(name.clone_ref()),
},
EntryDocumentation::Placeholder => String::from("No documentation available."),
EntryDocumentation::Docs(docs) => render_documentation(docs.clone_ref()),
};
match validate_utf8(&html) {
@ -81,58 +75,18 @@ fn validate_utf8(s: &str) -> Result<&str, std::str::Utf8Error> {
}
fn render_documentation(docs: Documentation) -> String {
let back_link = match &docs {
Documentation::Constructor { type_docs, .. } => Some(BackLink {
displayed: type_docs.name.name().to_owned(),
id: anchor_name(&type_docs.name),
}),
Documentation::Method { type_docs, .. } => Some(BackLink {
displayed: type_docs.name.name().to_owned(),
id: anchor_name(&type_docs.name),
}),
Documentation::ModuleMethod { module_docs, .. } => Some(BackLink {
displayed: module_docs.name.name().to_owned(),
id: anchor_name(&module_docs.name),
}),
Documentation::Type { module_docs, .. } => Some(BackLink {
displayed: module_docs.name.name().to_owned(),
id: anchor_name(&module_docs.name),
}),
_ => None,
};
match docs {
Documentation::Module(module_docs) => render_module_documentation(&module_docs),
Documentation::Type { docs, .. } => render_type_documentation(&docs, back_link),
Documentation::Function(docs) => render_function_documentation(&docs, back_link),
Documentation::Type { docs, .. } => render_type_documentation(&docs),
Documentation::Function(docs) => render_function_documentation(&docs),
Documentation::Local(docs) => render_local_documentation(&docs),
Documentation::Constructor { docs, .. } => render_function_documentation(&docs, back_link),
Documentation::Method { docs, .. } => render_function_documentation(&docs, back_link),
Documentation::ModuleMethod { docs, .. } => render_function_documentation(&docs, back_link),
Documentation::Constructor { docs, .. } => render_function_documentation(&docs),
Documentation::Method { docs, .. } => render_function_documentation(&docs),
Documentation::ModuleMethod { docs, .. } => render_function_documentation(&docs),
Documentation::Builtin(builtin_docs) => render_builtin_documentation(&builtin_docs),
}
}
/// Render the documentation of the virtual component group. Includes the name of the group.
fn render_virtual_component_group_docs(name: ImString) -> String {
let content = owned_html! {
h1(class="header-top") {
: &*name
}
};
docs_content(content).into_string().unwrap()
}
/// An optional link to the parent entry (module or type), that is displayed in the documentation
/// header. Pressing this link will switch the documentation to the parent entry, allowing
/// bidirectional navigation.
#[derive(Debug, Clone)]
struct BackLink {
/// Displayed text.
displayed: String,
/// The unique ID of the link.
id: String,
}
// === Types ===
@ -143,12 +97,10 @@ struct BackLink {
/// - Synopsis and a list of constructors.
/// - Methods.
/// - Examples.
fn render_type_documentation(docs: &TypeDocumentation, back_link: Option<BackLink>) -> String {
fn render_type_documentation(docs: &TypeDocumentation) -> String {
let constructors_exist = !docs.constructors.is_empty();
let methods_exist = !docs.methods.is_empty();
let examples_exist = !docs.examples.is_empty();
let name = &docs.name;
let arguments = &docs.arguments;
let synopsis = &docs.synopsis;
let constructors = &docs.constructors;
let synopsis = section_content(type_synopsis(synopsis));
@ -156,11 +108,8 @@ fn render_type_documentation(docs: &TypeDocumentation, back_link: Option<BackLin
let methods = section_content(list_of_functions(&docs.methods));
let examples = section_content(list_of_examples(&docs.examples));
let tags = section_content(list_of_tags(&docs.tags));
let header_text = type_header(name.name(), arguments_list(arguments), back_link.as_ref());
let header = header(ICON_TYPE, header_text, "header-top");
let content = owned_html! {
: &header;
: &tags;
: &synopsis;
@ if constructors_exist {
@ -195,24 +144,6 @@ fn types_header() -> impl Render {
header(ICON_METHODS, "Types", "section-header types-header")
}
/// A header for the type documentation.
fn type_header<'a>(
name: &'a str,
arguments: impl Render + 'a,
back_link: Option<&'a BackLink>,
) -> Box<dyn Render + 'a> {
box_html! {
@ if let Some(BackLink { id, displayed }) = &back_link {
a(id=id, class="link") {
: displayed;
}
: ".";
}
: &name;
span(class="arguments") { : &arguments }
}
}
/// A synopsis of the type. Contains a list of constructors, if it is not empty.
fn type_synopsis<'a>(synopsis: &'a Synopsis) -> Box<dyn Render + 'a> {
box_html! {
@ -269,14 +200,12 @@ fn render_module_documentation(docs: &ModuleDocumentation) -> String {
let types_exist = !docs.types.is_empty();
let methods_exist = !docs.methods.is_empty();
let examples_exist = !docs.examples.is_empty();
let name = &docs.name;
let synopsis = section_content(module_synopsis(&docs.synopsis));
let types = section_content(list_of_types(&docs.types));
let methods = section_content(list_of_functions(&docs.methods));
let examples = section_content(list_of_examples(&docs.examples));
let tags = section_content(list_of_tags(&docs.tags));
let content = owned_html! {
: header(ICON_TYPE, name.name(), "header-top");
: &tags;
: &synopsis;
@ if types_exist {
@ -352,17 +281,14 @@ fn module_synopsis<'a>(synopsis: &'a Synopsis) -> Box<dyn Render + 'a> {
// === Functions ===
/// Render documentation of a function.
fn render_function_documentation(docs: &Function, back_link: Option<BackLink>) -> String {
let Function { name, arguments, synopsis, tags, .. } = docs;
fn render_function_documentation(docs: &Function) -> String {
let Function { synopsis, tags, .. } = docs;
let examples_exist = !docs.examples.is_empty();
let synopsis = section_content(function_synopsis(synopsis));
let tags = section_content(list_of_tags(tags));
let examples = section_content(list_of_examples(&docs.examples));
let header_text = function_header(name.name(), arguments_list(arguments), back_link.as_ref());
let header = header(ICON_TYPE, header_text, "header-top");
let content = owned_html! {
: &header;
: &tags;
: &synopsis;
@ if examples_exist {
@ -373,24 +299,6 @@ fn render_function_documentation(docs: &Function, back_link: Option<BackLink>) -
docs_content(content).into_string().unwrap()
}
/// A header for the function documentation.
fn function_header<'a>(
name: &'a str,
arguments: impl Render + 'a,
back_link: Option<&'a BackLink>,
) -> Box<dyn Render + 'a> {
box_html! {
@ if let Some(BackLink { id, displayed }) = &back_link {
a(id=id, class="link") {
: displayed;
}
: ".";
}
: name;
span(class="arguments") { : &arguments }
}
}
/// A synopsis of the function.
fn function_synopsis<'a>(synopsis: &'a Synopsis) -> Box<dyn Render + 'a> {
box_html! {
@ -405,17 +313,14 @@ fn function_synopsis<'a>(synopsis: &'a Synopsis) -> Box<dyn Render + 'a> {
/// Render documentation of a function.
fn render_local_documentation(docs: &LocalDocumentation) -> String {
let LocalDocumentation { name, synopsis, return_type, tags, .. } = docs;
let LocalDocumentation { synopsis, tags, .. } = docs;
let examples_exist = !docs.examples.is_empty();
let synopsis = section_content(local_synopsis(synopsis));
let tags = section_content(list_of_tags(tags));
let examples = section_content(list_of_examples(&docs.examples));
let header_text = local_header(name.name(), return_type.name());
let header = header(ICON_TYPE, header_text, "header-top");
let content = owned_html! {
: &header;
: &tags;
: &synopsis;
@ if examples_exist {
@ -426,14 +331,6 @@ fn render_local_documentation(docs: &LocalDocumentation) -> String {
docs_content(content).into_string().unwrap()
}
/// A header for the local documentation.
fn local_header<'a>(name: &'a str, return_type: &'a str) -> Box<dyn Render + 'a> {
box_html! {
: name; : " ";
span(class="arguments") { : return_type }
}
}
/// A synopsis of the local.
fn local_synopsis<'a>(synopsis: &'a Synopsis) -> Box<dyn Render + 'a> {
box_html! {
@ -550,6 +447,27 @@ fn paragraph<'a>(doc_section: &'a DocSection) -> Box<dyn Render + 'a> {
}
}
}
DocSection::List { items } => {
box_html! {
ul(class="unordered-list") {
@for item in items {
li { : Raw(&item); }
}
}
}
}
DocSection::Arguments { args } => {
box_html! {
ul(class="unordered-list") {
@for arg in args {
li {
span(class="argument") { : &arg.name; }
: ": "; : Raw(&arg.description);
}
}
}
}
}
_ => box_html! {
p(class="paragraph") { : "Unexpected doc section type." }
},

View File

@ -1,5 +1,4 @@
//! Documentation view visualization generating and presenting Enso Documentation under
//! the documented node.
//! Documentation view presenting the documentation in the Component Browser.
#![recursion_limit = "1024"]
// === Features ===
@ -69,6 +68,12 @@ pub struct Style {
height: f32,
background: color::Rgba,
corner_radius: f32,
#[theme_path = "theme::breadcrumbs::height"]
breadcrumbs_height: f32,
#[theme_path = "theme::breadcrumbs::padding_x"]
breadcrumbs_padding_x: f32,
#[theme_path = "theme::breadcrumbs::padding_y"]
breadcrumbs_padding_y: f32,
}
@ -81,12 +86,13 @@ pub struct Style {
#[derive(Clone, CloneRef, Debug, display::Object)]
#[allow(missing_docs)]
pub struct Model {
outer_dom: DomSymbol,
inner_dom: DomSymbol,
style_container: DomSymbol,
dom: DomSymbol,
pub breadcrumbs: breadcrumbs::Breadcrumbs,
/// The purpose of this overlay is stop propagating mouse events under the documentation panel
/// to EnsoGL shapes, and pass them to the DOM instead.
overlay: Rectangle,
background: Rectangle,
display_object: display::object::Instance,
event_handlers: Rc<RefCell<Vec<web::EventListenerHandle>>>,
}
@ -96,10 +102,11 @@ impl Model {
fn new(app: &Application) -> Self {
let scene = &app.display.default_scene;
let display_object = display::object::Instance::new();
let outer_div = web::document.create_div_or_panic();
let outer_dom = DomSymbol::new(&outer_div);
let inner_div = web::document.create_div_or_panic();
let inner_dom = DomSymbol::new(&inner_div);
let style_div = web::document.create_div_or_panic();
let style_container = DomSymbol::new(&style_div);
let div = web::document.create_div_or_panic();
let dom = DomSymbol::new(&div);
let background = Rectangle::new();
let overlay = Rectangle::new().build(|r| {
r.set_color(INVISIBLE_HOVER_COLOR);
});
@ -108,28 +115,26 @@ impl Model {
breadcrumbs.set_base_layer(&app.display.default_scene.layers.node_searcher);
display_object.add_child(&breadcrumbs);
outer_dom.dom().set_style_or_warn("white-space", "normal");
outer_dom.dom().set_style_or_warn("overflow-y", "auto");
outer_dom.dom().set_style_or_warn("overflow-x", "auto");
outer_dom.dom().set_style_or_warn("pointer-events", "auto");
dom.dom().set_attribute_or_warn("class", "scrollable");
dom.dom().set_style_or_warn("white-space", "normal");
dom.dom().set_style_or_warn("overflow-y", "auto");
dom.dom().set_style_or_warn("overflow-x", "auto");
dom.dom().set_style_or_warn("pointer-events", "auto");
inner_dom.dom().set_attribute_or_warn("class", "scrollable");
inner_dom.dom().set_style_or_warn("white-space", "normal");
inner_dom.dom().set_style_or_warn("overflow-y", "auto");
inner_dom.dom().set_style_or_warn("overflow-x", "auto");
inner_dom.dom().set_style_or_warn("pointer-events", "auto");
display_object.add_child(&outer_dom);
outer_dom.add_child(&inner_dom);
display_object.add_child(&background);
display_object.add_child(&style_container);
display_object.add_child(&dom);
display_object.add_child(&overlay);
scene.dom.layers.node_searcher.manage(&outer_dom);
scene.dom.layers.node_searcher.manage(&inner_dom);
scene.dom.layers.node_searcher.manage(&style_container);
scene.dom.layers.node_searcher.manage(&dom);
Model {
outer_dom,
inner_dom,
style_container,
dom,
breadcrumbs,
overlay,
background,
display_object,
event_handlers: default(),
}
@ -141,12 +146,12 @@ impl Model {
self
}
/// Add `<style>` tag with the stylesheet to the `outer_dom`.
/// Add `<style>` tag with the stylesheet to the `style_container`.
fn load_css_stylesheet(&self) {
let stylesheet = include_str!("../assets/styles.css");
let element = web::document.create_element_or_panic("style");
element.set_inner_html(stylesheet);
self.outer_dom.append_or_warn(&element);
self.style_container.append_or_warn(&element);
}
fn set_initial_breadcrumbs(&self) {
@ -156,35 +161,35 @@ impl Model {
}
/// Set size of the documentation view.
fn size_changed(&self, size: Vector2, fraction: f32) {
let visible_part = Vector2(size.x * fraction, size.y);
self.outer_dom.set_xy(size / 2.0);
fn size_changed(&self, size: Vector2, width_fraction: f32, style: &Style) {
let visible_part = Vector2(size.x * width_fraction, size.y);
let dom_size =
Vector2(size.x, size.y - style.breadcrumbs_height - style.breadcrumbs_padding_y);
self.dom.set_dom_size(dom_size);
self.dom.set_xy(dom_size / 2.0);
self.overlay.set_size(visible_part);
self.outer_dom.set_dom_size(Vector2(size.x, size.y));
self.inner_dom.set_dom_size(Vector2(size.x, size.y));
self.breadcrumbs.set_xy(Vector2(0.0, size.y + 36.0));
self.breadcrumbs.frp().set_size(Vector2(visible_part.x, 32.0));
self.breadcrumbs.set_xy(Vector2(style.breadcrumbs_padding_x, size.y));
self.breadcrumbs.frp().set_size(Vector2(visible_part.x, style.breadcrumbs_height));
self.background.set_size(visible_part);
}
/// Set the fraction of visible documentation panel. Used to animate showing/hiding the panel.
fn width_animation_changed(&self, style: &Style, size: Vector2, fraction: f32) {
let percentage = (1.0 - fraction) * 100.0;
let clip_path = format!("inset(0 {percentage}% 0 0 round {}px)", style.corner_radius);
self.inner_dom.set_style_or_warn("clip-path", &clip_path);
self.outer_dom.set_style_or_warn("clip-path", &clip_path);
let actual_size = Vector2(size.x * fraction, size.y);
self.overlay.set_size(actual_size);
self.breadcrumbs.frp().set_size(Vector2(actual_size.x, 32.0));
let clip_path =
format!("inset(0 {percentage}% 0 0 round 0px 0px {0}px {0}px)", style.corner_radius);
self.dom.set_style_or_warn("clip-path", clip_path);
self.size_changed(size, fraction, style);
}
/// Display the documentation and scroll to the qualified name if needed.
/// Display the documentation and scroll to default position.
fn display_doc(&self, docs: EntryDocumentation, display_doc: &frp::Source<EntryDocumentation>) {
let linked_pages = docs.linked_doc_pages();
let html = html::render(&docs);
self.inner_dom.dom().set_inner_html(&html);
self.dom.dom().set_inner_html(&html);
self.set_link_handlers(linked_pages, display_doc);
// Scroll to the top of the page.
self.inner_dom.dom().set_scroll_top(0);
self.dom.dom().set_scroll_top(0);
}
/// Setup event handlers for links on the documentation page.
@ -212,16 +217,16 @@ impl Model {
/// TODO(#5214): This should be replaced with a EnsoGL spinner.
fn load_waiting_screen(&self) {
let spinner = include_str!("../assets/spinner.html");
self.inner_dom.dom().set_inner_html(spinner)
self.dom.dom().set_inner_html(spinner)
}
fn update_style(&self, style: Style) {
// Size is updated separately in [`size_changed`] method.
self.overlay.set_corner_radius(style.corner_radius);
self.outer_dom.set_style_or_warn("border-radius", format!("{}px", style.corner_radius));
self.inner_dom.set_style_or_warn("border-radius", format!("{}px", style.corner_radius));
let bg_color = style.background.to_javascript_string();
self.outer_dom.set_style_or_warn("background-color", bg_color);
self.dom.set_style_or_warn("border-radius", format!("{}px", style.corner_radius));
self.background.set_color(style.background);
self.background.set_corner_radius(style.corner_radius);
}
}
@ -272,15 +277,6 @@ pub struct View {
}
impl View {
/// Definition of this visualization.
pub fn definition() -> visualization::Definition {
let path = visualization::Path::builtin("Documentation View");
visualization::Definition::new(
visualization::Signature::new_for_any_type(path, visualization::Format::Json),
|app| Ok(Self::new(app).into()),
)
}
/// Constructor.
pub fn new(app: &Application) -> Self {
let frp = Frp::new();
@ -319,7 +315,6 @@ impl View {
// === Size ===
size <- style.map(|s| Vector2(s.width, s.height));
_eval <- size.map2(&width_anim.value, f!((&s, &f) model.size_changed(s, f)));
// === Style ===
@ -331,7 +326,8 @@ impl View {
width_anim.target <+ frp.set_visible.map(|&visible| if visible { 1.0 } else { 0.0 });
width_anim.skip <+ frp.skip_animation;
_eval <- width_anim.value.map3(&size, &style, f!((&f, &sz, st) model.width_animation_changed(st, sz, f)));
size_change <- all3(&width_anim.value, &size, &style);
eval size_change(((f, sz, st)) model.width_animation_changed(st, *sz, *f));
// === Activation ===
@ -371,6 +367,11 @@ impl View {
impl From<View> for visualization::Instance {
fn from(t: View) -> Self {
Self::new(&t, &t.visualization_frp, &t.frp.network, Some(t.model.outer_dom.clone_ref()))
Self::new(
&t,
&t.visualization_frp,
&t.frp.network,
Some(t.model.style_container.clone_ref()),
)
}
}

View File

@ -87,9 +87,8 @@ fn database() -> SuggestionDatabase {
#[with_doc_section(doc_section!("It also contains the autobiography of the author of \
this code."))]
#[with_doc_section(doc_section!("And a long list of his cats."))]
#[with_doc_section(doc_section!(
"Here it is" => "<ul><li>Tom</li><li>Garfield</li><li>Mr. Bigglesworth</li></ul>"
))]
#[with_doc_section(doc_section!("Here it is" => ""))]
#[with_doc_section(doc_section!(- "Tom"; - "Garfield"; - "Mr. Bigglesworth"))]
#[with_doc_section(doc_section!(! "Important", "Important sections are used to warn the \
reader about the dangers of using the module."))]
#[with_doc_section(doc_section!(? "Info", "Info sections provide some insights."))]
@ -99,21 +98,22 @@ fn database() -> SuggestionDatabase {
type Delimited_Format (a) {
#[with_doc_section(doc_section!("Some constructor."))]
#[with_doc_section(doc_section!(> "Example", "Some 1"))]
#[with_doc_section(doc_section!("Documentation for the Some(a) constructor."))]
#[with_doc_section(doc_section!("Documentation for the <code>Some(a)</code> constructor."))]
Some (a);
#[with_doc_section(doc_section!("Documentation for the None constructor."))]
#[with_doc_section(doc_section!("Documentation for the <code>None</code> constructor."))]
None;
#[with_doc_section(doc_section!("Documentation for the is_some() method."))]
#[with_doc_section(doc_section!("Arguments" => "<ul><li>self</li></ul>"))]
#[with_doc_section(doc_section!("Documentation for the <code>is_some()</code> method."))]
#[with_doc_section(doc_section!("Arguments" => ""))]
#[with_doc_section(doc_section!(- "self", "Self argument"))]
#[with_doc_section(doc_section!(! "Important", "This method is important."))]
fn is_some(self) -> Standard.Base.Boolean;
#[with_doc_section(doc_section!("Documentation for the Maybe.map() method."))]
#[with_doc_section(doc_section!("Documentation for the <code>Maybe.map()</code> method."))]
fn comment_all_characters (self) -> Standard.Base.Maybe;
}
#[with_doc_section(doc_section!("Documentation for the foo method."))]
#[with_doc_section(doc_section!("Documentation for the <code>foo</code> method."))]
fn foo(a: Standard.Base.Maybe) -> Standard.Base.Boolean;
#[with_doc_section(doc_section!(> "Example", "Get the names of all of the items from \
@ -135,7 +135,9 @@ fn database() -> SuggestionDatabase {
let args = vec![Argument::new("a", "Standard.Base.Boolean")];
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::Paragraph {
body: "Documentation for the <code>bar</code> function.".into(),
},
DocSection::Tag { tag: Tag::Deprecated, body: default() },
DocSection::Marked {
mark: Mark::Example,
@ -147,7 +149,9 @@ 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::Paragraph {
body: "Documentation for the <code>local1</code> variable.".into(),
},
DocSection::Tag { tag: Tag::Advanced, body: default() },
])
});
@ -273,6 +277,7 @@ pub fn main() {
panel_visible <- any(...);
panel_visible <+ init.constant(true);
current_state <- panel_visible.sample(&show_hide.events_deprecated.mouse_down);
panel_visible <+ current_state.not();
panel.frp.set_visible <+ current_state.not();
// === Disable navigator on hover ===

View File

@ -239,7 +239,12 @@ define_themes! { [light:0, dark:1]
width = 406.0, 406.0;
height = 380.0, 380.0;
background = application::component_browser::component_list_panel::background_color, application::component_browser::component_list_panel::background_color;
corner_radius = 14.0, 14.0;
corner_radius = 20.0, 20.0;
breadcrumbs {
padding_y = 8.0, 8.0;
padding_x = 10.0, 10.0;
height = 40.0, 40.0;
}
}
component_list_panel {
width = 190.0, 190.0;

View File

@ -219,8 +219,11 @@ impl Model {
background_height: f32,
background_y_offset: f32,
) {
let background_size = Vector2::new(content_size.x, background_height);
self.background.set_size(background_size + Vector2(2.0 * background_padding_x, 0.0));
let background_x = content_size.x + 2.0 * background_padding_x;
let background_x = background_x.min(size.x);
let background_size = Vector2::new(background_x, background_height);
self.background.set_size(background_size);
self.background.set_corner_radius(background_size.y / 2.0);
self.background
.set_y(-background_height / 2.0 - content_size.y / 2.0 - background_y_offset);

View File

@ -19,3 +19,4 @@ enso-reflect = { path = "../../reflect" }
enso-metamodel = { path = "../../metamodel", features = ["rust"] }
enso-metamodel-lexpr = { path = "../../metamodel/lexpr" }
lexpr = "0.2.6"
pretty_assertions = "1.4"

View File

@ -62,6 +62,30 @@ impl DocParser {
/// Text rendered as HTML (may contain HTML tags).
pub type HtmlString = String;
/// A description of a single argument in the documentation. The name is delimited from the
/// description using a colon.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct Argument {
/// Name of the argument.
pub name: String,
/// Description of the argument.
pub description: HtmlString,
}
impl Argument {
/// Convert the given string to the argument description.
pub fn new(text: &str) -> Self {
// We split by the first colon or space, whatever comes first.
// Typically a colon must be used as a separator, but in some documentation snippets we
// have there is no colon and the name of the argument is simply the first word.
let split = text.splitn(2, |c| c == ':' || c == ' ');
let (name, description) = split.collect_tuple().unwrap_or((text, ""));
let name = name.trim().to_string();
let description = description.trim().to_string();
Self { name, description }
}
}
/// A single section of the documentation.
#[derive(Hash, Debug, Clone, PartialEq, Eq)]
#[allow(missing_docs)]
@ -78,6 +102,10 @@ pub enum DocSection {
/// The elements that make up this paragraph.
body: HtmlString,
},
/// A list of items. Each item starts with a dash (`-`).
List { items: Vec<HtmlString> },
/// A list of items, but each item is an [`Argument`]. Starts with `Arguments:` keyword.
Arguments { args: Vec<Argument> },
/// The section that starts with the key followed by the colon and the body.
Keyed {
/// The section key.
@ -106,7 +134,9 @@ pub enum DocSection {
struct DocSectionCollector {
sections: Vec<DocSection>,
in_secondary_section: bool,
inside_arguments: bool,
current_body: String,
current_list: Vec<String>,
}
impl DocSectionCollector {
@ -118,6 +148,7 @@ impl DocSectionCollector {
Some(DocSection::Paragraph { body, .. })
| Some(DocSection::Keyed { body, .. })
| Some(DocSection::Marked { body, .. }) => *body = text,
Some(DocSection::List { .. }) | Some(DocSection::Arguments { .. }) => (),
Some(DocSection::Tag { .. }) | None =>
self.sections.push(DocSection::Paragraph { body: text }),
}
@ -127,13 +158,16 @@ impl DocSectionCollector {
self.finish_section();
let result = self.sections.drain(..).collect();
let current_body = std::mem::take(&mut self.current_body);
let current_list = std::mem::take(&mut self.current_list);
let sections = std::mem::take(&mut self.sections);
*self = Self {
// Reuse the (empty) buffers.
current_body,
current_list,
sections,
// Reset the rest of state.
in_secondary_section: Default::default(),
inside_arguments: Default::default(),
};
result
}
@ -155,6 +189,9 @@ impl<L> TokenConsumer<L> for DocSectionCollector {
fn enter_keyed_section(&mut self, header: Span<'_, L>) {
self.finish_section();
let key = header.to_string();
if key.eq_ignore_ascii_case("Arguments") {
self.inside_arguments = true;
}
let body = Default::default();
self.sections.push(DocSection::Keyed { key, body });
}
@ -164,12 +201,10 @@ impl<L> TokenConsumer<L> for DocSectionCollector {
}
fn start_list(&mut self) {
self.current_body.push_str("<ul>");
self.current_list.clear();
}
fn start_list_item(&mut self) {
self.current_body.push_str("<li>");
}
fn start_list_item(&mut self) {}
fn start_paragraph(&mut self) {
let first_content = !self.in_secondary_section && self.current_body.is_empty();
@ -203,8 +238,19 @@ impl<L> TokenConsumer<L> for DocSectionCollector {
fn end(&mut self, scope: ScopeType) {
match scope {
ScopeType::List => self.current_body.push_str("</ul>"),
ScopeType::ListItem => (),
ScopeType::List => {
let items = mem::take(&mut self.current_list);
if self.inside_arguments {
let args = items.iter().map(|arg| Argument::new(arg)).collect();
self.sections.push(DocSection::Arguments { args });
self.inside_arguments = false;
} else {
self.sections.push(DocSection::List { items })
}
}
ScopeType::ListItem => {
self.current_list.push(self.current_body.drain(..).collect());
}
ScopeType::Paragraph => (),
ScopeType::Raw => self.current_body.push_str("</pre>"),
}

View File

@ -34,6 +34,7 @@ use enso_prelude::*;
pub mod doc_sections;
pub use doc_sections::parse;
pub use doc_sections::Argument;
pub use doc_sections::DocSection;
@ -691,3 +692,70 @@ pub trait TokenConsumer<L> {
/// the most recently-opened scope that has not been closed.
fn end(&mut self, scope: ScopeType);
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_list_parsing() {
use crate::doc_sections::Argument;
use crate::DocSection::*;
use crate::Mark::*;
use crate::Tag::*;
let docs = r#"
ALIAS From Text
Parses a textual representation of an integer into an integer number, returning
a `Number_Parse_Error` if the text does not represent a valid integer.
Arguments:
- text: The text to parse into a integer.
- radix: The number base to use for parsing (defaults to 10). `radix`
must be between 2 and 36 (inclusive)
- arg argument without colon
- argument_without_description
- List item 1
- List item 2
- List item 3
> Example
Parse the text "20220216" into an integer number.
Integer.parse "20220216""#;
let res = parse(docs);
let expected = [
Tag { tag: Alias, body: "From Text".into() },
Paragraph { body: "Parses a textual representation of an integer into an integer number, \
returning a <code>Number_Parse_Error</code> if the text does not represent a valid integer.".into() },
Keyed { key: "Arguments".into(), body: "".into() },
Arguments { args: [
Argument {
name: "text".into(),
description: "The text to parse into a integer.".into() },
Argument {
name: "radix".into(),
description: "The number base to use for parsing (defaults to 10). <code>radix</code> \
must be between 2 and 36 (inclusive)".into()
},
Argument {
name: "arg".into(),
description: "argument without colon".into()
},
Argument {
name: "argument_without_description".into(),
description: default(),
}].to_vec()
},
List { items: ["List item 1".into(), "List item 2".into(), "List item 3".into()].to_vec() },
Marked {
mark: Example,
header: Some("Example".into()),
body: "<p>Parse the text \"20220216\" into an integer number.<pre>\nInteger.parse \"20220216\"</pre>".into()
}].to_vec();
assert_eq!(res, expected);
}
}

View File

@ -1,4 +1,4 @@
//! Prints a debug representation of Enso documentation found in the given Enso sourec file(s).
//! Prints a debug representation of Enso documentation found in the given Enso source file(s).
#![recursion_limit = "256"]
// === Features ===