Skip to content

Commit

Permalink
Search bar for forc-doc (#5269)
Browse files Browse the repository at this point in the history
## Description

Closes #3480

A simple search bar for forc-doc. It uses a case-insensitive search of
item names, across the library and all of its dependencies. Names in the
search results are highlighted similar to docs.rs.

![Nov-14-2023
22-21-07](https://github.com/FuelLabs/sway/assets/47993817/0a8f5bea-eace-405c-a26e-e8c17b9756c1)

## Checklist

- [x] I have linked to any relevant issues.
- [x] I have commented my code, particularly in hard-to-understand
areas.
- [ ] I have updated the documentation where relevant (API docs, the
reference, and the Sway book).
- [x] I have added tests that prove my fix is effective or that my
feature works.
- [ ] I have added (or requested a maintainer to add) the necessary
`Breaking*` or `New Feature` labels where relevant.
- [x] I have done my best to ensure that my PR adheres to [the Fuel Labs
Code Review
Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md).
- [x] I have requested a review from the relevant team or maintainers.

---------

Co-authored-by: Chris O'Brien <[email protected]>
Co-authored-by: Joshua Batty <[email protected]>
  • Loading branch information
3 people authored Nov 15, 2023
1 parent 6bf7d4f commit d07a84b
Show file tree
Hide file tree
Showing 18 changed files with 438 additions and 118 deletions.
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion forc-plugins/forc-doc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,20 @@ anyhow = "1.0.65"
clap = { version = "4.0.18", features = ["derive"] }
colored = "2.0.0"
comrak = "0.16"
expect-test = "1.4.1"
forc-pkg = { version = "0.47.0", path = "../../forc-pkg" }
forc-util = { version = "0.47.0", path = "../../forc-util" }
horrorshow = "0.8.4"
include_dir = "0.7.3"
minifier = "0.3.0"
opener = "0.5.0"
serde = "1.0"
serde_json = "1.0"
sway-ast = { version = "0.47.0", path = "../../sway-ast" }
sway-core = { version = "0.47.0", path = "../../sway-core" }
sway-lsp = { version = "0.47.0", path = "../../sway-lsp" }
sway-types = { version = "0.47.0", path = "../../sway-types" }
swayfmt = { version = "0.47.0", path = "../../swayfmt" }

[dev-dependencies]
dir_indexer = "0.0.2"
expect-test = "1.4.1"
4 changes: 2 additions & 2 deletions forc-plugins/forc-doc/src/doc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use sway_types::{BaseIdent, Spanned};
mod descriptor;
pub mod module;

#[derive(Default)]
#[derive(Default, Clone)]
pub(crate) struct Documentation(pub(crate) Vec<Document>);
impl Documentation {
/// Gather [Documentation] from the [TyProgram].
Expand Down Expand Up @@ -214,7 +214,7 @@ impl Document {
preview_opt: self.preview_opt(),
}
}
fn preview_opt(&self) -> Option<String> {
pub(crate) fn preview_opt(&self) -> Option<String> {
create_preview(self.raw_attributes.clone())
}
}
19 changes: 18 additions & 1 deletion forc-plugins/forc-doc/src/doc/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub(crate) type ModulePrefixes = Vec<String>;
/// Information about a Sway module.
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub(crate) struct ModuleInfo {
/// The preceeding module names, used in navigating between modules.
/// The preceding module names, used in navigating between modules.
pub(crate) module_prefixes: ModulePrefixes,
/// Doc attributes of a module.
/// Renders into the module level docstrings.
Expand Down Expand Up @@ -121,6 +121,7 @@ impl ModuleInfo {
.map(|file_path_str| file_path_str.to_string())
.ok_or_else(|| anyhow::anyhow!("There will always be at least the item name"))
}

/// Compares the current `module_info` to the next `module_info` to determine how many directories to go back to make
/// the next file path valid, and returns that path as a `String`.
///
Expand Down Expand Up @@ -169,6 +170,22 @@ impl ModuleInfo {
Ok(new_path)
}
}

/// Returns the relative path to the root of the project.
///
/// Example:
/// ```
/// current_location = "project_root/module/submodule1/submodule2/struct.Name.html"
/// result = "../.."
/// ```
/// In this case the first module to match is "module", so we have no need to go back further than that.
pub(crate) fn path_to_root(&self) -> String {
(0..self.module_prefixes.len())
.map(|_| "..")
.collect::<Vec<_>>()
.join("/")
}

/// Create a path `&str` for navigation from the `module.depth()` & `file_name`.
///
/// This is only used for shorthand path syntax, e.g `../../file_name.html`.
Expand Down
22 changes: 15 additions & 7 deletions forc-plugins/forc-doc/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
doc::Documentation,
render::{constant::INDEX_FILENAME, RenderedDocumentation},
search::write_search_index,
};
use anyhow::{bail, Result};
use clap::Parser;
Expand All @@ -20,6 +21,7 @@ use sway_core::{language::ty::TyProgram, BuildTarget, Engines};
mod cli;
mod doc;
mod render;
mod search;
mod tests;

pub(crate) const ASSETS_DIR_NAME: &str = "static.files";
Expand Down Expand Up @@ -99,7 +101,7 @@ fn build_docs(
program_info: ProgramInfo,
doc_path: &Path,
build_instructions: &Command,
) -> Result<()> {
) -> Result<Documentation> {
let Command {
document_private_items,
no_deps,
Expand Down Expand Up @@ -134,7 +136,7 @@ fn build_docs(
.map(|ver| format!("Forc v{}.{}.{}", ver.major, ver.minor, ver.patch));
// render docs to HTML
let rendered_docs = RenderedDocumentation::from_raw_docs(
raw_docs,
raw_docs.clone(),
RenderPlan::new(no_deps, document_private_items, engines),
root_attributes,
ty_program.kind,
Expand All @@ -145,7 +147,7 @@ fn build_docs(
write_content(rendered_docs, doc_path)?;
println!(" {}", "Finished".bold().yellow());

Ok(())
Ok(raw_docs)
}

fn write_content(rendered_docs: RenderedDocumentation, doc_path: &Path) -> Result<()> {
Expand Down Expand Up @@ -218,10 +220,11 @@ pub fn compile_html(
&engines,
)?;

if !build_instructions.no_deps {
let raw_docs = if !build_instructions.no_deps {
let order = plan.compilation_order();
let graph = plan.graph();
let manifest_map = plan.manifest_map();
let mut raw_docs = Documentation(Vec::new());

for (node, (compile_result, _handler)) in order.iter().zip(compile_results) {
let id = &graph[*node].id();
Expand All @@ -242,9 +245,12 @@ pub fn compile_html(
pkg_manifest: pkg_manifest_file,
};

build_docs(program_info, &doc_path, build_instructions)?;
raw_docs
.0
.extend(build_docs(program_info, &doc_path, build_instructions)?.0);
}
}
raw_docs
} else {
let ty_program = match compile_results
.pop()
Expand All @@ -263,8 +269,10 @@ pub fn compile_html(
manifest: &manifest,
pkg_manifest,
};
build_docs(program_info, &doc_path, build_instructions)?;
}
build_docs(program_info, &doc_path, build_instructions)?
};
write_search_index(&doc_path, raw_docs)?;

Ok((doc_path, pkg_manifest.to_owned()))
}

Expand Down
11 changes: 8 additions & 3 deletions forc-plugins/forc-doc/src/render/index.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
//! Handles creation of `index.html` files.
use crate::{
doc::module::ModuleInfo,
render::{constant::IDENTITY, link::DocLinks, sidebar::*, BlockTitle, DocStyle, Renderable},
render::{
constant::IDENTITY, link::DocLinks, search::generate_searchbar, sidebar::*, BlockTitle,
DocStyle, Renderable,
},
RenderPlan, ASSETS_DIR_NAME,
};
use anyhow::Result;
Expand Down Expand Up @@ -58,13 +61,14 @@ impl Renderable for AllDocIndex {
: sidebar;
main {
div(class="width-limiter") {
// : generate_searchbar();
: generate_searchbar(self.project_name.clone());
section(id="main-content", class="content") {
h1(class="fqn") {
span(class="in-band") { : "List of all items" }
}
: doc_links;
}
section(id="search", class="search-results");
}
}
script(src=format!("../{ASSETS_DIR_NAME}/highlight.js"));
Expand Down Expand Up @@ -165,7 +169,7 @@ impl Renderable for ModuleIndex {
: sidebar;
main {
div(class="width-limiter") {
// : generate_searchbar();
: generate_searchbar(self.module_info.clone());
section(id="main-content", class="content") {
div(class="main-heading") {
h1(class="fqn") {
Expand All @@ -192,6 +196,7 @@ impl Renderable for ModuleIndex {
}
: doc_links;
}
section(id="search", class="search-results");
}
}
script(src=sway_hjs);
Expand Down
7 changes: 4 additions & 3 deletions forc-plugins/forc-doc/src/render/item/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
use crate::{
doc::module::ModuleInfo,
render::{
constant::IDENTITY, item::context::ItemContext, sidebar::*, title::DocBlockTitle, DocStyle,
Renderable,
constant::IDENTITY, item::context::ItemContext, search::generate_searchbar, sidebar::*,
title::DocBlockTitle, DocStyle, Renderable,
},
RenderPlan, ASSETS_DIR_NAME,
};
Expand Down Expand Up @@ -122,7 +122,7 @@ impl Renderable for ItemBody {
// this is the main code block
main {
div(class="width-limiter") {
// : generate_searchbar();
: generate_searchbar(module_info.clone());
section(id="main-content", class="content") {
div(class="main-heading") {
h1(class="fqn") {
Expand Down Expand Up @@ -158,6 +158,7 @@ impl Renderable for ItemBody {
: item_context.unwrap();
}
}
section(id="search", class="search-results");
}
}
script(src=sway_hjs);
Expand Down
99 changes: 77 additions & 22 deletions forc-plugins/forc-doc/src/render/search.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,92 @@
//! Generates the searchbar.
use horrorshow::{box_html, RenderBox};
use crate::doc::module::ModuleInfo;
use horrorshow::{box_html, Raw, RenderBox};
use minifier::js::minify;

// TODO: Implement Searchbar
// - Add search functionality to search bar
// - Add help.html support
// - Add settings.html support
pub(crate) fn _generate_searchbar() -> Box<dyn RenderBox> {
pub(crate) fn generate_searchbar(module_info: ModuleInfo) -> Box<dyn RenderBox> {
let path_to_root = module_info.path_to_root();
// Since this searchbar is rendered on all pages, we need to inject the path the the root into the script.
// Therefore, we can't simply import this script from a javascript file.
let minified_script = minify(&format!(r#"
function onSearchFormSubmit(event) {{
event.preventDefault();
const searchQuery = document.getElementById("search-input").value;
const url = new URL(window.location.href);
if (searchQuery) {{
url.searchParams.set('search', searchQuery);
}} else {{
url.searchParams.delete('search');
}}
history.pushState({{ search: searchQuery }}, "", url);
window.dispatchEvent(new HashChangeEvent("hashchange"));
}}
document.addEventListener('DOMContentLoaded', () => {{
const searchbar = document.getElementById("search-input");
const searchForm = document.getElementById("search-form");
searchbar.addEventListener("keyup", function(event) {{
searchForm.dispatchEvent(new Event('submit'));
}});
searchbar.addEventListener("search", function(event) {{
searchForm.dispatchEvent(new Event('submit'));
}});
function onQueryParamsChange() {{
const searchParams = new URLSearchParams(window.location.search);
const query = searchParams.get("search");
const searchSection = document.getElementById('search');
const mainSection = document.getElementById('main-content');
const searchInput = document.getElementById('search-input');
if (query) {{
searchInput.value = query;
const results = Object.values(SEARCH_INDEX).flat().filter(item => {{
const lowerQuery = query.toLowerCase();
return item.name.toLowerCase().includes(lowerQuery);
}});
const header = `<h1>Results for ${{query}}</h1>`;
if (results.length > 0) {{
const resultList = results.map(item => {{
const formattedName = `<span class="type ${{item.type_name}}">${{item.name}}</span>`;
const name = [...item.module_info, formattedName].join("::");
const path = ["{}", ...item.module_info, item.html_filename].join("/");
const left = `<td><span>${{name}}</span></td>`;
const right = `<td><p>${{item.preview}}</p></td>`;
return `<tr onclick="window.location='${{path}}';">${{left}}${{right}}</tr>`;
}}).join('');
searchSection.innerHTML = `${{header}}<table>${{resultList}}</table>`;
}} else {{
searchSection.innerHTML = `${{header}}<p>No results found.</p>`;
}}
searchSection.setAttribute("class", "search-results");
mainSection.setAttribute("class", "content hidden");
}} else {{
searchSection.setAttribute("class", "search-results hidden");
mainSection.setAttribute("class", "content");
}}
}}
window.addEventListener('hashchange', onQueryParamsChange);
// Check for any query parameters initially
onQueryParamsChange();
}}
);"#, path_to_root)).to_string();
box_html! {
script(src=format!("{}/search.js", path_to_root), type="text/javascript");
script {
: Raw(minified_script)
}
nav(class="sub") {
form(class="search-form") {
form(id="search-form", class="search-form", onsubmit="onSearchFormSubmit(event)") {
div(class="search-container") {
span;
input(
id="search-input",
class="search-input",
name="search",
autocomplete="off",
spellcheck="false",
placeholder="Click or press ‘S’ to search, ‘?’ for more options…",
placeholder="Search the docs...",
type="search"
);
div(id="help-button", title="help", tabindex="-1") {
a(href="../help.html") { : "?" }
}
div(id="settings-menu", tabindex="-1") {
a(href="../settings.html", title="settings") {
img(
width="22",
height="22",
alt="change settings",
src="../static.files/wheel.svg"
)
}
}
}
}
}
Expand Down
Loading

0 comments on commit d07a84b

Please sign in to comment.