├── .github └── workflows │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── README.md ├── cliff.toml ├── outline-design.txt ├── src ├── bin │ ├── color.rs │ ├── dashboard │ │ ├── mod.rs │ │ └── ui │ │ │ ├── database.rs │ │ │ ├── database │ │ │ ├── cache.rs │ │ │ └── cache │ │ │ │ ├── inner.rs │ │ │ │ └── util.rs │ │ │ ├── mod.rs │ │ │ ├── registry.rs │ │ │ ├── search.rs │ │ │ ├── ver_feat_toml.rs │ │ │ └── version_features.rs │ ├── database │ │ ├── cache_info.rs │ │ ├── features │ │ │ ├── mod.rs │ │ │ ├── parse_cargo_toml.rs │ │ │ └── ui.rs │ │ ├── meta.rs │ │ ├── mod.rs │ │ ├── pkg_key.rs │ │ └── util.rs │ ├── event │ │ └── mod.rs │ ├── frame │ │ ├── help.md │ │ ├── help.rs │ │ ├── mod.rs │ │ ├── update.rs │ │ └── util.rs │ ├── fuzzy.rs │ ├── local_registry.rs │ ├── logger.rs │ ├── main.rs │ ├── page │ │ ├── content.rs │ │ ├── layout.rs │ │ ├── mod.rs │ │ ├── navi │ │ │ ├── mod.rs │ │ │ └── outline.rs │ │ ├── outline.rs │ │ ├── page_fold.rs │ │ ├── page_scroll.rs │ │ └── panel.rs │ ├── tui.rs │ └── ui │ │ ├── mod.rs │ │ ├── scrollable │ │ ├── generics.rs │ │ ├── interaction.rs │ │ ├── markdown │ │ │ ├── fallback.rs │ │ │ ├── heading.rs │ │ │ ├── ingerated.rs │ │ │ ├── mod.rs │ │ │ ├── parse │ │ │ │ ├── block.rs │ │ │ │ ├── blocks.rs │ │ │ │ ├── code_block.rs │ │ │ │ ├── element.rs │ │ │ │ ├── entry_point.rs │ │ │ │ ├── entry_point │ │ │ │ │ ├── snapshots │ │ │ │ │ │ ├── term_rustdoc__ui__scrollable__markdown__parse__entry_point__tests__parse_markdown-StyledLines.snap │ │ │ │ │ │ ├── term_rustdoc__ui__scrollable__markdown__parse__entry_point__tests__parse_markdown-parsed.snap │ │ │ │ │ │ ├── term_rustdoc__ui__scrollable__markdown__parse__entry_point__tests__parse_markdown.snap │ │ │ │ │ │ ├── term_rustdoc__ui__scrollable__markdown__parse__entry_point__tests__parse_markdown_links-StyledLines.snap │ │ │ │ │ │ ├── term_rustdoc__ui__scrollable__markdown__parse__entry_point__tests__parse_markdown_links-parsed.snap │ │ │ │ │ │ └── term_rustdoc__ui__scrollable__markdown__parse__entry_point__tests__parse_markdown_links.snap │ │ │ │ │ └── tests.rs │ │ │ │ ├── line.rs │ │ │ │ ├── list.rs │ │ │ │ ├── meta_tag.rs │ │ │ │ ├── mod.rs │ │ │ │ └── word.rs │ │ │ ├── region.rs │ │ │ ├── render.rs │ │ │ └── wrapped.rs │ │ ├── mod.rs │ │ └── render.rs │ │ └── surround.rs ├── lib.rs ├── tree │ ├── id.rs │ ├── impls │ │ ├── debug.rs │ │ ├── mod.rs │ │ └── show.rs │ ├── mod.rs │ ├── nodes │ │ ├── enums.rs │ │ ├── impls.rs │ │ ├── imports.rs │ │ ├── item_inner.rs │ │ ├── mod.rs │ │ ├── structs.rs │ │ ├── traits.rs │ │ └── unions.rs │ ├── stats │ │ ├── impls.rs │ │ └── mod.rs │ ├── tag.rs │ ├── textline.rs │ └── textline │ │ └── fold.rs ├── type_name │ ├── mod.rs │ ├── render │ │ └── mod.rs │ └── style │ │ ├── decl │ │ ├── function.rs │ │ ├── mod.rs │ │ └── struct_.rs │ │ ├── function.rs │ │ ├── generics.rs │ │ ├── mod.rs │ │ ├── path.rs │ │ ├── type_.rs │ │ └── utils.rs └── util.rs └── tests ├── compile_doc.rs ├── integration ├── Cargo.toml ├── build.rs └── src │ └── lib.rs └── parse-json-docs ├── fn_item_decl.rs ├── generate_doc_json.rs ├── main.rs ├── parse.rs ├── snapshots ├── parse_json_docs__fn_item_decl__DeclarationLines-fn-items.snap ├── parse_json_docs__fn_item_decl__DeclarationLines-methods.snap ├── parse_json_docs__fn_item_decl__DeclarationLines-structs.snap ├── parse_json_docs__local_items.snap ├── parse_json_docs__parse__DModule.snap ├── parse_json_docs__parse__empty-tree-with-same-depth.snap ├── parse_json_docs__parse__flatten-tree.snap ├── parse_json_docs__parse__item-tree.snap ├── parse_json_docs__parse__show-id.snap ├── parse_json_docs__parse__show-prettier.snap └── parse_json_docs__syntect_set__syntax_set.snap └── syntect_set.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "term-rustdoc" 3 | version = "0.2.0" 4 | edition = "2021" 5 | autobins = false 6 | exclude = ["**/snapshots/", "tests/", "outline-design.txt", "cliff.toml", "CHANGELOG.md", ".github"] 7 | repository = "https://github.com/zjp-CN/term-rustdoc" 8 | description = "A TUI for Rust docs." 9 | license = "MIT" 10 | 11 | [[bin]] 12 | name = "term-rustdoc" 13 | path = "src/bin/main.rs" 14 | 15 | [dependencies] 16 | rustdoc-types = "0.41" 17 | rustdoc-json = "0.9" 18 | redb = "2.4" 19 | 20 | serde = { version = "1", features = ["derive", "rc"] } 21 | serde_json = "1" 22 | 23 | xz2 = "0.1" 24 | bytesize = "2" 25 | 26 | termtree = "0.5" 27 | compact_str = { version = "0.9", features = ["serde"] } 28 | bincode = { version = "2.0.0-rc.3", features = ["serde"] } 29 | 30 | color-eyre = "0.6" 31 | rustc-hash = "2" 32 | regex = "1" 33 | 34 | tracing = "0.1" 35 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 36 | 37 | constcat = "0.6" 38 | 39 | ratatui = "0.29.0" 40 | crossterm = "0.29" 41 | textwrap = "0.16" 42 | syntect = "5.1" 43 | pulldown-cmark = "0.13" 44 | unicode-width = "0.2" 45 | itertools = "0.14" 46 | smallvec = "1.13" 47 | icu_segmenter = "1" 48 | 49 | home = "0.5" 50 | dirs = "6" 51 | walkdir = "2" 52 | semver = "1" 53 | nucleo-matcher = "0.3" 54 | rayon = "1" 55 | tempfile = "3" 56 | cargo_toml = { version = "0.22", features = ["features"] } 57 | self_cell = "1" 58 | 59 | [dev-dependencies] 60 | insta = "1" 61 | similar-asserts = "1.5" 62 | 63 | # quicker diffing in tests of insta/similar-asserts 64 | [profile.dev.package.similar] 65 | opt-level = 3 66 | 67 | # The profile that 'cargo dist' will build with 68 | [profile.dist] 69 | inherits = "release" 70 | lto = "thin" 71 | 72 | [workspace] 73 | members = ["./tests/integration/"] 74 | 75 | # Config for 'cargo dist' 76 | [workspace.metadata.dist] 77 | # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) 78 | cargo-dist-version = "0.11.1" 79 | # CI backends to support 80 | ci = ["github"] 81 | # The installers to generate for each app 82 | installers = [] 83 | # Target platforms to build apps for (Rust target-triple syntax) 84 | targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] 85 | # Publish jobs to run in CI 86 | pr-run-mode = "plan" 87 | windows-archive = ".tar.gz" 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://img.shields.io/crates/v/term-rustdoc)](https://crates.io/crates/term-rustdoc) 2 | [![](https://img.shields.io/crates/l/term-rustdoc/0.1.0)](https://github.com/zjp-CN/term-rustdoc/) 3 | [![Release](https://github.com/zjp-CN/term-rustdoc/actions/workflows/release.yml/badge.svg)](https://github.com/zjp-CN/term-rustdoc) 4 | 5 | A TUI for Rust docs that aims to improve the UX on tree view and generic code. 6 | 7 | This tool utilizes the nightly compiler to compile JSON doc for a package. 8 | Therefore, make sure the nightly toolchain is usable on your machine. 9 | 10 | It's recommended to have `rust-docs-json` too, so you can run: 11 | 12 | ```console 13 | rustup rustup toolchain install nightly --component rust-docs-json 14 | 15 | # or add the component if nightly toolchain lacks it 16 | rustup component add --toolchain nightly rust-docs-json 17 | rustup update nightly 18 | ``` 19 | 20 | This tool is very immature, and far from the target for now. It only provides 21 | the basic ability to compile and cache docs, and view markdown docs. 22 | 23 | Key shortcuts or usage help can be found via `F1` key press. 24 | 25 | ![help](https://github.com/zjp-CN/term-rustdoc/assets/25300418/62166720-ba49-49af-9da4-77faaef03d02) 26 | 27 | More screen shots can be found [here][issue1]. 28 | 29 | [issue1]: https://github.com/zjp-CN/term-rustdoc/issues/1 30 | 31 | # Roadmap 32 | 33 | - [x] item outline 34 | - [x] expand / fold (based on module tree) 35 | - [x] expand zero level items (i.e. items in root module with sub modules folded) 36 | - [x] expand to first level items 37 | - [x] focus on the latest module only (but with all other level modules folded) 38 | - [x] expand all public items 39 | - [ ] features related 40 | - [x] doc content 41 | - [x] text wrapping 42 | - [x] syntax highlighting in codeblocks 43 | - [x] recognize rustdoc syntax attributes on codeblocks 44 | - [x] in links 45 | - [x] in codeblock (default to rust, hide lines, etc) 46 | - [ ] navigation 47 | - [x] markdown outline 48 | - [ ] item's associated items/fields outline 49 | - [ ] package source / DashBoard Popup 50 | - [x] local 51 | - [x] local registry src dirs 52 | - [x] fuzzing search 53 | - [x] select pkgs to compile docs and cache the artifacts in local db files 54 | - [x] caches in database (json docs that have been generated will be cached in local db) 55 | - [x] cache raw JSON output and compress it via xz 56 | - [x] cache parsed output for faster loading and compress it via xz 57 | - [x] Sorting the cache list for all items or in groups 58 | - [ ] local paths to Cargo.toml: low priority 59 | - [ ] non-local (i.e. download pkgs from the web): low priority 60 | - [ ] configuration 61 | - [ ] theme: low priority 62 | - [ ] keybind: low priority 63 | - [ ] fuzzing search 64 | - [ ] by item name 65 | - [ ] by all documentation contents 66 | - [ ] by function/method signature 67 | - [ ] on concrete types 68 | - [ ] on generic types 69 | - [ ] on trait bounds 70 | - [ ] by crate features 71 | - [ ] generic types enhancement 72 | - [ ] generic type parameters 73 | - [ ] list concrete candidate types that meet the trait bounds 74 | - from within the current pkg 75 | - from within the caches in database 76 | - [ ] list the functions/methods that 77 | - [ ] return generic types that hold the same trait bounds 78 | - [ ] return concrete candidate types 79 | - [ ] list the function/methods that 80 | - [ ] accept generic types that hold the same trait bounds 81 | - [ ] accept concrete candidate types 82 | - [ ] lifetime parameters 83 | - [ ] variance (lack of this info in json docs, but maybe not hard to have it) 84 | - [ ] concrete types 85 | - [ ] list methods in which the concrete `Type` and its ownership variants `&Type` / `&mut Type` is 86 | - [ ] receiver type 87 | - [ ] argument type 88 | - [ ] return type 89 | - [ ] traits 90 | - [ ] classify trait implementors 91 | - [ ] by ownership (`impl Trait` for `Type` vs `&mut Type` vs `&Type` vs `Box`) 92 | - [ ] by concrete vs generic 93 | 94 | # Misc/Basics 95 | 96 | * [data access policy on crates.io ](https://crates.io/data-access) 97 | * can be accessed without rate limits to query crates' history versions, features and dependencies 98 | * local registry cache: 99 | * `~/.cargo/registry/src/` contains the official crates.io registry and other mirror/custom registries 100 | * `~/.cargo/registry/index/` contains the API URLs to query or download crates from these registries 101 | 102 | ## id rules 103 | 104 | Mainly steal from [`id_from_item_inner`](https://doc.rust-lang.org/nightly/nightly-rustc/rustdoc/json/conversions/fn.id_from_item_inner.html) 105 | 106 | `[IMPL:]CRATE_ID:ITEM_ID[:NAME_ID][-EXTRA]`: 107 | * `impl` 108 | * `a:` for auto impls 109 | * `b:` for blanket impls 110 | * empty if others, like non-impls, inherent impls, normal trait impls 111 | * `name` is the item's name if available (it's not for impl blocks for example). 112 | * `extra` is used for reexports: it contains the ID of the reexported item. It is used to allow 113 | to have items with the same name but different types to both appear in the generated JSON. 114 | 115 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ default configuration file 2 | # https://git-cliff.org/docs/configuration 3 | # 4 | # Lines starting with "#" are comments. 5 | # Configuration options are organized into tables and keys. 6 | # See documentation for more information on available options. 7 | 8 | [changelog] 9 | # changelog header 10 | header = """ 11 | # Changelog\n 12 | All notable changes to this project will be documented in this file.\n 13 | """ 14 | # template for the changelog body 15 | # https://keats.github.io/tera/docs/#introduction 16 | body = """ 17 | {% if version %}\ 18 | ## [{{ version }}] - {{ timestamp | date(format="%Y-%m-%d") }} 19 | {% else %}\ 20 | ## [unreleased] 21 | {% endif %}\ 22 | {% for group, commits in commits | group_by(attribute="group") %} 23 | ### {{ group | striptags | trim | upper_first }} 24 | {% for commit in commits %} 25 | - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ 26 | {% if commit.breaking %}[**breaking**] {% endif %}\ 27 | {{ commit.message | upper_first }}\ 28 | {% endfor %} 29 | {% endfor %}\n 30 | """ 31 | # template for the changelog footer 32 | footer = """ 33 | 34 | """ 35 | # remove the leading and trailing s 36 | trim = true 37 | # postprocessors 38 | postprocessors = [ 39 | # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL 40 | ] 41 | 42 | [git] 43 | # parse the commits based on https://www.conventionalcommits.org 44 | conventional_commits = true 45 | # filter out the commits that are not conventional 46 | filter_unconventional = true 47 | # process each line of a commit as an individual commit 48 | split_commits = false 49 | # regex for preprocessing the commit messages 50 | commit_preprocessors = [ 51 | # Replace issue numbers 52 | #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, 53 | # Check spelling of the commit with https://github.com/crate-ci/typos 54 | # If the spelling is incorrect, it will be automatically fixed. 55 | #{ pattern = '.*', replace_command = 'typos --write-changes -' }, 56 | ] 57 | # regex for parsing and grouping commits 58 | commit_parsers = [ 59 | { message = "^feat", group = "🚀 Features" }, 60 | { message = "^fix", group = "🐛 Bug Fixes" }, 61 | { message = "^doc", group = "📚 Documentation" }, 62 | { message = "^perf", group = "⚡ Performance" }, 63 | { message = "^refactor", group = "🚜 Refactor" }, 64 | { message = "^style", group = "🎨 Styling" }, 65 | { message = "^test", group = "🧪 Testing" }, 66 | { message = "^chore\\(release\\): prepare for", skip = true }, 67 | { message = "^chore\\(deps.*\\)", skip = true }, 68 | { message = "^chore\\(pr\\)", skip = true }, 69 | { message = "^chore\\(pull\\)", skip = true }, 70 | { message = "^chore|^ci", group = "⚙️ Miscellaneous Tasks" }, 71 | { body = ".*security", group = "🛡️ Security" }, 72 | { message = "^revert", group = "◀️ Revert" }, 73 | ] 74 | # protect breaking changes from being skipped due to matching a skipping commit_parser 75 | protect_breaking_commits = false 76 | # filter out the commits that are not matched by commit parsers 77 | filter_commits = true 78 | # regex for matching git tags 79 | # tag_pattern = "v[0-9].*" 80 | # regex for skipping tags 81 | # skip_tags = "" 82 | # regex for ignoring tags 83 | # ignore_tags = "" 84 | # sort the tags topologically 85 | topo_order = false 86 | # sort the commits inside sections by oldest/newest order 87 | sort_commits = "oldest" 88 | # limit the number of commits included in the changelog. 89 | # limit_commits = 42 90 | -------------------------------------------------------------------------------- /outline-design.txt: -------------------------------------------------------------------------------- 1 | 2 | ┌───────────┐ Navigation Panel 3 | │Item Detail│ ┌────────────────────┐ 4 | └─────┬─────┘ │Module Tree │ 5 | │ ┌───────┐ │Field/Variant Tree │ 6 | ├─────────┤Modules│ │ITAB Impl Tree │ 7 | │ └───────┘ │Args Type Tree │ 8 | │ │Return Type Tree │ 9 | │ ┌───────────────────┐ ┌───────────────┐ │Construct Fn Tree │ 10 | │ │Data Structures ├─┬────►│Fields/Varaints│ │Non-receiver Fn Tree│ 11 | ├─────────┤(struct/enum/union)│ │ └───────────────┘ └────────────────────┘ 12 | │ └───────────────────┘ │ 13 | │ ▲ │ 14 | │ │ │ ┌─────┐ For methods in all Impls 15 | │ │ ├────►│Impls├────────────────────────┐ 16 | │ ┌────────┐ │ │ └───┬─┘ │ 17 | ├───│Funcions│ │ │ ├─────► Inherent methods │ 18 | │ └──┬─────┘ │ │ │ │ 19 | │ │ ┌──────────┐ │ │ Sort ├─────► Trait methods │ 20 | │ ├──►│Args Types├───┐ │ │ By Name │ │ 21 | │ │ └──────────┘ ├─┤ │ In kinds├─────► Auto traits │ 22 | │ │ ┌───────────┐ ├─┤ │ │ │ 23 | │ └──►│Return Type├──┘ │ │ └─────► Blanket traits │ 24 | │ └───────────┘ │ │ │ 25 | │ │ │ ┌───────┘ 26 | │ │ │ │ ┌──────────────────┐ 27 | │ ┌──────┐ │ │ ├───►│Sort By Args Types├───┐ 28 | └───┤Traits│ │ │ No │ └──────────────────┘ │ 29 | └─┬────┘ TODO │ │ ITAB│ ┌───────────────────┐ │ 30 | │ ┌────────────────┐ │ │ └───►│Sort By Return Type├──┤ 31 | │ │Sub/Super traits│ │ │ └───────────────────┘ │ 32 | │ │Assoc Types │ │ │ ┌───────────────────┐ │ 33 | └──┤Type Parameters │ │ ├─────│Construct Functions│ fn(...) -> Self │ 34 | │Method Args │ │ │ └───────────────────┘ │ │ 35 | │Method Return │ │ │ ┌───────────────────┐ │ │ 36 | └───────┬────────┘ │ └─────│Used as an argument│ │ │ 37 | │ │ └───────────────────┘ │ │ 38 | │ │ fn(.., Self) -> ... │ │ 39 | │ │ i.e. non-receiver fn │ │ 40 | ▼ ▼ │ │ │ 41 | ┌────────────────────────────────────────────┐ │ │ │ 42 | │ ┌──► Concrete Types │ │ │ │ 43 | │ │ │ │ │ │ 44 | │Types──┤ │ │ │ │ 45 | │ │ ┌─► Semi Generic │◄────────┴───────────────────────┴────────────────┘ 46 | │ └──► Generic Types──┤ │ 47 | │ └─► Fully Generic│ 48 | └────────────────────────────────────────────┘ 49 | -------------------------------------------------------------------------------- /src/bin/color.rs: -------------------------------------------------------------------------------- 1 | use ratatui::prelude::{Color, Modifier, Style}; 2 | 3 | pub const BG_CURSOR: Color = Color::Green; 4 | pub const BG_CURSOR_LINE: Color = Color::from_u32(0x0029335b); // #29335b 5 | pub const FG_CURSOR_LINE: Color = Color::from_u32(0x00FFD48E); // #FFD48E 6 | 7 | pub const FG_FEATURES: Color = Color::Cyan; 8 | pub const FG_VERSION: Color = Color::from_u32(0x00686363); // #686363 9 | 10 | pub const PKG_NAME: Style = Style { 11 | fg: Some(Color::White), 12 | add_modifier: Modifier::BOLD, 13 | ..Style::new() 14 | }; 15 | 16 | pub const PKG_VERSION: Style = Style { 17 | fg: Some(FG_VERSION), 18 | ..Style::new() 19 | }; 20 | 21 | pub const PKG_FEATURES: Style = Style { 22 | fg: Some(FG_FEATURES), 23 | add_modifier: Modifier::ITALIC, 24 | ..Style::new() 25 | }; 26 | 27 | // Database Panel 28 | pub const LOADED: Style = Style { 29 | fg: Some(Color::from_u32(0x00FFD48E)), // #FFD48E 30 | ..Style::new() 31 | }; 32 | pub const CACHED: Style = Style { 33 | fg: Some(Color::from_u32(0x006FA2FF)), // #6FA2FF 34 | ..Style::new() 35 | }; 36 | pub const HOLDON: Style = Style { 37 | fg: Some(Color::from_u32(0x00FF768C)), // #FF768C 38 | ..Style::new() 39 | }; 40 | 41 | pub const PKG_TOML: Style = Style { 42 | fg: Some(Color::Green), 43 | add_modifier: Modifier::BOLD, 44 | ..Style::new() 45 | }; 46 | 47 | // Page 48 | pub const HEAD: Style = Style { 49 | fg: Some(Color::DarkGray), 50 | bg: Some(Color::LightCyan), 51 | ..Style::new() 52 | }; 53 | pub const SET: Style = Style::new().bg(Color::Rgb(20, 19, 18)); // #141312 54 | pub const NEW: Style = Style::new(); 55 | pub const DECLARATION_BORDER: Style = Style::new().fg(Color::Gray); 56 | pub const JUMP: Style = Style::new() 57 | .fg(Color::from_u32(0x004083d6)) 58 | .add_modifier(Modifier::BOLD); // #4083d6 59 | -------------------------------------------------------------------------------- /src/bin/dashboard/mod.rs: -------------------------------------------------------------------------------- 1 | mod ui; 2 | 3 | use crate::{event::Sender, fuzzy::Fuzzy, Result}; 4 | use ratatui::layout::Rect; 5 | 6 | use self::ui::UI; 7 | 8 | pub struct DashBoard { 9 | ui: UI, 10 | } 11 | 12 | impl DashBoard { 13 | pub fn new(full: Rect, fuzzy: Fuzzy, sender: Sender) -> Result { 14 | let ui = UI::new(full, fuzzy, sender); 15 | Ok(DashBoard { ui }) 16 | } 17 | 18 | pub fn ui(&mut self) -> &mut UI { 19 | &mut self.ui 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/bin/dashboard/ui/database/cache/inner.rs: -------------------------------------------------------------------------------- 1 | use super::LoadedDoc; 2 | use crate::{ 3 | color::{CACHED, HOLDON, LOADED}, 4 | database::{CachedDocInfo, PkgKey}, 5 | }; 6 | use ratatui::prelude::Style; 7 | use std::time::SystemTime; 8 | 9 | pub enum CacheInner { 10 | /// cached & loaded pkg docs 11 | Loaded(LoadedDoc), 12 | /// cached but not loaded docs 13 | Unloaded(CachedDocInfo), 14 | /// pkgs which is being sent to compile doc 15 | BeingCached(PkgKey, SystemTime), 16 | } 17 | 18 | impl CacheInner { 19 | pub fn pkg_key(&self) -> &PkgKey { 20 | match self { 21 | CacheInner::Loaded(load) => &load.info.pkg, 22 | CacheInner::Unloaded(unload) => &unload.pkg, 23 | CacheInner::BeingCached(pk, _) => pk, 24 | } 25 | } 26 | 27 | pub fn kind(&self) -> (&'static str, Style) { 28 | match self { 29 | CacheInner::Loaded(_) => ("[Loaded]", LOADED), 30 | CacheInner::Unloaded(_) => ("[Cached]", CACHED), 31 | CacheInner::BeingCached(_, _) => ("[HoldOn]", HOLDON), 32 | } 33 | } 34 | } 35 | 36 | impl Eq for CacheInner {} 37 | impl PartialEq for CacheInner { 38 | /// Only use PkgKey to compare if both are equal. 39 | fn eq(&self, other: &Self) -> bool { 40 | self.pkg_key() == other.pkg_key() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/bin/dashboard/ui/database/cache/util.rs: -------------------------------------------------------------------------------- 1 | use super::Cache; 2 | use crate::{database::CachedDocInfo, ui::LineState}; 3 | use std::cmp::Ordering; 4 | use term_rustdoc::{tree::CrateDoc, util::XString}; 5 | 6 | pub struct LoadedDoc { 7 | pub info: CachedDocInfo, 8 | pub doc: CrateDoc, 9 | } 10 | 11 | pub struct CacheID(pub usize); 12 | 13 | impl LineState for CacheID { 14 | type State = usize; 15 | 16 | fn state(&self) -> Self::State { 17 | self.0 18 | } 19 | 20 | fn is_identical(&self, state: &Self::State) -> bool { 21 | self.0 == *state 22 | } 23 | } 24 | 25 | #[derive(Default, Clone, Copy)] 26 | pub struct Count { 27 | pub loaded: usize, 28 | pub unloaded: usize, 29 | pub in_progress: usize, 30 | } 31 | 32 | impl Count { 33 | pub fn describe(self) -> XString { 34 | use std::fmt::Write; 35 | let Count { 36 | loaded, 37 | unloaded, 38 | in_progress, 39 | } = self; 40 | let mut text = XString::const_new(" "); 41 | if loaded != 0 { 42 | write!(&mut text, "Loaded: {loaded} / ").unwrap(); 43 | } 44 | if unloaded != 0 { 45 | write!(&mut text, "Cached: {unloaded} / ").unwrap(); 46 | } 47 | if in_progress != 0 { 48 | write!(&mut text, "HoldOn: {in_progress} / ").unwrap(); 49 | } 50 | let total = loaded + unloaded + in_progress; 51 | if total != 0 { 52 | write!(&mut text, "Total: {total} ").unwrap(); 53 | } 54 | text 55 | } 56 | } 57 | 58 | /// Sort kind for pkg list in database panel. 59 | #[derive(Clone, Copy, Debug, Default)] 60 | pub enum SortKind { 61 | #[default] 62 | TimeForAll, 63 | PkgKeyForAll, 64 | TimeGrouped, 65 | PkgKeyGrouped, 66 | } 67 | 68 | impl SortKind { 69 | pub fn cmp_fn(self) -> fn(&Cache, &Cache) -> Ordering { 70 | match self { 71 | SortKind::TimeForAll => Cache::cmp_by_time_for_all, 72 | SortKind::PkgKeyForAll => Cache::cmp_by_pkg_key_for_all, 73 | SortKind::TimeGrouped => Cache::cmp_by_time_grouped, 74 | SortKind::PkgKeyGrouped => Cache::cmp_by_pkg_key_grouped, 75 | } 76 | } 77 | 78 | pub fn next(self) -> Self { 79 | match self { 80 | SortKind::TimeForAll => SortKind::PkgKeyForAll, 81 | SortKind::PkgKeyForAll => SortKind::TimeGrouped, 82 | SortKind::TimeGrouped => SortKind::PkgKeyGrouped, 83 | SortKind::PkgKeyGrouped => SortKind::TimeForAll, 84 | } 85 | } 86 | 87 | pub fn describe(self) -> &'static str { 88 | match self { 89 | SortKind::TimeForAll => " [For All] Sort by time ", 90 | SortKind::PkgKeyForAll => " [For All] Sort by PkgKey ", 91 | SortKind::TimeGrouped => " [In Groups] Sort by time ", 92 | SortKind::PkgKeyGrouped => " [In Groups] Sort by PkgKey ", 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/bin/dashboard/ui/search.rs: -------------------------------------------------------------------------------- 1 | use crate::ui::Surround; 2 | use ratatui::prelude::{Buffer, Color, Rect, Style}; 3 | 4 | #[derive(Default)] 5 | pub(super) struct Search { 6 | pub input: String, 7 | area: Rect, 8 | source: Source, 9 | border: Surround, 10 | } 11 | 12 | #[derive(Clone, Copy, Default, Debug)] 13 | enum Source { 14 | #[default] 15 | LocalRegistry, 16 | DataBase, 17 | Both, 18 | } 19 | 20 | impl Search { 21 | pub fn set_area(&mut self, border: Surround) { 22 | self.area = border.inner(); 23 | self.border = border; 24 | } 25 | 26 | fn render_border(&self, buf: &mut Buffer) { 27 | self.border.render(buf); 28 | // render border title 29 | let text = match self.source { 30 | Source::LocalRegistry => " Search Package In Local Registry ", 31 | Source::DataBase => " Search Package In Database ", 32 | Source::Both => " Search Package In Both Local Registry And Database ", 33 | }; 34 | self.border.render_only_top_left_text(buf, text, 0); 35 | } 36 | 37 | pub fn render(&self, buf: &mut Buffer) { 38 | self.render_border(buf); 39 | 40 | let Rect { x, y, width, .. } = self.area; 41 | let width = width.saturating_sub(1) as usize; 42 | let mut text = self.input.as_str(); 43 | // show end half if the input exceeds the width 44 | text = &text[text.len().saturating_sub(width)..]; 45 | let (x, _) = buf.set_stringn(x, y, text, width, Style::new()); 46 | 47 | // the last width is used as cursor 48 | let cursor = Style { 49 | bg: Some(Color::Green), 50 | ..Default::default() 51 | }; 52 | buf.set_stringn(x, y, " ", 1, cursor); 53 | } 54 | } 55 | 56 | /// Search related 57 | impl super::UI { 58 | pub fn push_char(&mut self, ch: char) { 59 | self.search.input.push(ch); 60 | self.update_search(); 61 | } 62 | 63 | // update fuzzy matcher 64 | fn update_search(&mut self) { 65 | match self.search.source { 66 | Source::LocalRegistry => self.registry.update_search(&self.search.input), 67 | Source::DataBase => self.database.update_search(&self.search.input), 68 | Source::Both => { 69 | self.registry.update_search(&self.search.input); 70 | self.database.update_search(&self.search.input); 71 | } 72 | }; 73 | } 74 | 75 | pub fn pop_char(&mut self) { 76 | self.search.input.pop(); 77 | // update fuzzy matcher 78 | self.update_search(); 79 | } 80 | 81 | pub fn clear_input(&mut self) { 82 | self.search.input.clear(); 83 | self.registry.clear_and_reset(); 84 | 85 | match self.search.source { 86 | Source::LocalRegistry => self.registry.clear_and_reset(), 87 | Source::DataBase => self.database.clear_and_reset(), 88 | Source::Both => { 89 | self.registry.clear_and_reset(); 90 | self.database.clear_and_reset(); 91 | } 92 | }; 93 | } 94 | 95 | pub fn switch_search_source(&mut self) { 96 | self.search.source = match self.search.source { 97 | Source::LocalRegistry => Source::DataBase, 98 | Source::DataBase => Source::Both, 99 | Source::Both => Source::LocalRegistry, 100 | }; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/bin/dashboard/ui/ver_feat_toml.rs: -------------------------------------------------------------------------------- 1 | //! A block that shows name, version and features of a selected pkg in toml. 2 | //! 3 | //! Note: if the line is too long, you should move the cursor to see exceeding texts. 4 | 5 | use crate::{ 6 | color::{BG_CURSOR, PKG_TOML}, 7 | database::Features, 8 | ui::{render_line, Surround}, 9 | }; 10 | use ratatui::{ 11 | prelude::{Alignment, Buffer, Color, Constraint, Layout, Line, Rect}, 12 | widgets::{Block, Borders}, 13 | }; 14 | use std::fmt::Write; 15 | use unicode_width::UnicodeWidthStr; 16 | 17 | #[derive(Default)] 18 | pub struct PkgToml { 19 | toml: String, 20 | /// The width on toml string. 21 | toml_width: u16, 22 | inner: Rect, 23 | border: Surround, 24 | } 25 | 26 | pub fn surround(area: Rect) -> Surround { 27 | Surround::new( 28 | Block::new() 29 | .title_bottom(Line::from(" Selected Pkg ").alignment(Alignment::Right)) 30 | .borders(Borders::ALL), 31 | area, 32 | ) 33 | } 34 | 35 | /// Returns [Remainings, PkgToml] areas in vertical split. 36 | pub fn split_for_pkg_toml(area: Rect) -> [Rect; 2] { 37 | Layout::vertical([Constraint::Min(0), Constraint::Length(3)]).areas(area) 38 | } 39 | 40 | impl PkgToml { 41 | pub fn update_toml(&mut self, name: &str, ver: &str, features: &Features) { 42 | self.toml.clear(); 43 | let buf = &mut self.toml; 44 | let _ = match features { 45 | Features::Default => write!(buf, "{name} = {ver:?}"), 46 | // TODO: currently no way to generate All variant, but if we want to support it, 47 | // we must check the pkg's all features, because no all-features field here. 48 | Features::All => write!(buf, "{name} = {{ version = {ver:?} }}"), 49 | Features::DefaultPlus(feats) => { 50 | write!( 51 | buf, 52 | "{name} = {{ version = {ver:?}, features = {feats:?} }}" 53 | ) 54 | } 55 | Features::NoDefault => { 56 | write!( 57 | buf, 58 | "{name} = {{ version = {ver:?}, default-features = false }}" 59 | ) 60 | } 61 | Features::NoDefaultPlus(feats) => { 62 | write!(buf,"{name} = {{ version = {ver:?}, features = {feats:?}, default-features = false }}") 63 | } 64 | }; 65 | self.toml_width = self.toml.width() as u16; 66 | } 67 | 68 | pub fn set_area(&mut self, border: Surround) { 69 | self.inner = border.inner(); 70 | self.border = border; 71 | } 72 | 73 | pub fn update_area(&mut self, area: Rect) { 74 | if let Some(inner) = self.border.update_area(area) { 75 | self.inner = inner; 76 | } 77 | } 78 | 79 | pub fn render(&self, buf: &mut Buffer) { 80 | self.border.render(buf); 81 | 82 | let Rect { x, y, width, .. } = self.inner; 83 | render_line(Some((&*self.toml, PKG_TOML)), buf, x, y, width as usize); 84 | 85 | if self.toml_width > width { 86 | let cell = &mut buf[(width.saturating_sub(1) + x, y)]; 87 | cell.bg = BG_CURSOR; 88 | cell.fg = Color::White; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/bin/database/features/mod.rs: -------------------------------------------------------------------------------- 1 | mod parse_cargo_toml; 2 | mod ui; 3 | 4 | pub use self::ui::FeaturesUI; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use term_rustdoc::util::XString; 8 | 9 | #[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)] 10 | #[allow(dead_code)] 11 | pub enum Features { 12 | #[default] 13 | Default, 14 | All, 15 | DefaultPlus(Box<[XString]>), 16 | NoDefault, 17 | NoDefaultPlus(Box<[XString]>), 18 | } 19 | -------------------------------------------------------------------------------- /src/bin/database/meta.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::time::{Duration, SystemTime}; 3 | use term_rustdoc::util::XString; 4 | 5 | #[derive(Debug, Deserialize, Serialize)] 6 | pub(super) struct DocMeta { 7 | /// the rustc/rustdoc/cargo version compiling the doc, gotten by `cargo +nightly -Vv` 8 | /// NOTE: only nightly toolchain is supported for now 9 | cargo_version: String, 10 | /// the host field from `rustc_version` 11 | host_triple: XString, 12 | /// TODO: the target platform. we haven't supported this other than host triple, 13 | /// so usually this equals to host_triple. 14 | target_triple: XString, 15 | // /// For now, each doc is generated on local machine. 16 | // /// TODO: 17 | // /// But for the future, we can support save and load docs non-locally generated. 18 | // /// For example, crates.io or docs.rs or somthing can provide compiled docs, so 19 | // /// we don't need to compile them locally. Or if you migrate/duplicate docs from 20 | // /// one machine to another machine. 21 | // is_local: bool, 22 | /// the time when the doc starts to compile 23 | started: SystemTime, 24 | /// the time when the doc takes to be compiled and generated 25 | duration: Duration, 26 | } 27 | 28 | impl Default for DocMeta { 29 | fn default() -> Self { 30 | let started = SystemTime::now(); 31 | let (cargo_version, host_triple, target_triple, duration) = Default::default(); 32 | DocMeta { 33 | cargo_version, 34 | host_triple, 35 | target_triple, 36 | started, 37 | duration, 38 | } 39 | } 40 | } 41 | 42 | impl DocMeta { 43 | pub fn new() -> Self { 44 | match std::process::Command::new("cargo") 45 | .args(["+nightly", "-Vv"]) 46 | .output() 47 | { 48 | Ok(output) => { 49 | if output.status.success() { 50 | let started = SystemTime::now(); 51 | let cargo_version = String::from_utf8_lossy(&output.stdout).into_owned(); 52 | let host_triple = cargo_version 53 | .lines() 54 | .find_map(|line| { 55 | if line.starts_with("host: ") { 56 | line.get(6..).map(XString::from) 57 | } else { 58 | None 59 | } 60 | }) 61 | .unwrap_or_default(); 62 | let target_triple = host_triple.clone(); 63 | return DocMeta { 64 | cargo_version, 65 | host_triple, 66 | target_triple, 67 | started, 68 | duration: Duration::default(), 69 | }; 70 | } 71 | let err = String::from_utf8_lossy(&output.stderr); 72 | error!("Failed to run `cargo +nightly -Vv` to get version and host_triple:\n{err}"); 73 | } 74 | Err(err) => { 75 | error!("Failed to run `cargo +nightly -Vv` to get version and host_triple:\n{err}") 76 | } 77 | } 78 | DocMeta::default() 79 | } 80 | 81 | pub fn set_finished_duration(&mut self) { 82 | self.duration = self.started.elapsed().unwrap_or_default(); 83 | } 84 | 85 | pub fn duration_as_secs(&self) -> f32 { 86 | self.duration.as_secs_f32() 87 | } 88 | 89 | pub fn started_time(&self) -> SystemTime { 90 | self.started 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/bin/database/mod.rs: -------------------------------------------------------------------------------- 1 | mod cache_info; 2 | mod features; 3 | mod meta; 4 | mod pkg_key; 5 | mod util; 6 | 7 | use self::meta::DocMeta; 8 | use crate::{ 9 | err, 10 | event::{Event, Sender}, 11 | Result, 12 | }; 13 | use color_eyre::eyre::WrapErr; 14 | use std::path::PathBuf; 15 | 16 | pub use self::{ 17 | cache_info::CachedDocInfo, 18 | features::{Features, FeaturesUI}, 19 | pkg_key::PkgKey, 20 | util::PkgWithFeatures, 21 | }; 22 | 23 | #[derive(Default)] 24 | pub struct DataBase { 25 | /// [`dirs::data_local_dir`] + `term-rustdoc` folder 26 | /// 27 | /// `Some` means the folder does exist. 28 | /// 29 | /// `None` means 30 | /// * can't find config_local_dir 31 | /// * or the term-rustdoc folder is checked to be created 32 | dir: Option, 33 | /// When a pkg doc is compiled and written into its db file, use this to send an event to notify UI. 34 | sender: Option, 35 | } 36 | 37 | impl DataBase { 38 | pub fn init(sender: Sender) -> Result { 39 | let dir = crate::logger::data_dir()?; 40 | Ok(DataBase { 41 | dir: Some(dir), 42 | sender: Some(sender), 43 | }) 44 | } 45 | 46 | pub fn compile_doc(&self, pkg: PkgWithFeatures) -> Option { 47 | let Some(parent) = self.dir.clone() else { 48 | error!("data_local_dir/term_rustdoc does not exist"); 49 | return None; 50 | }; 51 | let Some(sender) = self.sender.clone() else { 52 | error!("DataBase doesn't have a sender. This is a bug."); 53 | return None; 54 | }; 55 | Some(util::build(sender, parent, pkg)) 56 | } 57 | 58 | pub fn all_caches(&self) -> Result> { 59 | use redb::ReadableTable; 60 | let dir = self 61 | .dir 62 | .as_deref() 63 | .ok_or_else(|| err!("Can't fetch all caches because the dir path is not set up"))?; 64 | let db = redb::Database::create(dir.join("index.db")) 65 | .wrap_err_with(|| "Can't create index.db")?; 66 | let table = redb::TableDefinition::::new("CachedDocInfo"); 67 | let read_txn = db.begin_read()?; 68 | let read_only_table = match read_txn.open_table(table) { 69 | Ok(tab) => tab, 70 | Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()), 71 | err => err.wrap_err_with(|| "Can't read CachedDocInfo table from index.db")?, 72 | }; 73 | let info: Vec = read_only_table 74 | .iter()? 75 | .filter_map(|res| match res { 76 | Ok((_, v)) => Some(v.value()), 77 | Err(err) => { 78 | error!("Failed to read a key-value pair in index.db:\n{err}"); 79 | None 80 | } 81 | }) 82 | .collect(); 83 | info!("Succeefully read {} CachedDocInfo", info.len()); 84 | Ok(info) 85 | } 86 | 87 | pub fn send_doc(&self, key: Box) -> Result<()> { 88 | if let Some(sender) = &self.sender { 89 | Ok(sender.send(Event::CrateDoc(key))?) 90 | } else { 91 | Err(err!( 92 | "DataBase doesn't have a sender to send loaded CrateDoc for {key:?}. This is a bug." 93 | )) 94 | } 95 | } 96 | 97 | pub fn send_downgraded_doc(&self, key: Box) { 98 | if let Some(sender) = &self.sender { 99 | if let Err(err) = sender.send(Event::Downgraded(key)) { 100 | error!("Fail to send the downgraded pkg_key:\n{err}"); 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/bin/database/pkg_key.rs: -------------------------------------------------------------------------------- 1 | use super::features::Features; 2 | use crate::local_registry::PkgNameVersion; 3 | use semver::Version; 4 | use serde::{Deserialize, Serialize}; 5 | use std::fmt; 6 | 7 | /// The key in doc db file. 8 | /// 9 | /// NOTE: the reason why PkgKey doesn't implement PartialOrd and Ord is 10 | /// we can't directly compare the version string, and the parsed Version 11 | /// should be stored outside this struct. 12 | #[derive(Deserialize, Serialize, PartialEq, Eq, Clone)] 13 | pub struct PkgKey { 14 | name_ver: PkgNameVersion, 15 | /// features enabled/used when the doc is compiled 16 | /// TODO: for now, we haven't supported feature selection. 17 | features: Features, 18 | } 19 | 20 | impl fmt::Debug for PkgKey { 21 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 22 | let [name, ver] = self.name_ver.name_ver(); 23 | let features = &self.features; 24 | if matches!(features, Features::Default) { 25 | write!(f, "{name}_v{ver}") 26 | } else { 27 | write!(f, "{name}_v{ver} [{features:?}]") 28 | } 29 | } 30 | } 31 | 32 | impl PkgKey { 33 | pub fn new_with_default_feature(name_ver: PkgNameVersion) -> PkgKey { 34 | PkgKey { 35 | name_ver, 36 | features: Features::Default, 37 | } 38 | } 39 | 40 | pub fn new(name_ver: PkgNameVersion, features: Features) -> PkgKey { 41 | PkgKey { name_ver, features } 42 | } 43 | 44 | pub fn name(&self) -> &str { 45 | self.name_ver.name() 46 | } 47 | 48 | pub fn ver_str(&self) -> &str { 49 | self.name_ver.ver_str() 50 | } 51 | 52 | /// Parse the version. When the version can't be parsed, this will return a `0.0.0` version. 53 | pub fn version(&self) -> Version { 54 | self.ver_str() 55 | .parse() 56 | .map_err(|err| error!("Failed to parse the version in {self:?}:\n{err}")) 57 | .unwrap_or(Version::new(0, 0, 0)) 58 | } 59 | 60 | pub fn features(&self) -> &Features { 61 | &self.features 62 | } 63 | 64 | pub fn empty_state() -> PkgKey { 65 | PkgKey { 66 | name_ver: PkgNameVersion::empty_state(), 67 | features: Features::Default, 68 | } 69 | } 70 | } 71 | 72 | impl redb::Value for PkgKey { 73 | type SelfType<'a> = PkgKey; 74 | 75 | type AsBytes<'a> = Vec; 76 | 77 | fn fixed_width() -> Option { 78 | None 79 | } 80 | 81 | fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> 82 | where 83 | Self: 'a, 84 | { 85 | super::util::decode(data).unwrap() 86 | } 87 | 88 | fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> 89 | where 90 | Self: 'a, 91 | Self: 'b, 92 | { 93 | super::util::encode(value).unwrap() 94 | } 95 | 96 | fn type_name() -> redb::TypeName { 97 | redb::TypeName::new("PkgKey") 98 | } 99 | } 100 | 101 | impl redb::Key for PkgKey { 102 | fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { 103 | data1.cmp(data2) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/bin/database/util.rs: -------------------------------------------------------------------------------- 1 | use super::{features::Features, PkgKey}; 2 | use crate::{ 3 | database::CachedDocInfo, 4 | event::{Event, Sender}, 5 | local_registry::PkgInfo, 6 | Result, 7 | }; 8 | use bincode::config; 9 | use bytesize::ByteSize; 10 | use serde::{de::DeserializeOwned, Serialize}; 11 | use std::{io::Write, path::PathBuf}; 12 | use xz2::write::{XzDecoder, XzEncoder}; 13 | 14 | /// Pkg info and local dir that are used to build the doc. 15 | #[derive(Clone)] 16 | pub struct PkgWithFeatures { 17 | pub features: Features, 18 | pub info: PkgInfo, 19 | } 20 | 21 | pub fn build(sender: Sender, db_dir: PathBuf, pkg: PkgWithFeatures) -> PkgKey { 22 | let in_progress = PkgKey::new(pkg.info.to_name_ver(), pkg.features.clone()); 23 | rayon::spawn(move || { 24 | let cargo_toml = pkg.info.path().join("Cargo.toml"); 25 | let dir = match tempfile::tempdir() { 26 | Ok(dir) => dir, 27 | Err(err) => { 28 | error!("Can't create a tempdir:\n{err}"); 29 | return; 30 | } 31 | }; 32 | let mut cache_info = 33 | CachedDocInfo::new(pkg.info.to_name_ver(), pkg.features.clone(), db_dir); 34 | info!(?cache_info.pkg, "begin to compile the doc under {}", dir.path().display()); 35 | let compile = rustdoc_json::Builder::default() 36 | .toolchain("nightly") 37 | .silent(true) 38 | .target_dir(&dir) 39 | .manifest_path(&cargo_toml); 40 | let built = match pkg.features { 41 | Features::Default => compile, 42 | Features::All => compile.all_features(true), 43 | Features::DefaultPlus(f) => compile.features(f.iter()), 44 | Features::NoDefault => compile.no_default_features(true), 45 | Features::NoDefaultPlus(f) => compile.no_default_features(true).features(f.iter()), 46 | } 47 | .build(); 48 | match built { 49 | Ok(json_path) => { 50 | let meta = cache_info.meta_mut(); 51 | meta.set_finished_duration(); 52 | let duration = meta.duration_as_secs(); 53 | info!(?cache_info.pkg, ?json_path, "succeefully compiled the doc in {duration:.2}s"); 54 | if let Err(err) = cache_info.save_doc(&json_path, pkg.info) { 55 | error!("{err}"); 56 | } 57 | match sender.send(Event::DocCompiled(Box::new(cache_info))) { 58 | Ok(()) => (), 59 | Err(err) => { 60 | error!( 61 | "Failed to send `DocCompiled` event when CachedDocInfo is ready:\n{err}" 62 | ) 63 | } 64 | } 65 | } 66 | Err(err) => error!("Failed to compile {}:\n{err}", cargo_toml.display()), 67 | } 68 | }); 69 | in_progress 70 | } 71 | 72 | /// Write source data into db file. 73 | pub fn encode(t: T) -> Result> { 74 | Ok(bincode::serde::encode_to_vec(t, config::standard())?) 75 | } 76 | 77 | /// Read data from db file. 78 | pub fn decode(raw: &[u8]) -> Result { 79 | Ok(bincode::serde::decode_from_slice(raw, config::standard())?.0) 80 | } 81 | 82 | /// Write source data into db file with xz compression. 83 | /// 84 | /// NOTE: not all bytes are compressed via xz, because compression on small bytes 85 | /// will increase the size. 86 | /// For now, only compress the doc data, i.e. host-json and host-parsed table. 87 | /// Be careful to decompress them before deserialization. 88 | pub fn encode_with_xz(t: T) -> Result> { 89 | let raw = bincode::serde::encode_to_vec(t, config::standard())?; 90 | xz_encode_on_bytes(&raw) 91 | } 92 | 93 | pub fn xz_encode_on_bytes(raw: &[u8]) -> Result> { 94 | let mut compressed = Vec::with_capacity(raw.len() / 2); 95 | let mut xz_encoder = XzEncoder::new(&mut compressed, 9); 96 | xz_encoder.write_all(raw)?; 97 | xz_encoder.finish()?; 98 | compressed.shrink_to_fit(); 99 | let (before, after) = (raw.len(), compressed.len()); 100 | info!( 101 | "compress {} => {} (reduced {:.1}%)", 102 | ByteSize(before as u64), 103 | ByteSize(after as u64), 104 | (after as f32 / before as f32 - 1.0) * 100.0 105 | ); 106 | Ok(compressed) 107 | } 108 | 109 | /// Read data from db file with xz decompression. 110 | pub fn decode_with_xz(raw: &[u8]) -> Result { 111 | let decompressed = xz_decode_on_bytes(raw)?; 112 | Ok(bincode::serde::decode_from_slice(&decompressed, config::standard())?.0) 113 | } 114 | 115 | pub fn xz_decode_on_bytes(raw: &[u8]) -> Result> { 116 | let mut decompressed = Vec::with_capacity(raw.len() * 4); 117 | { 118 | let mut xz_decoder = XzDecoder::new(&mut decompressed); 119 | xz_decoder.write_all(raw)?; 120 | xz_decoder.finish()?; 121 | } 122 | let (before, after) = (raw.len(), decompressed.len()); 123 | info!( 124 | "decompress {} => {} (ratio {:.1}%)", 125 | ByteSize(before as u64), 126 | ByteSize(after as u64), 127 | (1.0 - before as f32 / after as f32) * 100.0 128 | ); 129 | Ok(decompressed) 130 | } 131 | -------------------------------------------------------------------------------- /src/bin/event/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | database::{CachedDocInfo, PkgKey}, 3 | Result, 4 | }; 5 | use crossterm::event::{ 6 | self, Event as CrosstermEvent, KeyEvent, MouseButton, MouseEvent, MouseEventKind, 7 | }; 8 | use std::{ 9 | sync::mpsc, 10 | thread, 11 | time::{Duration, Instant}, 12 | }; 13 | 14 | /// Terminal events. 15 | #[derive(Debug)] 16 | pub enum Event { 17 | /// Key press. 18 | Key(KeyEvent), 19 | /// Mouse click/scroll. 20 | Mouse(MouseEvent), 21 | /// Left double click in (x, y). 22 | MouseDoubleClick(u16, u16), 23 | /// Terminal resize. 24 | Resize(u16, u16), 25 | /// Pkg doc that's compiled and written into its db file. 26 | DocCompiled(Box), 27 | /// Compiled and loaded doc for Page. 28 | CrateDoc(Box), 29 | /// Downgraded doc which may or may not be the current one. 30 | Downgraded(Box), 31 | } 32 | 33 | pub type Sender = mpsc::Sender; 34 | 35 | /// Terminal event handler. 36 | #[derive(Debug)] 37 | pub struct EventHandler { 38 | /// Event sender channel. 39 | sender: mpsc::Sender, 40 | /// Event receiver channel. 41 | receiver: mpsc::Receiver, 42 | 43 | /// Event handler thread. 44 | #[allow(dead_code)] 45 | handler: thread::JoinHandle<()>, 46 | } 47 | 48 | impl EventHandler { 49 | /// Constructs a new instance of [`EventHandler`]. 50 | pub fn new(timeout: u64) -> Self { 51 | let timeout = Duration::from_millis(timeout); 52 | let (sender, receiver) = mpsc::channel(); 53 | let handler = { 54 | let sender = sender.clone(); 55 | thread::spawn(move || { 56 | let mut last_click = Instant::now(); 57 | loop { 58 | if event::poll(timeout).expect("unable to poll for event") { 59 | match event::read().expect("unable to read event") { 60 | CrosstermEvent::Key(e) => { 61 | if e.kind == event::KeyEventKind::Press { 62 | sender.send(Event::Key(e)) 63 | } else { 64 | Ok(()) // ignore KeyEventKind::Release on windows 65 | } 66 | } 67 | CrosstermEvent::Mouse(e) => { 68 | // Record the time of left clicking, and emit the MouseDoubleClick event 69 | // when the gap between two clicks is shorter than a duration. 70 | if let MouseEventKind::Down(MouseButton::Left) = &e.kind { 71 | let now = Instant::now(); 72 | let old = std::mem::replace(&mut last_click, now); 73 | if let Some(diff) = now.checked_duration_since(old) { 74 | if diff < Duration::from_millis(450) { 75 | sender 76 | .send(Event::MouseDoubleClick(e.column, e.row)) 77 | .expect("failed to send MouseDoubleClick event"); 78 | continue; // no need to emit Mouse click event 79 | } 80 | } 81 | } 82 | sender.send(Event::Mouse(e)) 83 | } 84 | CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)), 85 | _ => Ok(()), 86 | } 87 | .expect("failed to send terminal event") 88 | } 89 | } 90 | }) 91 | }; 92 | Self { 93 | sender, 94 | receiver, 95 | handler, 96 | } 97 | } 98 | 99 | /// Receive the next event from the handler thread. 100 | /// 101 | /// This function will always block the current thread if 102 | /// there is no data available and it's possible for more data to be sent. 103 | pub fn next(&self) -> Result { 104 | Ok(self.receiver.recv()?) 105 | } 106 | 107 | pub fn get_sender(&self) -> Sender { 108 | self.sender.clone() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/bin/frame/help.rs: -------------------------------------------------------------------------------- 1 | use crate::ui::{MarkdownAndHeading, ScrollMarkdown, Surround}; 2 | use ratatui::{ 3 | layout::Alignment, 4 | prelude::{Buffer, Rect}, 5 | text::Line, 6 | widgets::{Block, Borders}, 7 | }; 8 | 9 | use super::centered_rect; 10 | 11 | pub struct Help { 12 | md: HelpMarkdown, 13 | /// full screen area 14 | full: Rect, 15 | } 16 | 17 | impl Help { 18 | pub fn new(full: Rect) -> Help { 19 | let outer = split_surround(full); 20 | Help { 21 | md: HelpMarkdown::new(outer), 22 | full, 23 | } 24 | } 25 | 26 | pub fn update_area(&mut self, full: Rect) { 27 | if self.full == full { 28 | return; 29 | } 30 | self.full = full; 31 | let outer = split_surround(full); 32 | self.md = HelpMarkdown::new(outer); 33 | } 34 | 35 | pub fn render(&self, buf: &mut Buffer) { 36 | self.md.border.render(buf); 37 | self.md.inner.render(buf); 38 | } 39 | 40 | pub fn scroll_text(&mut self) -> &mut ScrollMarkdown { 41 | self.md.inner.scroll_text() 42 | } 43 | 44 | pub fn heading_jump(&mut self, position: (u16, u16)) -> bool { 45 | // position: (x, y) 46 | if self.md.inner.heading().area.contains(position.into()) { 47 | return self.md.inner.heading_jump(position.1); 48 | } 49 | false 50 | } 51 | 52 | pub fn contains(&self, position: (u16, u16)) -> bool { 53 | self.md.border.area().contains(position.into()) 54 | } 55 | } 56 | 57 | fn split_surround(full: Rect) -> Surround { 58 | let outer = centered_rect(full, 80, 80); 59 | let title = Line::from(" Press F1 to toggle this Help ").alignment(Alignment::Right); 60 | Surround::new( 61 | Block::new() 62 | .title(" Help ") 63 | .title_bottom(title) 64 | .borders(Borders::ALL), 65 | outer, 66 | ) 67 | } 68 | 69 | struct HelpMarkdown { 70 | inner: MarkdownAndHeading, 71 | border: Surround, 72 | } 73 | 74 | impl HelpMarkdown { 75 | fn new(border: Surround) -> Self { 76 | let inner = MarkdownAndHeading::new(Self::HELP, border.inner()); 77 | HelpMarkdown { inner, border } 78 | } 79 | 80 | const HELP: &'static str = include_str!("help.md"); 81 | } 82 | -------------------------------------------------------------------------------- /src/bin/frame/mod.rs: -------------------------------------------------------------------------------- 1 | mod help; 2 | mod update; 3 | mod util; 4 | 5 | pub use self::util::centered_rect; 6 | 7 | use self::help::Help; 8 | use crate::{dashboard::DashBoard, page::Page}; 9 | use ratatui::prelude::{Buffer, Rect, Widget}; 10 | 11 | pub struct Frame { 12 | dash_board: DashBoard, 13 | page: Page, 14 | focus: Focus, 15 | /// Initialize this when needed the first time. 16 | help: Option>, 17 | pub should_quit: bool, 18 | } 19 | 20 | #[derive(Default, Debug, Clone, Copy)] 21 | enum Focus { 22 | #[default] 23 | DashBoard, 24 | Page, 25 | Help, 26 | } 27 | 28 | impl Frame { 29 | pub fn new(dash_board: DashBoard) -> Frame { 30 | let (page, focus, help, should_quit) = Default::default(); 31 | Frame { 32 | dash_board, 33 | page, 34 | focus, 35 | help, 36 | should_quit, 37 | } 38 | } 39 | 40 | fn switch_to_page(&mut self) { 41 | self.focus = Focus::Page; 42 | } 43 | 44 | fn switch_focus(&mut self) { 45 | let before = self.focus; 46 | self.focus = match self.focus { 47 | Focus::DashBoard | Focus::Help if !self.page.is_empty() => Focus::Page, 48 | _ => Focus::DashBoard, 49 | }; 50 | info!("Frame: swicth from {before:?} to {:?}", self.focus); 51 | } 52 | 53 | fn get_help(&mut self) -> &mut Help { 54 | self.focus = Focus::Help; 55 | self.help.get_or_insert_with(|| { 56 | let full = self.dash_board.ui().get_full_area(); 57 | let help = Help::new(full); 58 | info!("Initialized Help"); 59 | Box::new(help) 60 | }) 61 | } 62 | 63 | fn quit(&mut self) { 64 | self.should_quit = true; 65 | } 66 | } 67 | 68 | impl Widget for &mut Frame { 69 | /// entry point for all rendering 70 | fn render(self, full: Rect, buf: &mut Buffer) { 71 | match self.focus { 72 | Focus::DashBoard => self.dash_board.ui().render(full, buf), 73 | Focus::Page => self.page.render(full, buf), 74 | Focus::Help => { 75 | let help = self.get_help(); 76 | help.update_area(full); 77 | help.render(buf); 78 | } 79 | }; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/bin/frame/util.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::Flex, 3 | prelude::{Constraint, Layout, Rect}, 4 | }; 5 | 6 | pub fn centered_rect(area: Rect, width: u16, height: u16) -> Rect { 7 | let horizontal = Layout::horizontal([Constraint::Percentage(width)]).flex(Flex::Center); 8 | let vertical = Layout::vertical([Constraint::Percentage(height)]).flex(Flex::Center); 9 | let [area] = vertical.areas(area); 10 | let [area] = horizontal.areas(area); 11 | area 12 | } 13 | -------------------------------------------------------------------------------- /src/bin/fuzzy.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use nucleo_matcher::{ 3 | pattern::{Atom, CaseMatching, Normalization}, 4 | *, 5 | }; 6 | use std::{cell::RefCell, rc::Rc}; 7 | 8 | /// A shared fuzzy matcher that only stores the source text for computing its score 9 | /// and is used as Atom pattern. 10 | /// 11 | /// This is cheap to clone, and appropriate for small set of source texts like a list 12 | /// of short texts, lines in a page of documentation etc, in which case ascii conversion 13 | /// for `Utf32Str` is free and less frequently to fuzz. 14 | #[derive(Clone)] 15 | pub struct Fuzzy { 16 | fuzzy: Rc>, 17 | } 18 | 19 | impl Fuzzy { 20 | #[allow(clippy::new_without_default)] 21 | pub fn new() -> Self { 22 | Fuzzy { 23 | fuzzy: Rc::new(RefCell::new(FuzzyInner::new())), 24 | } 25 | } 26 | 27 | fn fuzzy(&self, f: impl FnOnce(&mut FuzzyInner) -> T) -> Option { 28 | self.fuzzy.try_borrow_mut().ok().as_deref_mut().map(f) 29 | } 30 | 31 | pub fn parse(&self, pattern: &str) { 32 | self.fuzzy(|f| f.parse(pattern)); 33 | } 34 | 35 | pub fn score(&self, text: &str) -> Option { 36 | self.fuzzy(|f| f.score(text)).flatten() 37 | } 38 | 39 | pub fn match_list, U: From>( 40 | &self, 41 | texts: impl IntoIterator, 42 | buf: &mut Vec, 43 | ) { 44 | self.fuzzy(|f| f.match_list(texts, buf)); 45 | } 46 | } 47 | 48 | struct FuzzyInner { 49 | /// non-ascii string buffer for `Utf32Str` 50 | buf: Vec, 51 | pat: Atom, 52 | matcher: Matcher, 53 | } 54 | 55 | impl FuzzyInner { 56 | fn new() -> Self { 57 | FuzzyInner { 58 | buf: Vec::new(), 59 | pat: Atom::parse("", CaseMatching::Smart, Normalization::Smart), 60 | matcher: Matcher::new(Config::DEFAULT), 61 | } 62 | } 63 | 64 | fn parse(&mut self, pattern: &str) { 65 | self.pat = Atom::parse(pattern, CaseMatching::Smart, Normalization::Smart); 66 | } 67 | 68 | fn score(&mut self, source_text: &str) -> Option { 69 | let text = Utf32Str::new(source_text, &mut self.buf); 70 | self.pat.score(text, &mut self.matcher) 71 | } 72 | 73 | fn match_list, U: From>( 74 | &mut self, 75 | texts: impl IntoIterator, 76 | buf: &mut Vec, 77 | ) { 78 | let output = texts 79 | .into_iter() 80 | .filter_map(|t| self.score(t.as_ref()).filter(|x| *x > 0).map(|x| (x, t))) 81 | .sorted_unstable_by_key(|val| std::cmp::Reverse(val.0)); 82 | buf.clear(); 83 | buf.extend(output.map(|(_, t)| t.into())); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/bin/logger.rs: -------------------------------------------------------------------------------- 1 | use crate::{err, Result}; 2 | use std::{ 3 | fs::{self, File}, 4 | path::PathBuf, 5 | }; 6 | use tracing_subscriber::filter::{EnvFilter, LevelFilter}; 7 | 8 | pub fn init() -> Result<()> { 9 | #[cfg(debug_assertions)] 10 | let path = PathBuf::from_iter(["target", "term_rustdoc.log"]); 11 | #[cfg(not(debug_assertions))] 12 | let path = data_dir()?.join("term_rustdoc.log"); 13 | let file = File::create(path)?; 14 | 15 | // RUST_LOG="debug" or RUST_LOG="module_path=debug" environment variable 16 | // https://docs.rs/tracing-subscriber/0.3.18/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax 17 | let env_filter = EnvFilter::builder() 18 | .with_default_directive(LevelFilter::INFO.into()) 19 | .from_env_lossy(); 20 | tracing_subscriber::fmt() 21 | .with_env_filter(env_filter) 22 | .with_writer(file) 23 | // .with_ansi(false) 24 | .try_init() 25 | .map_err(|_| err!("logger init failed: maybe there is another initializer?"))?; 26 | info!("logging initialized and the program starts"); 27 | Ok(()) 28 | } 29 | 30 | pub fn data_dir() -> Result { 31 | let mut dir = dirs::data_local_dir().ok_or_else(|| err!("Can't find the config_local_dir"))?; 32 | dir.push("term-rustdoc"); 33 | if !dir.exists() { 34 | fs::create_dir(&dir)?; 35 | } 36 | Ok(dir) 37 | } 38 | -------------------------------------------------------------------------------- /src/bin/main.rs: -------------------------------------------------------------------------------- 1 | mod color; 2 | mod dashboard; 3 | mod database; 4 | mod event; 5 | mod frame; 6 | mod fuzzy; 7 | mod local_registry; 8 | mod logger; 9 | mod page; 10 | mod tui; 11 | mod ui; 12 | 13 | #[macro_use] 14 | extern crate tracing; 15 | 16 | use self::frame::Frame; 17 | use color_eyre::eyre::{eyre as err, Result, WrapErr}; 18 | 19 | fn main() -> Result<()> { 20 | tui::install_hooks()?; 21 | logger::init()?; 22 | 23 | let mut tui = tui::Tui::new(1000)?; 24 | let fuzz = fuzzy::Fuzzy::new(); 25 | 26 | let full = tui.size()?; 27 | let sender = tui.events.get_sender(); 28 | let dash_board = dashboard::DashBoard::new(full, fuzz, sender)?; 29 | let mut frame = Frame::new(dash_board); 30 | 31 | // Start the main loop. 32 | while !frame.should_quit { 33 | // Render the user interface. 34 | tui.draw(&mut frame)?; 35 | // Handle events. 36 | frame.consume_event(tui.events.next()?); 37 | } 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /src/bin/page/layout.rs: -------------------------------------------------------------------------------- 1 | use super::{navi::navi_outline_width, Page, Panel, Surround}; 2 | use crate::color::SET; 3 | use ratatui::{ 4 | prelude::{Constraint, Direction, Layout, Rect}, 5 | widgets::{Block, BorderType, Borders}, 6 | }; 7 | 8 | impl Page { 9 | fn layout(&self) -> Layout { 10 | let outline_width = self.outline.max_width() + 1; 11 | Layout::default() 12 | .direction(Direction::Horizontal) 13 | .constraints([ 14 | // The width is dynamic, always fits the max_width. 15 | // User will see variable widths of outline and content. 16 | Constraint::Length(outline_width), 17 | Constraint::Min(20), 18 | // Leave the exact space for NaviOutline. 19 | // navi_outline_width is not dynamic. 20 | // If the constraint is flexible by Min(width), 21 | // we'll see variable widths on both side, which is not good UX. 22 | Constraint::Length(navi_outline_width()), 23 | ]) 24 | } 25 | 26 | /// This is called in Widget's render method because inner widgets don't implement 27 | /// Widget, since the areas they draw are updated only from here, not from Widget trait. 28 | pub(super) fn update_area(&mut self, full: Rect) { 29 | // skip updating since the size is the same 30 | if self.area == full { 31 | return; 32 | } 33 | 34 | self.update_area_inner(full); 35 | } 36 | 37 | /// Force update Page inner layout. 38 | /// 39 | /// `full` usually should be the full screen area or Page area. 40 | pub(super) fn update_area_inner(&mut self, full: Rect) { 41 | // layout 42 | self.area = full; 43 | let [a_outline, a_content, a_navi] = self.layout().areas(full); 44 | 45 | // outline 46 | let outline_border = Block::new() 47 | .borders(Borders::RIGHT) 48 | .border_type(BorderType::Thick); 49 | let outline_border = Surround::new( 50 | if matches!(self.current, None | Some(Panel::Outline)) { 51 | outline_border.style(SET) 52 | } else { 53 | outline_border 54 | }, 55 | a_outline, 56 | ); 57 | self.outline.update_area(outline_border); 58 | 59 | // content 60 | let border = Surround::new(Block::new(), a_content); 61 | let id = self 62 | .outline 63 | .inner 64 | .display() 65 | .get_line_of_current_cursor() 66 | .and_then(|t| t.id); 67 | self.content.update_area(border, id); 68 | 69 | // navi 70 | self.navi.update_area(Surround::new( 71 | Block::new() 72 | .borders(Borders::LEFT) 73 | .border_type(BorderType::Thick), 74 | a_navi, 75 | )); 76 | 77 | // auto update content when screen size changes 78 | self.update_content(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/bin/page/mod.rs: -------------------------------------------------------------------------------- 1 | use self::{ 2 | navi::{NaviAction, Navigation}, 3 | panel::Panel, 4 | }; 5 | use crate::{ 6 | database::PkgKey, 7 | ui::{scrollable::ScrollTreeLines, Surround}, 8 | Result, 9 | }; 10 | use ratatui::prelude::{Buffer, Rect, Widget}; 11 | use rustdoc_types::Id; 12 | use term_rustdoc::tree::CrateDoc; 13 | 14 | mod content; 15 | mod layout; 16 | mod navi; 17 | mod outline; 18 | /// fold/expand a tree view 19 | mod page_fold; 20 | /// scroll up/down behavior and with what offset 21 | mod page_scroll; 22 | mod panel; 23 | 24 | #[derive(Default, Debug)] 25 | pub struct Page { 26 | outline: Outline, 27 | content: Content, 28 | navi: Navigation, 29 | current: Option, 30 | pkg_key: Option, 31 | area: Rect, 32 | } 33 | 34 | impl Page { 35 | pub fn new(pkg_key: PkgKey, doc: CrateDoc, area: Rect) -> Result { 36 | let mut page = Page { 37 | outline: Outline::new(&doc, area.height), 38 | content: Content { 39 | inner: content::ContentInner::new(&doc), 40 | ..Default::default() 41 | }, 42 | // page scrolling like HOME/END will check the current Panel 43 | current: Some(Panel::Outline), 44 | area, 45 | pkg_key: Some(pkg_key), 46 | navi: Default::default(), 47 | }; 48 | page.update_area_inner(area); 49 | info!(?area, "Page ready"); 50 | Ok(page) 51 | } 52 | 53 | #[allow(clippy::single_match)] 54 | pub fn double_click(&mut self) { 55 | match self.current { 56 | Some(Panel::Outline) => self.outline_fold_expand_toggle(), 57 | _ => {} 58 | } 59 | } 60 | 61 | pub fn is_empty(&self) -> bool { 62 | self.area.height == 0 || self.area.width == 0 63 | } 64 | 65 | /// Drop the data when PkgKey matches. 66 | pub fn drop(&mut self, pkg_key: &PkgKey) { 67 | if self 68 | .pkg_key 69 | .as_ref() 70 | .map(|key| key == pkg_key) 71 | .unwrap_or(false) 72 | { 73 | *self = Page::default(); 74 | } 75 | } 76 | } 77 | 78 | impl Widget for &mut Page { 79 | fn render(self, area: Rect, buf: &mut Buffer) { 80 | debug!("Page rendering starts"); 81 | self.update_area(area); 82 | self.outline.render(buf); 83 | self.content.border.render(buf); 84 | self.content.inner.render(buf); 85 | self.navi.render(buf, self.content.inner.md_ref()); 86 | debug!("Page rendered"); 87 | } 88 | } 89 | 90 | #[derive(Default, Debug)] 91 | struct Outline { 92 | inner: outline::OutlineInner, 93 | border: Surround, 94 | } 95 | 96 | impl Outline { 97 | fn new(doc: &CrateDoc, height: u16) -> Self { 98 | Outline { 99 | inner: outline::OutlineInner::new(doc, height), 100 | ..Default::default() 101 | } 102 | } 103 | 104 | fn render(&self, buf: &mut Buffer) { 105 | self.border.render(buf); 106 | self.inner.render(buf); 107 | } 108 | 109 | fn action(&mut self, action: NaviAction) { 110 | self.inner.action(action); 111 | } 112 | 113 | fn reset_to_module_tree(&mut self) { 114 | self.inner.reset_to_module_tree(); 115 | } 116 | 117 | fn display(&mut self) -> &mut ScrollTreeLines { 118 | self.inner.display() 119 | } 120 | 121 | fn display_ref(&self) -> &ScrollTreeLines { 122 | self.inner.display_ref() 123 | } 124 | 125 | fn update_area(&mut self, border: Surround) { 126 | self.inner.update_area(border.inner()); 127 | self.border = border; 128 | } 129 | 130 | fn set_setu_id(&mut self, id: Id) { 131 | self.inner.set_setu_id(id); 132 | } 133 | 134 | fn is_module_tree(&self) -> bool { 135 | self.inner.is_module_tree() 136 | } 137 | 138 | fn max_width(&self) -> u16 { 139 | self.display_ref() 140 | .visible_lines() 141 | .and_then(|lines| lines.iter().map(|l| l.width()).max()) 142 | .unwrap_or(0) 143 | } 144 | } 145 | 146 | #[derive(Default, Debug)] 147 | struct Content { 148 | inner: content::ContentInner, 149 | border: Surround, 150 | } 151 | 152 | impl Content { 153 | fn update_area(&mut self, border: Surround, id: Option) { 154 | self.border = border; 155 | let outer = self.border.inner(); 156 | if let Some(id) = id { 157 | self.inner.update_decl(&id, outer); 158 | } else { 159 | self.inner.update_area(outer); 160 | } 161 | } 162 | 163 | fn update_doc(&mut self, id: &Id) -> Option { 164 | self.inner.update_doc(id, self.border.inner()) 165 | } 166 | 167 | fn jumpable_id(&self, x: u16, y: u16) -> Option { 168 | self.inner.jumpable_id(x, y) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/bin/page/navi/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | mod outline; 4 | 5 | use self::outline::NaviOutline; 6 | use crate::ui::{ 7 | scrollable::{ScrollHeading, ScrollText}, 8 | Surround, 9 | }; 10 | use ratatui::{ 11 | layout::Position, 12 | prelude::{Buffer, Constraint, Layout, Rect}, 13 | }; 14 | use rustdoc_types::Id; 15 | use term_rustdoc::tree::CrateDoc; 16 | 17 | pub use self::outline::{width as navi_outline_width, NaviAction}; 18 | 19 | #[derive(Default, Debug)] 20 | pub struct Navigation { 21 | display: Navi, 22 | border: Surround, 23 | } 24 | 25 | impl Navigation { 26 | pub fn heading(&mut self) -> &mut ScrollHeading { 27 | &mut self.display.heading 28 | } 29 | 30 | // position in (x, y) 31 | pub fn contains(&self, position: Position) -> bool { 32 | self.border.area().contains(position) 33 | } 34 | 35 | pub fn border(&mut self) -> &mut Surround { 36 | &mut self.border 37 | } 38 | 39 | pub fn set_item_inner(&mut self, id: Option, doc: &CrateDoc) -> Option { 40 | self.display.outline.set_item_inner(id, doc) 41 | } 42 | 43 | pub fn reset_navi_outline(&mut self) { 44 | self.display.outline.reset(); 45 | } 46 | 47 | pub fn set_outline_cursor_back_to_home(&mut self) { 48 | self.display.outline.set_cursor_back_to_home(); 49 | } 50 | 51 | pub fn update_area(&mut self, border: Surround) { 52 | let inner = border.inner(); 53 | let [heading, outline] = split(inner); 54 | self.display.heading.area = heading; 55 | self.border = border; 56 | self.display.outline.update_area(outline); 57 | } 58 | 59 | pub fn render(&self, buf: &mut Buffer, content: &ScrollText) { 60 | self.border.render(buf); 61 | 62 | let content_start = content.start; 63 | let content_end = content.area.height as usize + content_start; 64 | self.display.heading.render(buf, content_start, content_end); 65 | 66 | self.display.outline.render(buf); 67 | } 68 | 69 | pub fn update_outline(&mut self, y: u16) -> Option { 70 | self.display.outline.update_outline(y) 71 | } 72 | 73 | pub fn next_action(&mut self) -> Option { 74 | self.display.outline.next_action() 75 | } 76 | 77 | pub fn previous_action(&mut self) -> Option { 78 | self.display.outline.previous_action() 79 | } 80 | } 81 | 82 | #[derive(Default)] 83 | struct Navi { 84 | heading: ScrollHeading, 85 | outline: NaviOutline, 86 | } 87 | 88 | impl std::fmt::Debug for Navi { 89 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 90 | write!(f, "Navi {{ ... }}") 91 | } 92 | } 93 | 94 | fn split(area: Rect) -> [Rect; 2] { 95 | // leave the minimum height for NaviOutline 96 | Layout::vertical([ 97 | Constraint::Percentage(70), 98 | Constraint::Min(outline::height()), 99 | ]) 100 | .areas(area) 101 | } 102 | -------------------------------------------------------------------------------- /src/bin/page/outline.rs: -------------------------------------------------------------------------------- 1 | use super::navi::NaviAction; 2 | use crate::ui::scrollable::ScrollTreeLines; 3 | use ratatui::prelude::{Buffer, Rect}; 4 | use rustdoc_types::Id; 5 | use term_rustdoc::tree::{CrateDoc, TreeLines}; 6 | 7 | #[derive(Default)] 8 | pub struct OutlineInner { 9 | kind: OutlineKind, 10 | modules: ScrollTreeLines, 11 | setu: Setu, 12 | } 13 | 14 | impl std::fmt::Debug for OutlineInner { 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | f.debug_struct("OutlineInner") 17 | .field("kind", &self.kind) 18 | .finish() 19 | } 20 | } 21 | 22 | impl OutlineInner { 23 | pub fn new(doc: &CrateDoc, height: u16) -> Self { 24 | let modules = match ScrollTreeLines::new_tree_lines(doc.clone().into(), height) { 25 | Ok(lines) => lines, 26 | Err(err) => { 27 | error!("Failed to init module Outline:\n{err}"); 28 | return OutlineInner::default(); 29 | } 30 | }; 31 | OutlineInner { 32 | modules, 33 | ..Default::default() 34 | } 35 | } 36 | 37 | pub fn is_module_tree(&self) -> bool { 38 | matches!(self.kind, OutlineKind::Modules) 39 | } 40 | 41 | pub fn display(&mut self) -> &mut ScrollTreeLines { 42 | match self.kind { 43 | OutlineKind::Modules => &mut self.modules, 44 | OutlineKind::InnerItem => &mut self.setu.display, 45 | } 46 | } 47 | 48 | pub fn display_ref(&self) -> &ScrollTreeLines { 49 | match self.kind { 50 | OutlineKind::Modules => &self.modules, 51 | OutlineKind::InnerItem => &self.setu.display, 52 | } 53 | } 54 | 55 | pub fn update_area(&mut self, area: Rect) { 56 | self.modules.area = area; 57 | self.setu.update_area(area); 58 | } 59 | 60 | pub fn render(&self, buf: &mut Buffer) { 61 | match self.kind { 62 | OutlineKind::Modules => self.modules.render(buf), 63 | OutlineKind::InnerItem => self.setu.render(buf), 64 | }; 65 | } 66 | } 67 | 68 | /// Action from Navi 69 | impl OutlineInner { 70 | pub fn set_setu_id(&mut self, id: Id) { 71 | self.setu.outer_item = id; 72 | } 73 | 74 | pub fn action(&mut self, action: NaviAction) { 75 | match action { 76 | NaviAction::BackToHome => self.back_to_home(), 77 | x => { 78 | // keep Modules kind if invalid id for outer item 79 | if self.setu.update_lines(&self.modules, x).is_some() { 80 | self.kind = OutlineKind::InnerItem; 81 | } 82 | } 83 | }; 84 | } 85 | 86 | fn back_to_home(&mut self) { 87 | self.kind = OutlineKind::Modules; 88 | } 89 | 90 | pub fn reset_to_module_tree(&mut self) { 91 | self.setu.outer_item = Id(0); 92 | // we don't have to overwrite the real lines because we only check by id 93 | // self.setu.display = Default::default(); 94 | self.back_to_home(); 95 | } 96 | } 97 | 98 | #[derive(Default, Debug, Clone, Copy)] 99 | pub enum OutlineKind { 100 | #[default] 101 | Modules, 102 | InnerItem, 103 | } 104 | 105 | /// Stands for struct/enum/trait/union. 106 | /// 107 | /// This also supports focus on a module, but not very much designed. 108 | pub struct Setu { 109 | outer_item: Id, 110 | display: ScrollTreeLines, 111 | } 112 | 113 | impl Default for Setu { 114 | fn default() -> Self { 115 | Setu { 116 | outer_item: Id(0), 117 | display: Default::default(), 118 | } 119 | } 120 | } 121 | 122 | impl Setu { 123 | pub fn update_area(&mut self, area: Rect) { 124 | self.display.area = area; 125 | } 126 | 127 | pub fn update_lines(&mut self, modules: &ScrollTreeLines, action: NaviAction) -> Option<()> { 128 | let doc = modules.lines.doc_ref(); 129 | // If id is not valid, lines won't be updated. 130 | self.display.lines = TreeLines::try_new_with(doc, |map| { 131 | let id = &self.outer_item; 132 | let dmod = map.dmodule(); 133 | match action { 134 | NaviAction::ITABImpls => dmod.impl_tree(id, map), 135 | NaviAction::Item => dmod.item_inner_tree(id, map), 136 | NaviAction::TraitAssociated => dmod.associated_item_tree(id, map), 137 | NaviAction::TraitImplementors => dmod.implementor_tree(id, map), 138 | NaviAction::StructInner | NaviAction::EnumInner => dmod.field_tree(id, map), 139 | _ => dmod.item_inner_tree(id, map), 140 | } 141 | })?; 142 | if self.display.total_len() == 0 { 143 | let path = modules.lines.doc_ref().path(&self.outer_item); 144 | error!("{path} generated unexpected empty TreeLines"); 145 | } 146 | // self.update_area(modules.area); 147 | self.display.start = 0; 148 | self.display.cursor.y = 0; 149 | Some(()) 150 | } 151 | 152 | pub fn render(&self, buf: &mut Buffer) { 153 | if self.display.lines.is_empty() { 154 | return; 155 | } 156 | self.display.render(buf); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/bin/page/page_fold.rs: -------------------------------------------------------------------------------- 1 | use super::Page; 2 | 3 | /// For now, the expand/fold behavior only works for Module tree. 4 | impl Page { 5 | pub fn outline_fold_expand_all(&mut self) { 6 | if !self.outline.is_module_tree() { 7 | return; 8 | } 9 | self.outline().lines.expand_all(); 10 | self.update_after_folding_outline(); 11 | } 12 | 13 | pub fn outline_fold_expand_current_module_only(&mut self) { 14 | if !self.outline.is_module_tree() { 15 | return; 16 | } 17 | if let Some(id) = self.outline().get_id() { 18 | self.outline().lines.expand_current_module_only(id); 19 | self.update_after_folding_outline(); 20 | } 21 | } 22 | 23 | pub fn outline_fold_expand_zero_level(&mut self) { 24 | if !self.outline.is_module_tree() { 25 | return; 26 | } 27 | self.outline().lines.expand_zero_level(); 28 | self.update_after_folding_outline(); 29 | } 30 | 31 | pub fn outline_fold_expand_to_first_level_modules(&mut self) { 32 | if !self.outline.is_module_tree() { 33 | return; 34 | } 35 | self.outline().lines.expand_to_first_level_modules(); 36 | self.update_after_folding_outline(); 37 | } 38 | 39 | pub fn outline_fold_expand_toggle(&mut self) { 40 | if !self.outline.is_module_tree() { 41 | return; 42 | } 43 | if let Some(id) = self.outline().get_id() { 44 | self.outline().lines.expand_toggle(id); 45 | self.update_after_folding_outline(); 46 | } 47 | } 48 | 49 | fn update_after_folding_outline(&mut self) { 50 | self.update_area_inner(self.area); 51 | 52 | let outline = self.outline(); 53 | if outline.visible_lines().is_none() { 54 | // start from the begining if nothing needs to show up 55 | outline.start = 0; 56 | } 57 | // try jumping to previous line 58 | if !outline.check_if_can_return_to_previous_cursor() { 59 | // if no previous line is found, jump to the first line 60 | outline.set_cursor(0); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/bin/page/panel.rs: -------------------------------------------------------------------------------- 1 | use crate::color::{NEW, SET}; 2 | 3 | #[derive(Debug)] 4 | pub enum Panel { 5 | Outline, 6 | Content, 7 | Navigation, 8 | } 9 | 10 | impl super::Page { 11 | /// Responde to mouse click from left button. 12 | pub fn set_current_panel(&mut self, y: u16, x: u16) { 13 | macro_rules! set { 14 | (outline) => { set!(#Outline 0 1 2) }; 15 | (content) => { set!(#Content 1 0 2) }; 16 | (navi) => { set!(#Navigation 2 0 1) }; 17 | (#$var:ident $a:tt $b:tt $c:tt) => {{ 18 | let block = ( 19 | self.outline.border.block_mut(), 20 | self.content.border.block_mut(), 21 | self.navi.border().block_mut(), 22 | ); 23 | *block.$a = block.$a.clone().style(SET); 24 | *block.$b = block.$b.clone().style(NEW); 25 | *block.$c = block.$c.clone().style(NEW); 26 | Some(Panel::$var) 27 | }}; 28 | } 29 | let position = (x, y).into(); 30 | // Block area covers border and its inner 31 | self.current = if self.outline.border.area().contains(position) { 32 | self.outline().set_cursor(y); 33 | self.update_content(); 34 | set!(outline) 35 | } else if self.content.border.area().contains(position) { 36 | if let Some(id) = self.content.jumpable_id(x, y) { 37 | self.jump_to_id(&id); 38 | } 39 | set!(content) 40 | } else if self.navi.contains(position) { 41 | if self.heading_jump(y) { 42 | // succeed to jump to a heading, thus focus on content panel 43 | set!(content) 44 | } else if let Some(action) = self.navi.update_outline(y) { 45 | self.outline.action(action); 46 | self.update_area_inner(self.area); 47 | set!(outline) 48 | } else { 49 | set!(navi) 50 | } 51 | } else { 52 | None 53 | }; 54 | info!(?self.current); 55 | } 56 | 57 | pub fn set_next_action(&mut self) { 58 | let next_action = self.navi.next_action(); 59 | debug!(?next_action); 60 | if let Some(action) = next_action { 61 | self.outline.action(action); 62 | self.update_area_inner(self.area); 63 | // set!(outline) 64 | } 65 | } 66 | 67 | pub fn set_previous_action(&mut self) { 68 | let next_action = self.navi.previous_action(); 69 | debug!(?next_action); 70 | if let Some(action) = next_action { 71 | self.outline.action(action); 72 | self.update_area_inner(self.area); 73 | // set!(outline) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/bin/tui.rs: -------------------------------------------------------------------------------- 1 | use crate::{event::EventHandler, Frame, Result}; 2 | use color_eyre::eyre; 3 | use crossterm::{ 4 | cursor, 5 | event::{DisableMouseCapture, EnableMouseCapture}, 6 | execute, 7 | terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}, 8 | }; 9 | use ratatui::{backend::CrosstermBackend, layout::Rect, Terminal}; 10 | use std::{io, panic}; 11 | 12 | pub struct Tui { 13 | /// Interface to the Terminal. 14 | terminal: CrosstermTerminal, 15 | /// Terminal event handler. 16 | pub events: EventHandler, 17 | } 18 | 19 | impl Tui { 20 | pub fn new(timeout: u64) -> Result { 21 | enter_terminal()?; 22 | let terminal = CrosstermTerminal::new(CrosstermBackend::new(pipeline()))?; 23 | let events = EventHandler::new(timeout); 24 | Ok(Tui { terminal, events }) 25 | } 26 | 27 | pub fn draw(&mut self, widgets: &mut Frame) -> Result<()> { 28 | self.terminal 29 | .draw(|frame| frame.render_widget(widgets, frame.area()))?; 30 | Ok(()) 31 | } 32 | 33 | pub fn size(&self) -> Result { 34 | let size = self.terminal.size()?; 35 | Ok(Rect::new(0, 0, size.width, size.height)) 36 | } 37 | } 38 | 39 | impl Drop for Tui { 40 | fn drop(&mut self) { 41 | restore_terminal().expect("Failed to restore terminal when dropping App"); 42 | } 43 | } 44 | 45 | pub type CrosstermTerminal = Terminal>; 46 | 47 | fn pipeline() -> io::Stdout { 48 | io::stdout() 49 | } 50 | 51 | /// Resets the terminal interface when the program exits. 52 | /// 53 | /// This function is also used for the panic hook to revert 54 | /// the terminal properties if unexpected errors occur. 55 | fn restore_terminal() -> io::Result<()> { 56 | terminal::disable_raw_mode()?; 57 | execute!( 58 | pipeline(), 59 | LeaveAlternateScreen, 60 | DisableMouseCapture, 61 | cursor::Show 62 | )?; 63 | Ok(()) 64 | } 65 | 66 | /// Set alternate screen and mouse capturing etc when the program starts. 67 | fn enter_terminal() -> io::Result<()> { 68 | terminal::enable_raw_mode()?; 69 | execute!( 70 | pipeline(), 71 | EnterAlternateScreen, 72 | EnableMouseCapture, 73 | cursor::Hide, 74 | Clear(ClearType::All) 75 | )?; 76 | Ok(()) 77 | } 78 | 79 | /// This replaces the standard color_eyre panic and error hooks with hooks that 80 | /// restore the terminal before printing the panic or error. 81 | pub fn install_hooks() -> crate::Result<()> { 82 | // add any extra configuration you need to the hook builder 83 | let hook_builder = color_eyre::config::HookBuilder::default(); 84 | let (panic_hook, eyre_hook) = hook_builder.into_hooks(); 85 | 86 | // convert from a color_eyre PanicHook to a standard panic hook 87 | let panic_hook = panic_hook.into_panic_hook(); 88 | panic::set_hook(Box::new(move |panic_info| { 89 | restore_terminal().unwrap(); 90 | panic_hook(panic_info); 91 | })); 92 | 93 | // convert from a color_eyre EyreHook to a eyre ErrorHook 94 | let eyre_hook = eyre_hook.into_eyre_hook(); 95 | eyre::set_hook(Box::new(move |error| { 96 | // restore_terminal().unwrap(); 97 | eyre_hook(error) 98 | }))?; 99 | 100 | Ok(()) 101 | } 102 | -------------------------------------------------------------------------------- /src/bin/ui/mod.rs: -------------------------------------------------------------------------------- 1 | /// Scrollable widget 2 | pub mod scrollable; 3 | /// A block with area. Use the inner area to draw the real content. 4 | mod surround; 5 | 6 | pub use scrollable::{ 7 | render_line, LineState, MarkdownAndHeading, Scroll, ScrollMarkdown, ScrollOffset, Scrollable, 8 | }; 9 | pub use surround::Surround; 10 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/generics.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{buffer::Buffer, style::Style}; 2 | use rustdoc_types::Id; 3 | use std::ops::Deref; 4 | use term_rustdoc::{tree::TreeLine, util::XString}; 5 | use unicode_width::UnicodeWidthStr; 6 | 7 | pub trait Lines: Deref { 8 | type Line: LineState; 9 | } 10 | 11 | impl> Lines for Ls { 12 | type Line = L; 13 | } 14 | 15 | pub trait LineState { 16 | type State: PartialEq + Default; 17 | fn state(&self) -> Self::State; 18 | fn is_identical(&self, state: &Self::State) -> bool; 19 | } 20 | 21 | impl LineState for TreeLine { 22 | type State = Option; 23 | 24 | fn state(&self) -> Self::State { 25 | self.id 26 | } 27 | 28 | fn is_identical(&self, state: &Self::State) -> bool { 29 | self.id == *state 30 | } 31 | } 32 | 33 | // Render a line as much as possible. Stop when the width is not enough, but still try to 34 | // write the stoping text in the remaining width. 35 | pub fn render_line<'t, I>(line: I, buf: &mut Buffer, mut x: u16, y: u16, width: usize) -> usize 36 | where 37 | I: IntoIterator, 38 | { 39 | let mut used_width = 0usize; 40 | for (text, style) in line { 41 | // stop rendering once it hits the end of width 42 | let text_width = text.width(); 43 | if used_width + text_width > width { 44 | if let Some(rest) = width.checked_sub(used_width).filter(|w| *w > 0) { 45 | let (succeed, fail) = if let Some(text) = text.get(..rest) { 46 | let (x_pos, _) = buf.set_stringn(x, y, text, width, style); 47 | used_width += x_pos.saturating_sub(x) as usize; 48 | x = x_pos; 49 | (rest, text_width.saturating_sub(rest)) 50 | } else { 51 | (0, text_width) 52 | }; 53 | warn!( 54 | "{text:?} truncated in row {y} col {x} and possibly partially written. \ 55 | (used {used_width}, the next text_width {text_width} \ 56 | with {succeed} written {fail} not written, maximum {width})", 57 | ); 58 | } 59 | return used_width; 60 | } 61 | let (x_pos, _) = buf.set_stringn(x, y, text, width, style); 62 | used_width += x_pos.saturating_sub(x) as usize; 63 | x = x_pos; 64 | } 65 | used_width 66 | } 67 | 68 | pub fn render_line_fill_gap<'t, I>( 69 | line: I, 70 | style: Style, 71 | buf: &mut Buffer, 72 | mut x: u16, 73 | y: u16, 74 | width: usize, 75 | gap_str: &mut XString, 76 | ) where 77 | I: IntoIterator, 78 | { 79 | let mut used_width = 0usize; 80 | for text in line { 81 | // stop rendering once it hits the end of width 82 | let text_width = text.width(); 83 | if used_width + text_width > width { 84 | if let Some(rest) = width.checked_sub(used_width).filter(|w| *w > 0) { 85 | let (succeed, fail) = if let Some(text) = text.get(..rest) { 86 | let (x_pos, _) = buf.set_stringn(x, y, text, width, style); 87 | used_width += x_pos.saturating_sub(x) as usize; 88 | x = x_pos; 89 | (rest, text_width.saturating_sub(rest)) 90 | } else { 91 | (0, text_width) 92 | }; 93 | warn!( 94 | "{text:?} truncated in row {y} col {x} and possibly partially written. \ 95 | (used {used_width}, the next text_width {text_width} \ 96 | with {succeed} written {fail} not written, maximum {width})", 97 | ); 98 | } 99 | } 100 | let (x_pos, _) = buf.set_stringn(x, y, text, width, style); 101 | used_width += x_pos.saturating_sub(x) as usize; 102 | x = x_pos; 103 | } 104 | if let Some(gap) = width.checked_sub(used_width).filter(|gap| *gap > 0) { 105 | gap_str.clear(); 106 | (0..gap).for_each(|_| gap_str.push(' ')); 107 | buf.set_stringn(x, y, gap_str, gap, style); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/markdown/heading.rs: -------------------------------------------------------------------------------- 1 | use super::region::SelectedRegion; 2 | use crate::{ 3 | color::HEAD, 4 | ui::scrollable::{ 5 | generics::{render_line, render_line_fill_gap, LineState}, 6 | Scroll, 7 | }, 8 | }; 9 | use ratatui::prelude::Buffer; 10 | use term_rustdoc::{tree::Text, util::XString}; 11 | 12 | pub type ScrollHeading = Scroll; 13 | 14 | #[derive(Debug)] 15 | pub struct Heading { 16 | line: Text, 17 | jump: SelectedRegion, 18 | } 19 | 20 | impl Heading { 21 | fn new(text: XString, jump: SelectedRegion) -> Self { 22 | Self { 23 | line: Text { 24 | text, 25 | style: Default::default(), 26 | }, 27 | jump, 28 | } 29 | } 30 | 31 | pub fn as_str(&self) -> &str { 32 | &self.line.text 33 | } 34 | 35 | pub fn jump_row_start(&self) -> usize { 36 | self.jump.row_start() 37 | } 38 | } 39 | 40 | impl LineState for Heading { 41 | type State = XString; 42 | 43 | fn state(&self) -> Self::State { 44 | self.line.text.clone() 45 | } 46 | 47 | fn is_identical(&self, state: &Self::State) -> bool { 48 | self.as_str() == *state 49 | } 50 | } 51 | 52 | #[derive(Default)] 53 | pub struct Headings { 54 | lines: Vec, 55 | } 56 | 57 | impl std::ops::Deref for Headings { 58 | type Target = [Heading]; 59 | 60 | fn deref(&self) -> &Self::Target { 61 | &self.lines 62 | } 63 | } 64 | 65 | impl Headings { 66 | pub fn with_capacity(cap: usize) -> Self { 67 | Headings { 68 | lines: Vec::with_capacity(cap), 69 | } 70 | } 71 | 72 | pub fn push(&mut self, text: XString, region: SelectedRegion) { 73 | self.lines.push(Heading::new(text, region)); 74 | } 75 | } 76 | 77 | impl ScrollHeading { 78 | pub fn update_headings(&mut self, headings: Headings) { 79 | self.lines = headings; 80 | } 81 | 82 | pub fn render(&self, buf: &mut Buffer, content_start: usize, content_end: usize) { 83 | let width = self.area.width; 84 | if width == 0 { 85 | return; 86 | } 87 | let (x, mut y, width) = (self.area.x, self.area.y, width as usize); 88 | let mut gap_str = XString::const_new(""); 89 | let lines = &self.lines.lines; 90 | for (idx, line) in lines.iter().enumerate() { 91 | let text = &line.as_str(); 92 | let text = text.get(..width.min(text.len())).unwrap_or(""); 93 | let row_start = line.jump.row_start(); 94 | // highlight the heading when the heading line is in visual range or 95 | // only the contents after the heading is in visual range 96 | if (content_start <= row_start && content_end > row_start) 97 | || (content_start > row_start 98 | && lines 99 | .get(idx + 1) 100 | .map(|l| content_end < l.jump.row_start()) 101 | .unwrap_or(true)) 102 | { 103 | render_line_fill_gap(Some(text), HEAD, buf, x, y, width, &mut gap_str); 104 | } else { 105 | let style = line.line.style; 106 | render_line(Some((text, style)), buf, x, y, width); 107 | } 108 | y += 1; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/markdown/ingerated.rs: -------------------------------------------------------------------------------- 1 | use super::{fallback::StyledLine, render::write_lines, ScrollHeading}; 2 | use crate::ui::{scrollable::markdown::parse::parse_doc, Scroll}; 3 | use ratatui::prelude::{Buffer, Constraint, Layout, Rect}; 4 | 5 | pub struct MarkdownAndHeading { 6 | md: MarkdownArea, 7 | heading: ScrollHeading, 8 | } 9 | 10 | pub type ScrollMarkdown = Scroll; 11 | 12 | const TOC_WIDTH: u16 = 16; 13 | 14 | impl MarkdownAndHeading { 15 | pub fn new(mut md: &str, area: Rect) -> Self { 16 | let width = area.width.saturating_sub(1); 17 | if width < TOC_WIDTH { 18 | md = "too narrow to show anything"; 19 | } 20 | let [md_area, head_area] = split_area(area); 21 | let (lines, _, headings) = parse_doc(md, md_area.width as f64); 22 | let mut heading = ScrollHeading::default(); 23 | heading.update_headings(headings); 24 | heading.area = head_area; 25 | MarkdownAndHeading { 26 | md: MarkdownArea::new(md_area, lines), 27 | heading, 28 | } 29 | } 30 | 31 | pub fn render(&self, buf: &mut Buffer) { 32 | self.md.render(buf); 33 | let content_start = self.md.inner.start; 34 | let content_end = self.md.inner.area.height as usize + content_start; 35 | self.heading.render(buf, content_start, content_end); 36 | } 37 | 38 | pub fn scroll_text(&mut self) -> &mut ScrollMarkdown { 39 | &mut self.md.inner 40 | } 41 | 42 | pub fn heading(&mut self) -> &mut ScrollHeading { 43 | &mut self.heading 44 | } 45 | 46 | /// y is the row in full screen 47 | pub fn heading_jump(&mut self, y: u16) -> bool { 48 | const MARGIN: usize = 1; 49 | if let Some(heading) = self.heading.get_line_on_screen(y) { 50 | // set the upper bound: usually no need to use this, but who knows if y points 51 | // to a line out of the doc range. 52 | let limit = self.md.inner.total_len().saturating_sub(MARGIN); 53 | let old = self.md.inner.start; 54 | self.md.inner.start = heading.jump_row_start().saturating_sub(MARGIN).min(limit); 55 | let new = self.md.inner.start; 56 | info!(old, new); 57 | return true; 58 | } 59 | false 60 | } 61 | } 62 | 63 | fn split_area(area: Rect) -> [Rect; 2] { 64 | // in case heading is too wide 65 | let [md, _, head] = Layout::horizontal([ 66 | Constraint::Min(0), 67 | Constraint::Length(4), 68 | Constraint::Length(TOC_WIDTH), 69 | ]) 70 | .areas(area); 71 | [md, head] 72 | } 73 | 74 | #[derive(Default)] 75 | pub struct MarkdownArea { 76 | inner: Scroll, 77 | } 78 | 79 | impl MarkdownArea { 80 | fn new(area: Rect, lines: Vec) -> Self { 81 | let inner = Scroll:: { 82 | area, 83 | lines: MarkdownInner { lines }, 84 | ..Default::default() 85 | }; 86 | MarkdownArea { inner } 87 | } 88 | 89 | pub fn render(&self, buf: &mut Buffer) { 90 | write_lines(&self.inner.lines, self.inner.start, self.inner.area, buf); 91 | } 92 | } 93 | 94 | #[derive(Default)] 95 | pub struct MarkdownInner { 96 | lines: Vec, 97 | } 98 | 99 | impl std::ops::Deref for MarkdownInner { 100 | type Target = [StyledLine]; 101 | 102 | fn deref(&self) -> &Self::Target { 103 | &self.lines 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/markdown/mod.rs: -------------------------------------------------------------------------------- 1 | /// Use the custom markdown highlighting based on parsing contents to wrap texts. 2 | /// But still can fall back to syntect's highlights without text wrapping. 3 | mod fallback; 4 | /// markdown headings 5 | mod heading; 6 | mod parse; 7 | /// A continuous region that may be across lines. 8 | mod region; 9 | mod render; 10 | /// cached and styled lines that are wrapped and incompletely highlighted 11 | mod wrapped; 12 | 13 | /// A rendering widget that contains 14 | /// * a scrollable markdown area with texts wrapped 15 | /// * a scrollable, auto-updated and clickable heading area 16 | mod ingerated; 17 | 18 | pub use self::{ 19 | fallback::ScrollText, 20 | heading::{Headings, ScrollHeading}, 21 | ingerated::{MarkdownAndHeading, ScrollMarkdown}, 22 | wrapped::StyledText, 23 | }; 24 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/markdown/parse/code_block.rs: -------------------------------------------------------------------------------- 1 | // fenced codeblock including the tags and snippet are special in rustdoc: 2 | // * empty fence tag means Rust code 3 | // * extra tags following `rust,` hint extra rendering 4 | // * code snippet beginning with `# ` is hidden as default 5 | 6 | use super::{convert_style, Block, Line, MetaTag, Word, SYNTHEME}; 7 | use ratatui::style::{Color, Style}; 8 | use syntect::{easy::HighlightLines, util::LinesWithEndings}; 9 | 10 | pub fn parse(fence: &mut str, code: &str) -> Block { 11 | fence.make_ascii_lowercase(); 12 | match &*fence { 13 | "" | "rust" | "rs" => rust(code), 14 | _ => other(fence, code), 15 | } 16 | } 17 | 18 | fn word(text: &str, style: syntect::highlighting::Style) -> Word { 19 | Word { 20 | word: text.into(), 21 | style: convert_style(style), 22 | tag: MetaTag::CodeBlock("rust".into()), 23 | trailling_whitespace: false, 24 | } 25 | } 26 | 27 | /// If the lang is not in SyntaxSet, first fall back to Rust lang, then this one. 28 | #[cold] 29 | fn fallback(code: &str) -> Block { 30 | code.lines() 31 | .map(|line| Word { 32 | word: line.into(), 33 | style: Style { 34 | fg: Some(Color::LightRed), 35 | ..Default::default() 36 | }, 37 | tag: MetaTag::CodeBlock("Unknown".into()), 38 | trailling_whitespace: false, 39 | }) 40 | .collect() 41 | } 42 | 43 | pub fn rust(code: &str) -> Block { 44 | SYNTHEME.with(|(ps, ts)| { 45 | let Some(syntax) = ps.find_syntax_by_name("Rust") else { 46 | return fallback(code); 47 | }; 48 | let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]); 49 | let mut lines = Vec::with_capacity(8); 50 | // filter out the lines starting `# ` used for hidden lines 51 | for line in code.lines().filter(|l| !{ 52 | // a line begins with optional whitespaces and `# `, or a line with mere `#` 53 | let line = l.trim(); 54 | line.starts_with("# ") || line == "#" 55 | }) { 56 | let mut words = Vec::with_capacity(8); 57 | for (style, text) in h.highlight_line(line, ps).unwrap() { 58 | words.push(word(text, style)); 59 | } 60 | lines.push(Line::from_iter(words)); 61 | } 62 | let mut block = Block::from_iter(lines); 63 | block.shrink_to_fit(); 64 | block 65 | }) 66 | } 67 | 68 | macro_rules! gen_parse_code { 69 | ($( $fname:ident ),+) => { $( 70 | pub fn $fname(code: &str) -> Block { 71 | SYNTHEME.with(|(ps, ts)| { 72 | let Some(syntax) = ps.find_syntax_by_name(stringify!($fname)) else { 73 | return rust(code); 74 | }; 75 | gen_parse_code! { #inner code ps ts syntax } 76 | }) 77 | } 78 | )+ }; 79 | (#inner $code:ident $ps:ident $ts:ident $syntax:ident) => { 80 | let mut h = HighlightLines::new($syntax, &$ts.themes["base16-ocean.dark"]); 81 | let mut lines = Vec::with_capacity(8); 82 | for line in LinesWithEndings::from($code) { 83 | let mut words = Vec::with_capacity(8); 84 | for (style, text) in h.highlight_line(line, $ps).unwrap() { 85 | words.push(word(text, style)); 86 | } 87 | lines.push(Line::from_iter(words)); 88 | } 89 | let mut block = Block::from_iter(lines); 90 | block.shrink_to_fit(); 91 | block 92 | }; 93 | } 94 | 95 | /// If the lang is not found by file extention, use Rust as fallback. 96 | pub fn other(lang: &str, code: &str) -> Block { 97 | SYNTHEME.with(|(ps, ts)| { 98 | let Some(syntax) = ps.find_syntax_by_extension(lang) else { 99 | return rust(code); 100 | }; 101 | gen_parse_code! { #inner code ps ts syntax } 102 | }) 103 | } 104 | 105 | gen_parse_code!(markdown); 106 | 107 | pub fn md_table(table: &str) -> Block { 108 | markdown(table) 109 | } 110 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/markdown/parse/entry_point.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | code_block, 3 | element::{Element, FOOTNOTE}, 4 | list::{self, parse_codeblock}, 5 | Block, Blocks, MetaTag, Word, 6 | }; 7 | use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; 8 | use term_rustdoc::util::{xformat, XString}; 9 | 10 | pub fn parse(doc: &str) -> Blocks { 11 | if doc.is_empty() { 12 | return Blocks::default(); 13 | } 14 | let mut blocks = Blocks::new(); 15 | let mut iter = markdown_iter(doc); 16 | while let Some((event, range)) = iter.by_ref().next() { 17 | match event { 18 | Event::Start(Tag::Paragraph) => { 19 | let mut block = Block::default(); 20 | let para = ele!(iter, Paragraph, range); 21 | Element::new(doc, &mut block, blocks.links(), para).parse_paragraph(); 22 | blocks.push(block); 23 | } 24 | Event::Start(Tag::CodeBlock(CodeBlockKind::Indented)) => { 25 | let code_block = &doc[range.clone()]; 26 | blocks.push(code_block::rust(code_block)); 27 | let _ = ele!(iter, CodeBlock, range); 28 | } 29 | Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(fence))) => { 30 | let fence = XString::from(&*fence); 31 | let mut block = Block::default(); 32 | parse_codeblock(doc[range.clone()].trim(), fence, &mut block); 33 | blocks.push(block); 34 | // consume the codeblock iterator 35 | let _ = ele!(iter, CodeBlock, range); 36 | } 37 | Event::Start(Tag::Heading { level, .. }) => { 38 | let raw = &doc[range.clone()]; 39 | let heading = ele!(#heading iter, level, range); 40 | let mut block = Block::default(); 41 | let mut sharps = XString::default(); 42 | let level = level as u8; 43 | (0..level).for_each(|_| sharps.push('#')); 44 | sharps.push(' '); 45 | block.push_a_word(Word { 46 | word: sharps, 47 | ..Default::default() 48 | }); 49 | Element::new(doc, &mut block, blocks.links(), heading).parse_paragraph(); 50 | let id = blocks.links().push_heading(level, raw); 51 | block.set_heading(id); 52 | blocks.push(block); 53 | } 54 | Event::Rule => blocks.push(Block::from_iter([Word { 55 | tag: MetaTag::Rule, 56 | ..Default::default() 57 | }])), 58 | Event::Start(Tag::Table(_)) => { 59 | // the table is rendered via original contents with syntect's highlights 60 | blocks.push(code_block::md_table(&doc[range.clone()])); 61 | let _ = ele!(iter, Table, range); // consume the whole table 62 | } 63 | Event::Start(Tag::BlockQuote(_)) => { 64 | if let Some((Event::Start(Tag::Paragraph), range)) = iter.next() { 65 | let mut block = Block::default(); 66 | let para = ele!(iter, Paragraph, range); 67 | Element::new(doc, &mut block, blocks.links(), para).parse_paragraph(); 68 | block.set_quote_block(); 69 | blocks.push(block); 70 | } 71 | } 72 | Event::Start(Tag::FootnoteDefinition(key)) => { 73 | if let Some((Event::Start(Tag::Paragraph), range)) = iter.next() { 74 | let mut block = Block::default(); 75 | block.push_a_word(Word { 76 | word: xformat!("[^{key}]: "), 77 | style: FOOTNOTE, 78 | tag: MetaTag::FootnoteSource, 79 | trailling_whitespace: false, 80 | }); 81 | let para = ele!(iter, Paragraph, range); 82 | Element::new(doc, &mut block, blocks.links(), para).parse_paragraph(); 83 | block.set_foot_note(); 84 | blocks.links().push_footnote(&key, block); 85 | } 86 | } 87 | Event::Start(Tag::List(kind)) => { 88 | let iter = ele!(#list iter, kind.is_some(), range); 89 | let mut block = Block::default(); 90 | list::parse(&mut 0, kind, iter, &mut block, doc, blocks.links()); 91 | blocks.push(block); 92 | } 93 | _ => (), 94 | } 95 | } 96 | blocks.shrink_to_fit(); 97 | blocks 98 | } 99 | 100 | fn markdown_iter( 101 | doc: &str, 102 | ) -> pulldown_cmark::OffsetIter<'_, pulldown_cmark::DefaultBrokenLinkCallback> { 103 | Parser::new_ext( 104 | doc, 105 | Options::ENABLE_FOOTNOTES 106 | | Options::ENABLE_STRIKETHROUGH 107 | | Options::ENABLE_TABLES 108 | | Options::ENABLE_TASKLISTS, 109 | ) 110 | .into_offset_iter() 111 | } 112 | 113 | #[cfg(test)] 114 | mod tests; 115 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/markdown/parse/entry_point/snapshots/term_rustdoc__ui__scrollable__markdown__parse__entry_point__tests__parse_markdown-StyledLines.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/bin/ui/scrollable/markdown/parse/entry_point/tests.rs 3 | expression: lines 4 | --- 5 | [ 6 | "# ""h1 ""`", 7 | "code""`", 8 | , 9 | "aaa ""b ""c", 10 | "d ""e"".", 11 | "xxx"" ""z.", 12 | , 13 | "1 ""c", 14 | "ss"" ""d", 15 | "sadsad", 16 | "xxx"" ""`", 17 | "yyyy""`", 18 | , 19 | "```rust", 20 | "let"" a ""=", 21 | " ""1"";", 22 | "```", 23 | , 24 | "rrr ""sss", 25 | "tt", 26 | , 27 | "* ""[x]", 28 | "done!", 29 | " ""*", 30 | "nested", 31 | "list", 32 | "* ""[ ]", 33 | "undone", 34 | " ""1. ""a", 35 | " ""2. ""`", 36 | "b""`", 37 | , 38 | ] 39 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/markdown/parse/entry_point/snapshots/term_rustdoc__ui__scrollable__markdown__parse__entry_point__tests__parse_markdown_links-StyledLines.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/bin/ui/scrollable/markdown/parse/entry_point/tests.rs 3 | expression: lines 4 | --- 5 | [ 6 | "[""a""]""[""0""]"", ""[""c""]""[""1""]"", ""[""e""]""[", 7 | "1""]"". ""[""long""]""[""2""]", 8 | , 9 | "[0]: ""b", 10 | "[1]: ""d", 11 | "[2]:", 12 | "xxxxxxxxxxxxxxxxxxxx", 13 | "xxxxxxxxxxxxxxxxxxxx", 14 | "xxxxxx", 15 | , 16 | "[""`""f""`""]""[""1""]"".", 17 | , 18 | "[1]: ""d", 19 | , 20 | "## ""h2 ""[""c""]""[""1""]"" ""[""`""h""`""]""[", 21 | "1""]", 22 | , 23 | "[1]: ""d", 24 | , 25 | "m""[""^""n""]"".", 26 | , 27 | "[^n]: ""blah", 28 | , 29 | ] 30 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/markdown/parse/entry_point/tests.rs: -------------------------------------------------------------------------------- 1 | use super::{markdown_iter, parse}; 2 | use insta::{assert_debug_snapshot as snap, assert_snapshot as shot}; 3 | 4 | #[test] 5 | fn parse_markdown() { 6 | let doc = r#" 7 | # h1 `code` 8 | 9 | aaa b *c* d **e**. ~xxx~ z. 10 | 11 | 1 *c **ss** d sadsad xxx* `yyyy` 12 | 13 | ``` 14 | let a = 1; 15 | ``` 16 | 17 | > rrr sss 18 | > tt 19 | 20 | - [x] done! 21 | - nested list 22 | - [ ] undone 23 | 1. *a* 24 | 2. `b` 25 | "#; 26 | snap!(markdown_iter(doc).collect::>()); 27 | let mut blocks = parse(doc); 28 | shot!(blocks, @r###" 29 | # h1 `code` 30 | 31 | aaa b c d e. xxx z. 32 | 33 | 1 c ss d sadsad xxx `yyyy` 34 | 35 | ```rust 36 | let a = 1; 37 | ``` 38 | 39 | rrr sss tt 40 | 41 | * [x] done! 42 | * nested list 43 | * [ ] undone 44 | 1. a 45 | 2. `b` 46 | 47 | "###); 48 | 49 | let lines = blocks.write_styled_lines(7.0); 50 | snap!("parse_markdown-StyledLines", lines); 51 | snap!("parse_markdown-parsed", blocks); 52 | } 53 | 54 | /// This test is used to quickly test text wrapping. 55 | #[test] 56 | fn parse_markdown_dbg() { 57 | let doc = r#" 58 | "#; 59 | const WIDTH: f64 = 70.0; 60 | let lines = parse(doc).write_styled_lines(WIDTH); 61 | dbg!(lines); 62 | } 63 | 64 | #[test] 65 | fn parse_markdown_links() { 66 | let doc = " 67 | [a](b), [c], [e][c]. [long] 68 | 69 | [c]: d 70 | 71 | [long]: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 72 | 73 | [`f`][c]. 74 | 75 | ## h2 [c] [`h`][c] 76 | 77 | m[^n]. 78 | 79 | [^n]: blah 80 | "; 81 | snap!(markdown_iter(doc).collect::>()); 82 | let mut blocks = parse(doc); 83 | shot!(blocks, @r###" 84 | [a][0], [c][1], [e][1]. [long][2] 85 | 86 | [`f`][1]. 87 | 88 | ## h2 [c][1] [`h`][1] 89 | 90 | m[^n]. 91 | 92 | "###); 93 | 94 | let lines = blocks.write_styled_lines(20.0); 95 | snap!("parse_markdown_links-StyledLines", lines); 96 | snap!("parse_markdown_links-parsed", blocks); 97 | } 98 | 99 | #[test] 100 | fn parse_markdown_intra_code() { 101 | let doc = "A `code` in a line."; 102 | dbg!(markdown_iter(doc).collect::>(), parse(doc)); 103 | } 104 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/markdown/parse/line.rs: -------------------------------------------------------------------------------- 1 | use super::{word::Word, MetaTag}; 2 | use ratatui::prelude::{Color, Modifier, Style}; 3 | use std::{fmt, ops::Deref}; 4 | use term_rustdoc::util::XString; 5 | 6 | /// A line to be rendered on screen, containing multiple words. 7 | /// 8 | /// For a line in a Paragraph block, texts are usually wrapped at fixed width. 9 | #[derive(Default, Debug, Clone)] 10 | pub struct Line { 11 | pub words: Vec, 12 | } 13 | 14 | impl FromIterator for Line { 15 | fn from_iter>(iter: T) -> Self { 16 | Line { 17 | words: Vec::from_iter(iter), 18 | } 19 | } 20 | } 21 | 22 | impl Extend for Line { 23 | fn extend>(&mut self, iter: T) { 24 | self.words.extend(iter); 25 | } 26 | } 27 | 28 | impl fmt::Display for Line { 29 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 30 | let len = self.words.len(); 31 | self.words.iter().enumerate().try_for_each(|(idx, word)| { 32 | if word.trailling_whitespace && idx + 1 != len { 33 | write!(f, "{} ", word.word) 34 | } else { 35 | write!(f, "{}", word.word) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | impl Deref for Line { 42 | type Target = [Word]; 43 | 44 | fn deref(&self) -> &Self::Target { 45 | &self.words 46 | } 47 | } 48 | 49 | impl Line { 50 | pub fn backtick(text: &str, fence: XString) -> [Line; 2] { 51 | let mut words = Vec::with_capacity(2); 52 | let mut start = 0; 53 | if let Some(split) = text.find('`') { 54 | words.push(Word { 55 | word: text[..split].into(), 56 | ..Default::default() 57 | }); 58 | start = split; 59 | } 60 | words.push(Word { 61 | word: text[start..].into(), 62 | style: Style { 63 | fg: Some(Color::Red), 64 | add_modifier: Modifier::BOLD, 65 | ..Style::new() 66 | }, 67 | tag: MetaTag::CodeBlock(fence.clone()), 68 | trailling_whitespace: false, 69 | }); 70 | let pair2 = Line { words }; 71 | let mut pair1 = pair2.words.clone(); 72 | if let Some(tick) = pair1.last_mut() { 73 | let lang = if fence.is_empty() { 74 | "rust" 75 | } else { 76 | // This keeps rustdoc's attributes, like `ignore` or `should_panic`. 77 | // [attributes]: https://doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html#attributes 78 | &fence 79 | }; 80 | tick.word.push_str(lang); 81 | } 82 | [Line { words: pair1 }, pair2] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/markdown/parse/meta_tag.rs: -------------------------------------------------------------------------------- 1 | use rustdoc_types::Id; 2 | use term_rustdoc::util::XString; 3 | 4 | /// Extra meaning not so relevant to style in current word. 5 | #[allow(dead_code)] 6 | #[derive(Default, Clone, Debug)] 7 | pub enum MetaTag { 8 | #[default] 9 | Normal, 10 | Link(LinkTag), 11 | InlineCode, 12 | // InlineHTML, 13 | ListItem, 14 | ListItemN(u8), 15 | 16 | // separate block elements: directly rendered as a truncatable line 17 | Heading(usize), 18 | Image, 19 | Rule, 20 | FootnoteSource, 21 | 22 | CodeBlock(XString), 23 | QuoteBlock, 24 | } 25 | 26 | /// metadata/extra info in a chunk of text 27 | /// 28 | /// StyledText { text: String, style: Style, meta: Option, pos: (u16, u16) } 29 | /// Heading(u8) 30 | /// Line { inner: Vec, meta: Heading } 31 | /// Paragraph { inner: Vec, externallinks: Vec, 32 | /// hyperlinks: Vec, footnotes: Vec } 33 | /// 34 | /// Links { inner: Vec } 35 | /// the usize is used only in the doc to refer to the link position in vec to highlight it 36 | #[derive(Clone, Debug)] 37 | #[allow(dead_code)] 38 | pub enum LinkTag { 39 | /// local crate item can be referred by item ID 40 | LocalItemLink(Id), 41 | /// points to a external crate item path (may be supported once multi-crate docs are ready) 42 | ExternalItemLink(usize), 43 | /// Reference link 44 | ReferenceLink(usize), 45 | /// a link to styled text 46 | Footnote(XString), 47 | // /// Autolink or Email, both of which are in the form of `` 48 | // /// 49 | // /// the URL content will be rendered directly and won't be cached in vec 50 | // Url(XString), 51 | /// broken link or invalid input tag 52 | Unknown, 53 | } 54 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/markdown/parse/mod.rs: -------------------------------------------------------------------------------- 1 | use super::{fallback::StyledLine, heading::Headings}; 2 | use icu_segmenter::LineSegmenter; 3 | use itertools::Itertools; 4 | use ratatui::style::{Color, Modifier, Style}; 5 | use syntect::{ 6 | easy::HighlightLines, 7 | highlighting::{FontStyle, ThemeSet}, 8 | parsing::SyntaxSet, 9 | util::LinesWithEndings, 10 | }; 11 | 12 | mod code_block; 13 | #[macro_use] 14 | mod element; 15 | mod entry_point; 16 | mod list; 17 | mod meta_tag; 18 | 19 | mod block; 20 | mod blocks; 21 | mod line; 22 | mod word; 23 | 24 | pub use self::{ 25 | block::Block, 26 | blocks::{Blocks, Links}, 27 | line::Line, 28 | meta_tag::{LinkTag, MetaTag}, 29 | word::Word, 30 | }; 31 | 32 | thread_local! { 33 | static SYNTHEME: (SyntaxSet, ThemeSet) = ( 34 | SyntaxSet::load_defaults_newlines(), 35 | ThemeSet::load_defaults(), 36 | ); 37 | static SEGMENTER: LineSegmenter = LineSegmenter::new_auto(); 38 | } 39 | 40 | /// Split a `&str` into segmented words without considering trailling whitespaces. 41 | /// 42 | /// This is used in as-is words like intra-codes. 43 | fn segment_str(text: &str, mut f: impl FnMut(&str)) { 44 | SEGMENTER.with(|seg| { 45 | seg.segment_str(text) 46 | .tuple_windows() 47 | .for_each(|(start, end)| f(&text[start..end])); 48 | }) 49 | } 50 | 51 | /// Split a `&str` into segmented and trailling-whitespace-aware words. 52 | /// 53 | /// This is used in context where text wrapping is applied like in normal texts. 54 | pub fn segment_words(text: &str, mut f: impl FnMut(&str, bool)) { 55 | SEGMENTER.with(|seg| { 56 | for (start, end) in seg.segment_str(text).tuple_windows() { 57 | let word_with_potential_trail_whitespace = &text[start..end]; 58 | let word = word_with_potential_trail_whitespace.trim_end_matches(' '); 59 | let trailling_whitespace = word_with_potential_trail_whitespace.len() != word.len(); 60 | f(word, trailling_whitespace); 61 | } 62 | }); 63 | } 64 | 65 | pub fn parse_doc(doc: &str, width: f64) -> (Vec, Blocks, Headings) { 66 | let mut blocks = entry_point::parse(doc); 67 | let lines = blocks.write_styled_lines(width); 68 | let headings = blocks.links().to_heading(); 69 | (lines, blocks, headings) 70 | } 71 | 72 | pub fn md(doc: &str) -> Vec { 73 | let mut lines = Vec::with_capacity(128); 74 | SYNTHEME.with(|(ps, ts)| { 75 | let syntax = ps.find_syntax_by_extension("md").unwrap(); 76 | let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]); 77 | for line in LinesWithEndings::from(doc) { 78 | let mut styled_line = StyledLine::new(); 79 | for (style, text) in h.highlight_line(line, ps).unwrap() { 80 | styled_line.push(text, convert_style(style)); 81 | } 82 | styled_line.shrink_to_fit(); 83 | lines.push(styled_line); 84 | } 85 | }); 86 | lines 87 | } 88 | 89 | fn convert_style(style: syntect::highlighting::Style) -> Style { 90 | let fg = style.foreground; 91 | // let bg = style.background; 92 | let fg = Some(Color::Rgb(fg.r, fg.g, fg.b)); 93 | let add_modifier = match style.font_style { 94 | FontStyle::BOLD => Modifier::BOLD, 95 | FontStyle::UNDERLINE => Modifier::UNDERLINED, 96 | FontStyle::ITALIC => Modifier::ITALIC, 97 | _ => Modifier::empty(), 98 | }; 99 | // FIXME: Don't set underline_color, because it will conflict 100 | // with underline style on outline. 101 | // FIXME: bg seems needless 102 | Style { 103 | fg, 104 | // bg: Some(Color::Rgb(bg.r, bg.g, bg.b)), 105 | add_modifier, 106 | ..Default::default() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/markdown/parse/word.rs: -------------------------------------------------------------------------------- 1 | use super::MetaTag; 2 | use crate::ui::scrollable::markdown::{fallback::StyledLine, region::LinkedRegions, StyledText}; 3 | use ratatui::style::Style; 4 | use std::fmt::{self, Write}; 5 | use term_rustdoc::util::XString; 6 | use textwrap::core::Fragment; 7 | use unicode_width::UnicodeWidthStr; 8 | 9 | /// A unwrappable word that has styling and metadata. 10 | /// 11 | #[derive(Default, Clone)] 12 | pub struct Word { 13 | /// NOTE: the word doesn't contain trailling whitespace, 14 | /// so when generating an owned text, we should use the 15 | /// `trailling_whitespace` to add it back. 16 | pub word: XString, 17 | pub style: Style, 18 | pub tag: MetaTag, 19 | /// serves as two purposes: 20 | /// * indicates the word has an trailling whitespace when the word is amid the line 21 | /// as wrapping algorithm needs 22 | /// * since the style may extend to this potential whitespace, if the value is false, 23 | /// we don't generate a whitespace in owned styled text; but if true, we should do. 24 | pub trailling_whitespace: bool, 25 | } 26 | 27 | impl Word { 28 | pub fn words_to_line( 29 | mut words: &[Word], 30 | row: usize, 31 | linked_regions: &mut LinkedRegions, 32 | ) -> StyledLine { 33 | if let Some(word) = words.first() { 34 | if word.word.is_empty() { 35 | // skip the meaningless whitespace in the beginning of a line 36 | words = &words[1..]; 37 | } 38 | } 39 | if words.is_empty() { 40 | return StyledLine::new(); 41 | } 42 | let mut start = 0; 43 | let iter = words.iter().cloned(); 44 | let mut line = StyledLine::from( 45 | iter.map(|word| { 46 | let (text, tag) = word.into_text(start); 47 | #[allow(clippy::single_match)] 48 | match tag { 49 | MetaTag::Heading(idx) => linked_regions.push_heading(idx, row, text.span()), 50 | _ => (), 51 | } 52 | start = text.span_end(); 53 | text 54 | }) 55 | .collect::>(), 56 | ); 57 | line.remove_trailing_whitespace(); 58 | line 59 | } 60 | 61 | /// StyledText is a Word with ColumnSpan in a line. 62 | fn into_text(self, start: usize) -> (StyledText, MetaTag) { 63 | let mut text = self.word; 64 | if self.trailling_whitespace { 65 | text.push(' '); 66 | } 67 | (StyledText::new(text, self.style, start), self.tag) 68 | } 69 | } 70 | 71 | impl fmt::Debug for Word { 72 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 73 | let mut s = f.debug_struct("Word"); 74 | s.field("word", &self.word); 75 | let style = Style::default(); 76 | if self.style != style { 77 | // if self.style.fg != style.fg { 78 | // s.field("style.fg", &self.style.fg); 79 | // } 80 | if self.style.add_modifier != style.add_modifier { 81 | s.field("style.add_modifier", &self.style.add_modifier); 82 | } 83 | } 84 | if !matches!(self.tag, MetaTag::Normal) { 85 | s.field("tag", &self.tag); 86 | } 87 | if self.trailling_whitespace { 88 | s.field("trailling_whitespace", &true); 89 | } 90 | s.finish() 91 | } 92 | } 93 | 94 | impl fmt::Display for Word { 95 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 96 | ::fmt(&self.word, f)?; 97 | if self.trailling_whitespace { 98 | f.write_char(' ')?; 99 | } 100 | Ok(()) 101 | } 102 | } 103 | 104 | impl Fragment for Word { 105 | /// word width without whitespace before or after 106 | fn width(&self) -> f64 { 107 | self.word.width() as f64 108 | } 109 | 110 | /// occurence of trailing whitespace, like 0 for CJK or 1 for latin etc 111 | fn whitespace_width(&self) -> f64 { 112 | if self.trailling_whitespace { 113 | 1.0 114 | } else { 115 | 0.0 116 | } 117 | } 118 | 119 | /// imaginary extra width after the non-line-end word that the wrapping algorithm accepts 120 | fn penalty_width(&self) -> f64 { 121 | 0.0 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/markdown/region.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] // TargetRegion is reserved for bidirection of referencd links 2 | use super::{ 3 | parse::{LinkTag, MetaTag}, 4 | wrapped::ColumnSpan, 5 | }; 6 | use smallvec::SmallVec; 7 | use std::cmp::Ordering; 8 | use term_rustdoc::util::{hashmap, HashMap, XString}; 9 | 10 | /// The selected texts will be rendered with original fg but grey bg. 11 | /// 12 | /// NOTE: the region is continuous across lines. 13 | #[derive(Clone, Default, Debug, Hash, PartialEq, Eq)] 14 | pub struct SelectedRegion { 15 | row_start: usize, 16 | row_end: usize, 17 | col_start: usize, 18 | col_end: usize, 19 | } 20 | 21 | impl SelectedRegion { 22 | pub fn row_start(&self) -> usize { 23 | self.row_start 24 | } 25 | 26 | fn new_same_line(row: usize, col: ColumnSpan) -> Self { 27 | let [start, end] = col.span(); 28 | SelectedRegion { 29 | row_start: row, 30 | row_end: row, 31 | col_start: start, 32 | col_end: end, 33 | } 34 | } 35 | 36 | /// Merge two regions into one continuous region. 37 | /// 38 | /// NOTE: usually merge them into one larger continuous region 39 | /// * this means it's not used to merge usage regions of links or footnotes, 40 | /// because they can scatter in discontinuous lines. 41 | fn merge_continuous(&mut self, new: SelectedRegion) { 42 | match self.row_start.cmp(&new.row_start) { 43 | Ordering::Greater => { 44 | self.row_start = new.row_start; 45 | self.col_start = new.col_start; 46 | } 47 | Ordering::Equal => self.col_start = self.col_start.min(new.col_start), 48 | Ordering::Less => {} 49 | } 50 | match self.row_end.cmp(&new.row_end) { 51 | Ordering::Less => { 52 | self.row_end = new.row_end; 53 | self.col_end = new.col_end; 54 | } 55 | Ordering::Equal => self.col_end = self.col_end.max(new.col_end), 56 | Ordering::Greater => {} 57 | } 58 | } 59 | } 60 | 61 | #[derive(Debug, Hash, PartialEq, Eq)] 62 | #[allow(dead_code)] 63 | pub enum RegionTag { 64 | // /// A continuous region on screen. 65 | // OnScreen(SelectedRegion), 66 | /// A string key like impl or key of a footnote. 67 | FootNote(XString), 68 | FootNoteSrc(XString), 69 | /// A referencd link id 70 | Link(usize), 71 | LinkSrc(usize), 72 | } 73 | 74 | /// Multiple SelectedRegions, but in most cases still single SelectedRegion. 75 | /// 76 | /// ReferenceLinks usually only have one linked region, but it's still common to 77 | /// have multiple linked regions. 78 | #[derive(Clone, Debug, Default)] 79 | pub struct TargetRegion { 80 | targets: SmallVec<[SelectedRegion; 1]>, 81 | } 82 | 83 | impl From for TargetRegion { 84 | fn from(region: SelectedRegion) -> Self { 85 | TargetRegion { 86 | targets: SmallVec::from([region]), 87 | } 88 | } 89 | } 90 | 91 | /// Regions that bidirect to each other. 92 | /// When the cursor or selection falls into a region, 93 | /// the regions in targets will be into the same background color. 94 | #[derive(Debug, Default)] 95 | pub struct LinkedRegions { 96 | tag: HashMap, 97 | heading: Vec<(usize, SelectedRegion)>, 98 | } 99 | 100 | impl LinkedRegions { 101 | pub fn new() -> LinkedRegions { 102 | LinkedRegions { 103 | tag: hashmap(8), 104 | heading: Vec::with_capacity(8), 105 | } 106 | } 107 | 108 | pub fn push_heading(&mut self, idx: usize, row: usize, col: ColumnSpan) { 109 | let region = SelectedRegion::new_same_line(row, col); 110 | // since the writer writes lines from top to bottom, headings will be in order 111 | if let Some((index, old)) = self.heading.last_mut() { 112 | if *index == idx { 113 | old.merge_continuous(region); 114 | return; 115 | } 116 | } 117 | self.heading.push((idx, region)); 118 | } 119 | 120 | pub fn take_headings(&mut self) -> Vec<(usize, SelectedRegion)> { 121 | std::mem::take(&mut self.heading) 122 | } 123 | } 124 | 125 | pub fn region_tag(tag: MetaTag) -> Option { 126 | match tag { 127 | MetaTag::Link(LinkTag::ReferenceLink(id)) => Some(RegionTag::Link(id)), 128 | MetaTag::Link(LinkTag::Footnote(key)) => Some(RegionTag::FootNote(key)), 129 | _ => None, 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/markdown/render.rs: -------------------------------------------------------------------------------- 1 | use super::fallback::{ScrollText, StyledLine}; 2 | use crate::ui::scrollable::generics::render_line; 3 | use ratatui::prelude::{Buffer, Rect}; 4 | 5 | impl ScrollText { 6 | pub fn render(&self, buf: &mut Buffer) { 7 | write_lines(&self.lines, self.start, self.area, buf); 8 | } 9 | } 10 | 11 | pub fn write_lines(lines: &[StyledLine], row_start: usize, rect: Rect, buf: &mut Buffer) { 12 | if lines.is_empty() { 13 | return; 14 | } 15 | let Rect { 16 | x, 17 | mut y, 18 | width, 19 | height, 20 | } = rect; 21 | let row_end = (row_start + height as usize).min(lines.len()); 22 | let width = width as usize; 23 | if let Some(lines) = lines.get(row_start..row_end) { 24 | for line in lines { 25 | render_line(line.iter_text_style(), buf, x, y, width); 26 | y += 1; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/markdown/wrapped.rs: -------------------------------------------------------------------------------- 1 | use ratatui::style::Style; 2 | use std::fmt; 3 | use term_rustdoc::{tree::Text, util::XString}; 4 | use unicode_width::UnicodeWidthStr; 5 | 6 | pub struct StyledText { 7 | text: Text, 8 | span: ColumnSpan, 9 | } 10 | 11 | #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] 12 | pub struct ColumnSpan { 13 | start: usize, 14 | end: usize, 15 | } 16 | 17 | impl ColumnSpan { 18 | pub fn span(self) -> [usize; 2] { 19 | [self.start, self.end] 20 | } 21 | } 22 | 23 | impl fmt::Debug for StyledText { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | ::fmt(self.as_str(), f) 26 | } 27 | } 28 | 29 | impl StyledText { 30 | pub fn new>(text: T, style: Style, start: usize) -> Self { 31 | let text = text.into(); 32 | let end = start + text.width(); 33 | StyledText { 34 | text: Text { text, style }, 35 | span: ColumnSpan { start, end }, 36 | } 37 | } 38 | 39 | pub fn text(&self) -> XString { 40 | self.text.text.clone() 41 | } 42 | 43 | pub fn as_str(&self) -> &str { 44 | &self.text.text 45 | } 46 | 47 | pub fn style(&self) -> Style { 48 | self.text.style 49 | } 50 | 51 | pub fn span_end(&self) -> usize { 52 | self.span.end 53 | } 54 | 55 | pub fn span(&self) -> ColumnSpan { 56 | self.span.clone() 57 | } 58 | 59 | pub fn remove_trailing_whitespace(&mut self) -> bool { 60 | if let Some(last) = self.text.text.pop() { 61 | if last == ' ' { 62 | return true; 63 | } 64 | self.text.text.push(last); 65 | } 66 | false 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/bin/ui/scrollable/render.rs: -------------------------------------------------------------------------------- 1 | use super::ScrollTreeLines; 2 | use ratatui::prelude::{Buffer, Color, Rect}; 3 | use term_rustdoc::tree::TreeLine; 4 | 5 | impl ScrollTreeLines { 6 | pub fn render(&self, buf: &mut Buffer) { 7 | // if no visible lines, we won't render anything 8 | let Some(visible) = self.visible_lines() else { 9 | return; 10 | }; 11 | 12 | let Rect { x, y, .. } = self.area; 13 | let width = self.area.width as usize; 14 | 15 | // render tree by each line 16 | write_lines(visible, buf, x, y, width); 17 | 18 | // render the current row 19 | if let Some(current_line) = self.get_line_of_current_cursor() { 20 | render_current_line(current_line, buf, x, y + self.cursor.y, width); 21 | } 22 | } 23 | } 24 | 25 | fn write_lines(lines: &[TreeLine], buf: &mut Buffer, x: u16, mut y: u16, width: usize) { 26 | for line in lines { 27 | render_line(line, buf, x, y, width); 28 | y += 1; 29 | } 30 | } 31 | 32 | fn render_line(line: &TreeLine, buf: &mut Buffer, x: u16, y: u16, width: usize) { 33 | let [(glyph, g_style), (name, n_style)] = line.glyph_name(); 34 | let (x_name, _) = buf.set_stringn(x, y, glyph, width, g_style); 35 | if let Some(remain) = width.checked_sub((x_name - x) as usize) { 36 | buf.set_stringn(x_name, y, name, remain, n_style); 37 | } 38 | } 39 | 40 | // Usually the line doesn't contain bg, thus highlight it by adding DarkGray bg on glyph 41 | // and inversing bg the name with Black fg. 42 | fn render_current_line(line: &TreeLine, buf: &mut Buffer, x: u16, y: u16, width: usize) { 43 | let [(glyph, g_style), (name, mut n_style)] = line.glyph_name(); 44 | let (x_name, _) = buf.set_stringn(x, y, glyph, width, g_style.bg(Color::DarkGray)); 45 | n_style.bg = n_style.fg; 46 | n_style.fg = Some(Color::Black); 47 | if let Some(remain) = width.checked_sub((x_name - x) as usize) { 48 | buf.set_stringn(x_name, y, name, remain, n_style); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/bin/ui/surround.rs: -------------------------------------------------------------------------------- 1 | use crate::ui::scrollable::render_line; 2 | use ratatui::{ 3 | prelude::{Buffer, Rect, Style, Widget}, 4 | widgets::Block, 5 | }; 6 | use unicode_width::UnicodeWidthStr; 7 | 8 | #[derive(Default, Debug)] 9 | pub struct Surround { 10 | block: Block<'static>, 11 | area: Rect, 12 | } 13 | 14 | impl Surround { 15 | pub fn new(block: Block<'static>, area: Rect) -> Self { 16 | Surround { block, area } 17 | } 18 | 19 | pub fn inner(&self) -> Rect { 20 | self.block.inner(self.area) 21 | } 22 | 23 | pub fn block_mut(&mut self) -> &mut Block<'static> { 24 | &mut self.block 25 | } 26 | 27 | pub fn render(&self, buf: &mut Buffer) { 28 | (&self.block).render(self.area, buf); 29 | } 30 | 31 | pub fn area(&self) -> Rect { 32 | self.area 33 | } 34 | 35 | /// Update the border area and then return inner area only when the outer areas differ. 36 | pub fn update_area(&mut self, area: Rect) -> Option { 37 | if self.area == area { 38 | return None; 39 | } 40 | self.area = area; 41 | Some(self.inner()) 42 | } 43 | 44 | pub fn render_only_bottom_right_text(&self, buf: &mut Buffer, text: &str) -> usize { 45 | let area = self.area; 46 | let text_width = text.width(); 47 | if let Some(offset) = (area.width as usize).checked_sub(2 + text_width) { 48 | let x = area.x + offset as u16; 49 | let y = area.y + area.height - 1; 50 | render_line(Some((text, Style::new())), buf, x, y, text_width); 51 | return text_width + 2; 52 | } 53 | 0 54 | } 55 | 56 | pub fn render_only_bottom_left_text(&self, buf: &mut Buffer, text: &str, used: usize) { 57 | let area = self.area; 58 | if let Some(rest) = (area.width as usize).checked_sub(2 + used) { 59 | if rest < text.width() { 60 | // not enought space to show 61 | return; 62 | } 63 | let x = area.x + 2; 64 | let y = area.y + area.height - 1; 65 | render_line(Some((text, Style::new())), buf, x, y, rest); 66 | } 67 | } 68 | 69 | pub fn render_only_top_left_text(&self, buf: &mut Buffer, text: &str, used: usize) { 70 | let area = self.area; 71 | if let Some(rest) = (area.width as usize).checked_sub(2 + used) { 72 | if rest < text.width() { 73 | // not enought space to show 74 | return; 75 | } 76 | let x = area.x + 2; 77 | let y = area.y; 78 | render_line(Some((text, Style::new())), buf, x, y, rest); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate tracing; 3 | 4 | /// documentation tree 5 | pub mod tree; 6 | 7 | /// format Type and Path as string 8 | pub mod type_name; 9 | 10 | /// common public utils 11 | pub mod util; 12 | -------------------------------------------------------------------------------- /src/tree/impls/debug.rs: -------------------------------------------------------------------------------- 1 | use super::super::*; 2 | use std::fmt::{self, Debug}; 3 | 4 | /// skip formatting the field when the value is empty or false 5 | macro_rules! skip_fmt { 6 | ($base:ident, $self:ident . $($field:ident)+ ) => {$( 7 | if !$self.$field.is_empty() { 8 | $base.field(::std::stringify!($field), &$self.$field); 9 | } 10 | )+}; 11 | (bool: $base:ident, $self:ident . $($field:ident)+ ) => {$( 12 | if $self.$field { 13 | $base.field(::std::stringify!($field), &$self.$field); 14 | } 15 | )+}; 16 | (option: $base:ident, $self:ident . $($field:ident)+ ) => {$( 17 | if $self.$field.is_some() { 18 | $base.field(::std::stringify!($field), &$self.$field); 19 | } 20 | )+}; 21 | (0: $base:ident, $self:ident . $($field:ident)+ ) => {$( 22 | if $self.$field != 0 { 23 | $base.field(::std::stringify!($field), &$self.$field); 24 | } 25 | )+}; 26 | } 27 | 28 | impl Debug for DModule { 29 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 30 | let mut base = f.debug_struct("DModule"); 31 | base.field("id", &self.id); 32 | skip_fmt!( 33 | base, self . modules structs unions enums 34 | functions traits constants statics type_alias 35 | macros_decl macros_func macros_attr macros_derv 36 | ); 37 | base.finish() 38 | } 39 | } 40 | 41 | impl Debug for DImpl { 42 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 43 | let mut base = f.debug_struct("DImpl"); 44 | skip_fmt!(base, self . inherent trait_ auto blanket); 45 | base.finish() 46 | } 47 | } 48 | 49 | impl Debug for DImplInner { 50 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 51 | let mut base = f.debug_struct("DImplInner"); 52 | base.field("id", &self.id); 53 | skip_fmt!(base, self . functions constants types); 54 | base.finish() 55 | } 56 | } 57 | 58 | impl Debug for DStruct { 59 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 60 | let mut base = f.debug_struct("DStruct"); 61 | base.field("id", &self.id); 62 | skip_fmt!(bool: base, self.contain_private_fields); 63 | skip_fmt!(base, self . fields impls); 64 | base.finish() 65 | } 66 | } 67 | 68 | impl Debug for DUnion { 69 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 70 | let mut base = f.debug_struct("DUnion"); 71 | base.field("id", &self.id); 72 | skip_fmt!( 73 | base, self . fields impls 74 | ); 75 | base.finish() 76 | } 77 | } 78 | 79 | impl Debug for DEnum { 80 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 81 | let mut base = f.debug_struct("DEnum"); 82 | base.field("id", &self.id); 83 | skip_fmt!(base, self . variants impls); 84 | base.finish() 85 | } 86 | } 87 | 88 | impl Debug for DTrait { 89 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 90 | let mut base = f.debug_struct("DTrait"); 91 | base.field("id", &self.id); 92 | skip_fmt!(base, self . types constants functions implementations); 93 | base.finish() 94 | } 95 | } 96 | 97 | impl Debug for ItemCount { 98 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 99 | let mut base = f.debug_struct("ItemCount"); 100 | skip_fmt!( 101 | 0: base, self . modules structs unions enums functions 102 | traits constants statics type_alias 103 | macros_decl macros_func macros_attr macros_derv 104 | ); 105 | base.finish() 106 | } 107 | } 108 | 109 | impl Debug for ImplCount { 110 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 111 | let mut base = f.debug_struct("ImplCount"); 112 | if self.total != 0 { 113 | base.field("kind", &self.kind); 114 | base.field("total", &self.total); 115 | skip_fmt!(0: base, self . structs enums unions); 116 | } 117 | base.finish() 118 | } 119 | } 120 | 121 | impl Debug for ImplCounts { 122 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 123 | let mut base = f.debug_struct("ImplCounts"); 124 | if self.total.total != 0 { 125 | base.field("total", &self.total); 126 | if self.inherent.total != 0 { 127 | base.field("inherent", &self.inherent); 128 | } 129 | if self.trait_.total != 0 { 130 | base.field("trait", &self.trait_); 131 | } 132 | } 133 | base.finish() 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/tree/impls/mod.rs: -------------------------------------------------------------------------------- 1 | mod debug; 2 | 3 | #[macro_use] 4 | pub mod show; 5 | -------------------------------------------------------------------------------- /src/tree/mod.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod impls; 3 | // The inner macro `icon!` can be used afterwards in submods 4 | 5 | mod id; 6 | mod nodes; 7 | mod stats; 8 | mod tag; 9 | mod textline; 10 | 11 | use rustdoc_types::Crate; 12 | use std::{fmt, ops::Deref, rc::Rc}; 13 | 14 | pub use id::{IDMap, IDs, IndexMap, PathMap}; 15 | pub use impls::show::{DocTree, Show}; 16 | pub use nodes::{ 17 | DConstant, DEnum, DFunction, DImpl, DImplInner, DMacroAttr, DMacroDecl, DMacroDerv, DMacroFunc, 18 | DModule, DStatic, DStruct, DTrait, DTypeAlias, DUnion, DataItemKind, 19 | }; 20 | pub use stats::{ImplCount, ImplCounts, ImplKind, ItemCount}; 21 | pub use tag::Tag; 22 | pub use textline::{Text, TextTag, TreeLine, TreeLines}; 23 | 24 | /// This should be the main data structure to refer to documentation 25 | /// and the items tree structure in public modules. 26 | /// 27 | /// It's cheap to clone and use a ID buffer to avoid the cost of generating a new string in query. 28 | #[derive(Clone, Default, serde::Serialize, serde::Deserialize)] 29 | pub struct CrateDoc { 30 | inner: Rc, 31 | } 32 | 33 | impl CrateDoc { 34 | pub fn new(doc: Crate) -> CrateDoc { 35 | CrateDoc { 36 | inner: Rc::new(IDMap::new(doc)), 37 | } 38 | } 39 | } 40 | 41 | impl Deref for CrateDoc { 42 | type Target = IDMap; 43 | 44 | fn deref(&self) -> &Self::Target { 45 | &self.inner 46 | } 47 | } 48 | 49 | impl fmt::Debug for CrateDoc { 50 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 51 | write!(f, "CrateDoc for {}", self.name(&self.dmodule().id)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/tree/nodes/enums.rs: -------------------------------------------------------------------------------- 1 | use crate::tree::{ 2 | impls::show::{show_ids, DocTree, Show}, 3 | DImpl, IDMap, IDs, 4 | }; 5 | use rustdoc_types::{Enum, Id}; 6 | 7 | #[derive(serde::Serialize, serde::Deserialize, Clone)] 8 | pub struct DEnum { 9 | pub id: Id, 10 | pub variants: IDs, 11 | // variants_stripped: bool, -> Does this really make sense? 12 | pub impls: DImpl, 13 | } 14 | impl DEnum { 15 | pub fn new(id: Id, item: &Enum, map: &IDMap) -> Self { 16 | DEnum { 17 | id, 18 | variants: item.variants.clone().into_boxed_slice(), 19 | impls: DImpl::new(&item.impls, map), 20 | } 21 | } 22 | 23 | /// External items need external crates compiled to know details, 24 | /// and the ID here is for PathMap, not IndexMap. 25 | pub fn new_external(id: Id) -> Self { 26 | let (variants, impls) = Default::default(); 27 | DEnum { 28 | id, 29 | variants, 30 | impls, 31 | } 32 | } 33 | 34 | pub fn variants_tree(&self, map: &IDMap) -> DocTree { 35 | let mut root = node!(Enum: map, self.id); 36 | names_node!(@iter self map root 37 | variants Variant 38 | ); 39 | root 40 | } 41 | } 42 | 43 | impl Show for DEnum { 44 | fn show(&self) -> DocTree { 45 | "[enum]".show().with_leaves([ 46 | "Variants".show().with_leaves(show_ids(&self.variants)), 47 | self.impls.show(), 48 | ]) 49 | } 50 | 51 | fn show_prettier(&self, map: &IDMap) -> DocTree { 52 | let variants = names_node!(@single 53 | self map NoVariants, 54 | Variants variants Variant 55 | ); 56 | node!(Enum: map, self.id).with_leaves([variants, self.impls.show_prettier(map)]) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/tree/nodes/imports.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use rustdoc_types::{ItemEnum, ItemKind, Use}; 3 | 4 | /// Add the item of `pub use source {as name}` to DModule. 5 | /// 6 | /// ## Note 7 | /// 8 | /// pub-use item shouldn't be a real tree node because the source 9 | /// can be any other item which should be merged into one of DModule's fields. 10 | pub(super) fn parse_import( 11 | id: Id, 12 | import: &Use, 13 | map: &IDMap, 14 | dmod: &mut DModule, 15 | kin: &mut Vec, 16 | ) { 17 | let Some(import_id) = import.id else { return }; 18 | // Import's id can be empty when the source is Primitive. 19 | if let Some(source) = map.indexmap().get(&import_id) { 20 | match &source.inner { 21 | ItemEnum::Module(item) => { 22 | // check id for ItemSummary/path existence: 23 | // reexported modules are not like other normal items, 24 | // they can recursively points to each other, causing stack overflows. 25 | // RUST_LOG=term_rustdoc::tree::nodes=debug can be used to quickly check the logs. 26 | match map.path_or_name(&id) { 27 | Ok(path) => { 28 | // usual reexported module 29 | debug!("Push down the reexported `{path}` module."); 30 | dmod.modules 31 | .push(DModule::new_inner(id, &item.items, map, kin)) 32 | } 33 | Err(name) if !kin.contains(&id) => { 34 | // Unusual reexported module: copy the items inside until a duplicate 35 | // of ancestor module (if any). 36 | debug!("Push down the reexported `{name}` module that is not found in PathMap."); 37 | dmod.modules 38 | .push(DModule::new_inner(id, &item.items, map, &mut kin.clone())) 39 | } 40 | Err(name) => warn!( 41 | "Stop at the reexported `{name}` module that duplicates as an ancestor module.\n\ 42 | Ancestors before this module is {:?}", 43 | kin.iter().map(|id| map.path(id)).collect::>() 44 | ), 45 | } 46 | } 47 | ItemEnum::Union(item) => dmod.unions.push(DUnion::new(id, item, map)), 48 | ItemEnum::Struct(item) => dmod.structs.push(DStruct::new(id, item, map)), 49 | ItemEnum::Enum(item) => dmod.enums.push(DEnum::new(id, item, map)), 50 | ItemEnum::Trait(item) => dmod.traits.push(DTrait::new(id, item, map)), 51 | ItemEnum::Function(_) => dmod.functions.push(DFunction::new(id)), 52 | ItemEnum::TypeAlias(_) => dmod.type_alias.push(DTypeAlias::new(id)), 53 | ItemEnum::Constant { .. } => dmod.constants.push(DConstant::new(id)), 54 | ItemEnum::Static(_) => dmod.statics.push(DStatic::new(id)), 55 | ItemEnum::Macro(_) => dmod.macros_decl.push(DMacroDecl::new(id)), 56 | ItemEnum::ProcMacro(proc) => match proc.kind { 57 | MacroKind::Bang => dmod.macros_func.push(DMacroFunc::new(id)), 58 | MacroKind::Attr => dmod.macros_attr.push(DMacroAttr::new(id)), 59 | MacroKind::Derive => dmod.macros_derv.push(DMacroDerv::new(id)), 60 | }, 61 | _ => (), 62 | } 63 | } else if let Some(extern_item) = map.pathmap().get(&import_id) { 64 | let id = import_id; 65 | // TODO: external items are in path map, which means no further information 66 | // except full path and item kind will be known. 67 | // To get details of an external item, we need to compile the external crate, 68 | // and search it with the full path and kind. 69 | // A simple example of this is `nucleo` crate. 70 | match extern_item.kind { 71 | ItemKind::Module if !kin.contains(&id) => { 72 | // We don't know items inside external modules. 73 | debug!( 74 | "Push down the reexported external `{}` without inner items.", 75 | extern_item.path.join("::") 76 | ); 77 | dmod.modules.push(DModule::new_external(id)) 78 | } 79 | ItemKind::Struct => dmod.structs.push(DStruct::new_external(id)), 80 | ItemKind::Union => dmod.unions.push(DUnion::new_external(id)), 81 | ItemKind::Enum => dmod.enums.push(DEnum::new_external(id)), 82 | ItemKind::Function => dmod.functions.push(DFunction::new(id)), 83 | ItemKind::TypeAlias => dmod.type_alias.push(DTypeAlias::new(id)), 84 | ItemKind::Constant => dmod.constants.push(DConstant::new(id)), 85 | ItemKind::Trait => dmod.traits.push(DTrait::new_external(id)), 86 | ItemKind::Static => dmod.statics.push(DStatic::new(id)), 87 | ItemKind::Macro => dmod.macros_decl.push(DMacroDecl::new(id)), 88 | ItemKind::ProcAttribute => dmod.macros_attr.push(DMacroAttr::new(id)), 89 | ItemKind::ProcDerive => dmod.macros_derv.push(DMacroDerv::new(id)), 90 | ItemKind::Primitive => (), 91 | _ => (), 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/tree/nodes/item_inner.rs: -------------------------------------------------------------------------------- 1 | use super::{DEnum, DModule, DStruct, DTrait, DUnion}; 2 | use crate::tree::{DocTree, IDMap, Show}; 3 | use rustdoc_types::{Id, ItemEnum}; 4 | 5 | /// Data-carrying items that provide extra tree layer on fields/variants/impls. 6 | #[derive(Debug, Clone, Copy)] 7 | pub enum DataItemKind { 8 | Module, 9 | Struct, 10 | Enum, 11 | Trait, 12 | Union, 13 | } 14 | 15 | impl DataItemKind { 16 | pub fn new(id: &Id, map: &IDMap) -> Option { 17 | map.get_item(id).and_then(|item| { 18 | Some(match &item.inner { 19 | ItemEnum::Module(_) => DataItemKind::Module, 20 | ItemEnum::Struct(_) => DataItemKind::Struct, 21 | ItemEnum::Enum(_) => DataItemKind::Enum, 22 | ItemEnum::Trait(_) => DataItemKind::Trait, 23 | ItemEnum::Union(_) => DataItemKind::Union, 24 | ItemEnum::Use(reexport) => { 25 | let id = &reexport.id?; 26 | DataItemKind::new(id, map)? 27 | } 28 | _ => return None, 29 | }) 30 | }) 31 | } 32 | } 33 | 34 | macro_rules! search { 35 | () => { 36 | search! { 37 | search_for_struct structs DStruct, 38 | search_for_enum enums DEnum, 39 | search_for_trait traits DTrait, 40 | search_for_union unions DUnion, 41 | } 42 | }; 43 | ($fname:ident $field:ident $typ:ident) => { 44 | fn $fname( 45 | &self, 46 | id: &Id, 47 | f: impl Copy + Fn(&$typ) -> T, 48 | ) -> Option { 49 | for item in &self.$field { 50 | if item.id == *id { 51 | return Some(f(item)); 52 | } 53 | } 54 | for m in &self.modules { 55 | let tree = m.$fname(id, f); 56 | if tree.is_some() { 57 | return tree; 58 | } 59 | } 60 | None 61 | } 62 | }; 63 | ($($fname:ident $field:ident $typ:ident,)+) => { 64 | impl DModule { $( search! { $fname $field $typ } )+ } 65 | }; 66 | } 67 | 68 | search! {} 69 | 70 | // Search after the kind is known to improve efficiency. 71 | impl DModule { 72 | fn search_for_module(&self, id: &Id, f: impl Copy + Fn(&DModule) -> T) -> Option { 73 | if self.id == *id { 74 | return Some(f(self)); 75 | } 76 | for m in &self.modules { 77 | let tree = m.search_for_module(id, f); 78 | if tree.is_some() { 79 | return tree; 80 | } 81 | } 82 | None 83 | } 84 | 85 | pub fn item_inner_tree(&self, id: &Id, map: &IDMap) -> Option { 86 | let kind = DataItemKind::new(id, map)?; 87 | match kind { 88 | DataItemKind::Struct => self.search_for_struct(id, |x| x.show_prettier(map)), 89 | DataItemKind::Enum => self.search_for_enum(id, |x| x.show_prettier(map)), 90 | DataItemKind::Trait => self.search_for_trait(id, |x| x.show_prettier(map)), 91 | DataItemKind::Union => self.search_for_union(id, |x| x.show_prettier(map)), 92 | DataItemKind::Module => self.search_for_module(id, |x| x.item_tree(map)), 93 | } 94 | } 95 | 96 | pub fn impl_tree(&self, id: &Id, map: &IDMap) -> Option { 97 | let kind = DataItemKind::new(id, map)?; 98 | match kind { 99 | DataItemKind::Struct => self.search_for_struct(id, |x| x.impls.show_prettier(map)), 100 | DataItemKind::Enum => self.search_for_enum(id, |x| x.impls.show_prettier(map)), 101 | DataItemKind::Trait => self.search_for_trait(id, |x| x.show_prettier(map)), 102 | DataItemKind::Union => self.search_for_union(id, |x| x.impls.show_prettier(map)), 103 | _ => None, 104 | } 105 | } 106 | 107 | pub fn implementor_tree(&self, id: &Id, map: &IDMap) -> Option { 108 | self.search_for_trait(id, |x| x.implementors(map)) 109 | } 110 | 111 | pub fn associated_item_tree(&self, id: &Id, map: &IDMap) -> Option { 112 | self.search_for_trait(id, |x| x.associated_items(map)) 113 | } 114 | 115 | pub fn field_tree(&self, id: &Id, map: &IDMap) -> Option { 116 | let kind = DataItemKind::new(id, map)?; 117 | match kind { 118 | DataItemKind::Struct => self.search_for_struct(id, |x| x.fields_tree(map)), 119 | DataItemKind::Enum => self.search_for_enum(id, |x| x.variants_tree(map)), 120 | DataItemKind::Union => self.search_for_union(id, |x| x.fields_tree(map)), 121 | _ => None, 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/tree/nodes/structs.rs: -------------------------------------------------------------------------------- 1 | use crate::tree::{ 2 | impls::show::{show_ids, show_names, DocTree, Show}, 3 | DImpl, IDMap, IDs, Tag, 4 | }; 5 | use rustdoc_types::{Id, Struct, StructKind}; 6 | 7 | #[derive(serde::Serialize, serde::Deserialize, Clone)] 8 | pub struct DStruct { 9 | pub id: Id, 10 | pub fields: IDs, 11 | pub contain_private_fields: bool, 12 | pub impls: DImpl, 13 | } 14 | 15 | impl DStruct { 16 | pub fn new(id: Id, item: &Struct, map: &IDMap) -> Self { 17 | let mut contain_private_fields = false; 18 | let fields = match &item.kind { 19 | StructKind::Unit => IDs::default(), 20 | StructKind::Tuple(fields) => fields 21 | .iter() 22 | .filter_map(|&id| { 23 | if id.is_none() { 24 | contain_private_fields = true; 25 | } 26 | id 27 | }) 28 | .collect(), 29 | StructKind::Plain { 30 | fields, 31 | has_stripped_fields, 32 | } => { 33 | contain_private_fields = *has_stripped_fields; 34 | fields.clone().into_boxed_slice() 35 | } 36 | }; 37 | let impls = DImpl::new(&item.impls, map); 38 | DStruct { 39 | id, 40 | fields, 41 | contain_private_fields, 42 | impls, 43 | } 44 | } 45 | 46 | /// External items need external crates compiled to know details, 47 | /// and the ID here is for PathMap, not IndexMap. 48 | pub fn new_external(id: Id) -> Self { 49 | let (fields, impls) = Default::default(); 50 | DStruct { 51 | id, 52 | fields, 53 | impls, 54 | contain_private_fields: true, 55 | } 56 | } 57 | 58 | pub fn fields_tree(&self, map: &IDMap) -> DocTree { 59 | let mut root = node!(Struct: map, self.id); 60 | match (self.fields.len(), self.contain_private_fields) { 61 | (0, true) => root.push(private_fields()), 62 | (0, false) => root.push(Tag::NoFields.show()), 63 | (_, true) => { 64 | root.extend(show_names(&*self.fields, Tag::Field, map).chain([private_fields()])) 65 | } 66 | (_, false) => root.extend(show_names(&*self.fields, Tag::Field, map)), 67 | }; 68 | root 69 | } 70 | } 71 | 72 | fn private_fields() -> DocTree { 73 | Tag::FieldsPrivate.show() 74 | } 75 | 76 | fn fields_root() -> DocTree { 77 | Tag::Fields.show() 78 | } 79 | 80 | impl Show for DStruct { 81 | fn show(&self) -> DocTree { 82 | format!("[struct] {:?}", self.id).show().with_leaves([ 83 | "Fields".show().with_leaves( 84 | show_ids(&self.fields).chain(self.contain_private_fields.then(private_fields)), 85 | ), 86 | self.impls.show(), 87 | ]) 88 | } 89 | 90 | fn show_prettier(&self, map: &IDMap) -> DocTree { 91 | let mut root = node!(Struct: map, self.id); 92 | match (self.fields.len(), self.contain_private_fields) { 93 | (0, true) => root.push(private_fields()), 94 | (0, false) => root.push(Tag::NoFields.show()), 95 | (_, true) => { 96 | root.push(fields_root().with_leaves( 97 | show_names(&*self.fields, Tag::Field, map).chain([private_fields()]), 98 | )) 99 | } 100 | (_, false) => { 101 | root.push(fields_root().with_leaves(show_names(&*self.fields, Tag::Field, map))) 102 | } 103 | }; 104 | root.push(self.impls.show_prettier(map)); 105 | root 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/tree/nodes/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::tree::{ 2 | impls::show::{show_ids, DocTree, Show}, 3 | IDMap, IDs, Tag, 4 | }; 5 | use rustdoc_types::{Id, ItemEnum, Trait}; 6 | 7 | #[derive(serde::Serialize, serde::Deserialize, Clone)] 8 | pub struct DTrait { 9 | pub id: Id, 10 | pub types: IDs, 11 | pub constants: IDs, 12 | pub functions: IDs, 13 | pub implementations: IDs, 14 | } 15 | impl DTrait { 16 | pub fn new(id: Id, item: &Trait, map: &IDMap) -> Self { 17 | let [mut types, mut constants, mut functions]: [Vec; 3] = Default::default(); 18 | let trait_id = &id; 19 | for &id in &item.items { 20 | if let Some(assoc) = map.get_item(&id) { 21 | match &assoc.inner { 22 | ItemEnum::AssocType { .. } => types.push(id), 23 | ItemEnum::AssocConst { .. } => constants.push(id), 24 | ItemEnum::Function(_) => functions.push(id), 25 | _ => warn!( 26 | "`{}` ({id:?}) should refer to an associated item \ 27 | (type/constant/function) in Trait `{}` ({trait_id:?})", 28 | map.name(&id), 29 | map.name(trait_id) 30 | ), 31 | } 32 | } else { 33 | warn!( 34 | "the trait item `{}` ({id:?}) not found in Crate's index", 35 | map.name(&id) 36 | ); 37 | } 38 | } 39 | types.sort_unstable_by_key(|id| map.name(id)); 40 | constants.sort_unstable_by_key(|id| map.name(id)); 41 | functions.sort_unstable_by_key(|id| map.name(id)); 42 | DTrait { 43 | id, 44 | types: types.into(), 45 | constants: constants.into(), 46 | functions: functions.into(), 47 | implementations: item.implementations.clone().into_boxed_slice(), 48 | } 49 | } 50 | 51 | /// External items need external crates compiled to know details, 52 | /// and the ID here is for PathMap, not IndexMap. 53 | pub fn new_external(id: Id) -> Self { 54 | let (types, constants, functions, implementations) = Default::default(); 55 | DTrait { 56 | id, 57 | types, 58 | constants, 59 | functions, 60 | implementations, 61 | } 62 | } 63 | 64 | pub fn associated_items(&self, map: &IDMap) -> DocTree { 65 | let mut root = node!(Trait: map, self.id); 66 | names_node!(@iter self map root 67 | constants AssocConst, 68 | types AssocType, 69 | functions AssocFn, 70 | ); 71 | root 72 | } 73 | 74 | pub fn implementors(&self, map: &IDMap) -> DocTree { 75 | let mut root = node!(Trait: map, self.id); 76 | names_node!(@iter self map root 77 | implementations Implementor, 78 | ); 79 | root 80 | } 81 | } 82 | 83 | impl Show for DTrait { 84 | fn show(&self) -> DocTree { 85 | format!("[trait] {:?}", self.id).show().with_leaves([ 86 | "Associated Constants" 87 | .show() 88 | .with_leaves(show_ids(&self.constants)), 89 | "Associated Types".show().with_leaves(show_ids(&self.types)), 90 | "Associated Functions" 91 | .show() 92 | .with_leaves(show_ids(&self.functions)), 93 | "Implementors" 94 | .show() 95 | .with_leaves(show_ids(&self.implementations)), 96 | ]) 97 | } 98 | 99 | fn show_prettier(&self, map: &IDMap) -> DocTree { 100 | let root = node!(Trait: map, self.id); 101 | let leaves = names_node!( 102 | self map root.with_leaves([Tag::NoAssocOrImpls.show()]), 103 | AssocConsts constants AssocConst, 104 | AssocTypes types AssocType, 105 | AssocFns functions AssocFn, 106 | Implementors implementations Implementor, 107 | ); 108 | root.with_leaves(leaves) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/tree/nodes/unions.rs: -------------------------------------------------------------------------------- 1 | use crate::tree::{ 2 | impls::show::{show_ids, DocTree, Show}, 3 | DImpl, IDMap, IDs, 4 | }; 5 | use rustdoc_types::{Id, Union}; 6 | 7 | #[derive(serde::Serialize, serde::Deserialize, Clone)] 8 | pub struct DUnion { 9 | pub id: Id, 10 | pub fields: IDs, 11 | pub impls: DImpl, 12 | } 13 | impl DUnion { 14 | pub fn new(id: Id, item: &Union, map: &IDMap) -> Self { 15 | DUnion { 16 | id, 17 | fields: item.fields.clone().into_boxed_slice(), 18 | impls: DImpl::new(&item.impls, map), 19 | } 20 | } 21 | 22 | /// External items need external crates compiled to know details, 23 | /// and the ID here is for PathMap, not IndexMap. 24 | pub fn new_external(id: Id) -> Self { 25 | let (fields, impls) = Default::default(); 26 | DUnion { id, fields, impls } 27 | } 28 | 29 | pub fn fields_tree(&self, map: &IDMap) -> DocTree { 30 | let mut root = node!(Union: map, self.id); 31 | names_node!(@iter self map root 32 | fields Field 33 | ); 34 | root 35 | } 36 | } 37 | 38 | impl Show for DUnion { 39 | fn show(&self) -> DocTree { 40 | format!("[union] {:?}", self.id).show().with_leaves([ 41 | "Fields".show().with_leaves(show_ids(&self.fields)), 42 | self.impls.show(), 43 | ]) 44 | } 45 | 46 | fn show_prettier(&self, map: &IDMap) -> DocTree { 47 | let fields = names_node!(@single 48 | self map NoFields, 49 | Fields fields Field 50 | ); 51 | node!(Union: map, self.id).with_leaves([fields, self.impls.show_prettier(map)]) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/tree/stats/impls.rs: -------------------------------------------------------------------------------- 1 | use super::{ImplCount, ImplCounts, ImplKind, ItemCount}; 2 | use std::ops::{Add, AddAssign}; 3 | 4 | pub fn acc_sum(mut acc: T, new: T) -> T { 5 | acc += new; 6 | acc 7 | } 8 | 9 | impl Add for ItemCount { 10 | type Output = ItemCount; 11 | 12 | fn add(self, rhs: Self) -> Self::Output { 13 | ItemCount { 14 | modules: self.modules + rhs.modules, 15 | structs: self.structs + rhs.structs, 16 | unions: self.unions + rhs.unions, 17 | enums: self.enums + rhs.enums, 18 | functions: self.functions + rhs.functions, 19 | traits: self.traits + rhs.traits, 20 | constants: self.constants + rhs.constants, 21 | statics: self.statics + rhs.statics, 22 | type_alias: self.type_alias + rhs.type_alias, 23 | macros_decl: self.macros_decl + rhs.macros_decl, 24 | macros_func: self.macros_func + rhs.macros_func, 25 | macros_attr: self.macros_attr + rhs.macros_attr, 26 | macros_derv: self.macros_derv + rhs.macros_derv, 27 | } 28 | } 29 | } 30 | 31 | impl Add for ImplKind { 32 | type Output = ImplKind; 33 | 34 | fn add(self, rhs: Self) -> Self::Output { 35 | match (self, rhs) { 36 | (ImplKind::Inherent, ImplKind::Inherent) => ImplKind::Inherent, 37 | (ImplKind::Trait, ImplKind::Trait) => ImplKind::Trait, 38 | _ => ImplKind::Both, 39 | } 40 | } 41 | } 42 | 43 | impl AddAssign for ImplKind { 44 | fn add_assign(&mut self, rhs: Self) { 45 | *self = match (&self, rhs) { 46 | (ImplKind::Inherent, ImplKind::Inherent) => ImplKind::Inherent, 47 | (ImplKind::Trait, ImplKind::Trait) => ImplKind::Trait, 48 | _ => ImplKind::Both, 49 | }; 50 | } 51 | } 52 | 53 | impl AddAssign for ImplCount { 54 | fn add_assign(&mut self, rhs: Self) { 55 | self.structs += rhs.structs; 56 | self.enums += rhs.enums; 57 | self.unions += rhs.unions; 58 | self.kind += rhs.kind; 59 | self.total += rhs.total; 60 | } 61 | } 62 | 63 | impl Add for ImplCount { 64 | type Output = ImplCount; 65 | 66 | fn add(self, rhs: Self) -> Self::Output { 67 | ImplCount { 68 | structs: self.structs + rhs.structs, 69 | enums: self.enums + rhs.enums, 70 | unions: self.unions + rhs.unions, 71 | kind: self.kind + rhs.kind, 72 | total: self.total + rhs.total, 73 | } 74 | } 75 | } 76 | 77 | impl AddAssign for ItemCount { 78 | fn add_assign(&mut self, rhs: Self) { 79 | self.modules += rhs.modules; 80 | self.structs += rhs.structs; 81 | self.unions += rhs.unions; 82 | self.enums += rhs.enums; 83 | self.functions += rhs.functions; 84 | self.traits += rhs.traits; 85 | self.constants += rhs.constants; 86 | self.statics += rhs.statics; 87 | self.type_alias += rhs.type_alias; 88 | self.macros_decl += rhs.macros_decl; 89 | self.macros_func += rhs.macros_func; 90 | self.macros_attr += rhs.macros_attr; 91 | self.macros_derv += rhs.macros_derv; 92 | } 93 | } 94 | 95 | impl Add for ImplCounts { 96 | type Output = ImplCounts; 97 | 98 | fn add(self, rhs: Self) -> Self::Output { 99 | ImplCounts { 100 | inherent: self.inherent + rhs.trait_, 101 | trait_: self.trait_ + rhs.trait_, 102 | total: self.total + rhs.total, 103 | } 104 | } 105 | } 106 | 107 | impl AddAssign for ImplCounts { 108 | fn add_assign(&mut self, rhs: Self) { 109 | self.inherent += rhs.trait_; 110 | self.trait_ += rhs.trait_; 111 | self.total += rhs.total; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/type_name/mod.rs: -------------------------------------------------------------------------------- 1 | mod render; 2 | pub(crate) mod style; 3 | 4 | pub use render::{DeclarationLine, DeclarationLines, TextTag}; 5 | pub use style::StyledType; 6 | -------------------------------------------------------------------------------- /src/type_name/render/mod.rs: -------------------------------------------------------------------------------- 1 | use rustdoc_types::Id; 2 | 3 | use super::style::{StyledType, Tag}; 4 | use crate::{ 5 | tree::IDMap, 6 | type_name::style::{Punctuation, Symbol}, 7 | util::XString, 8 | }; 9 | use std::{fmt, mem}; 10 | 11 | #[derive(Clone, Default)] 12 | pub struct DeclarationLines { 13 | lines: Vec, 14 | } 15 | 16 | impl fmt::Debug for DeclarationLines { 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | for line in &self.lines { 19 | _ = writeln!(f, "{line:?}"); 20 | } 21 | Ok(()) 22 | } 23 | } 24 | 25 | impl std::ops::Deref for DeclarationLines { 26 | type Target = [DeclarationLine]; 27 | 28 | fn deref(&self) -> &Self::Target { 29 | &self.lines 30 | } 31 | } 32 | 33 | impl From<&StyledType> for DeclarationLines { 34 | fn from(value: &StyledType) -> Self { 35 | Self::new_(value) 36 | } 37 | } 38 | 39 | impl DeclarationLines { 40 | pub fn new(id: &Id, map: &IDMap) -> Self { 41 | Self::new_(&StyledType::new(id, map)) 42 | } 43 | 44 | fn new_(styled_type: &StyledType) -> Self { 45 | let tags = styled_type.tags(); 46 | let mut lines = Vec::with_capacity(8); 47 | let mut line = Vec::with_capacity(8); 48 | let mut iter = tags.iter(); 49 | let mut text_tag = TextTag::default(); 50 | while let Some(tag) = iter.next() { 51 | match tag { 52 | Tag::Name(s) => text_tag.text.push_str(s), 53 | Tag::Decl(s) => text_tag.text.push_str(s.to_str()), 54 | Tag::Path(id) | Tag::PubScope(id) => { 55 | line.push(text_tag.take()); 56 | text_tag.id = Some(*id); 57 | let next = iter.next(); 58 | if let Some(Tag::Name(name)) = next { 59 | text_tag.text = name.clone(); 60 | } else { 61 | error!(?id, ?next, "name doesn't follow id"); 62 | } 63 | line.push(text_tag.take()); 64 | } 65 | Tag::Symbol(Symbol::Punctuation(Punctuation::NewLine)) => { 66 | line.push(text_tag.take()); 67 | lines.push(DeclarationLine { 68 | line: mem::take(&mut line), 69 | }); 70 | } 71 | Tag::Symbol(s) => text_tag.text.push_str(s.to_str()), 72 | Tag::UnusualAbi(s) => text_tag.text.push_str(s), 73 | _ => (), 74 | } 75 | } 76 | if !text_tag.text.is_empty() { 77 | line.push(text_tag); 78 | } 79 | // If the last text_tag carries id, we'll meet an empty text_tag 80 | // with the last line not pushed, thus check the last line and push it! 81 | if !line.is_empty() { 82 | lines.push(DeclarationLine { line }); 83 | } 84 | let mut decl_lines = Self { lines }; 85 | decl_lines.shrink_to_fit(); 86 | decl_lines 87 | } 88 | 89 | fn shrink_to_fit(&mut self) { 90 | for line in &mut self.lines { 91 | line.line.shrink_to_fit(); 92 | } 93 | self.lines.shrink_to_fit(); 94 | } 95 | } 96 | 97 | #[derive(Clone, Default)] 98 | pub struct DeclarationLine { 99 | line: Vec, 100 | } 101 | 102 | impl fmt::Debug for DeclarationLine { 103 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 104 | for tt in &self.line { 105 | _ = write!(f, "{tt:?} "); 106 | } 107 | Ok(()) 108 | } 109 | } 110 | 111 | impl std::ops::Deref for DeclarationLine { 112 | type Target = [TextTag]; 113 | 114 | fn deref(&self) -> &Self::Target { 115 | &self.line 116 | } 117 | } 118 | 119 | #[derive(Clone, Default)] 120 | pub struct TextTag { 121 | pub text: XString, 122 | pub id: Option, 123 | } 124 | 125 | impl fmt::Debug for TextTag { 126 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 127 | let TextTag { text, id } = self; 128 | if let Some(id) = id { 129 | _ = write!(f, "{text}#{id:?}#☺️"); 130 | } else { 131 | _ = write!(f, "{text}☺️"); 132 | } 133 | Ok(()) 134 | } 135 | } 136 | 137 | impl TextTag { 138 | fn take(&mut self) -> Self { 139 | mem::take(self) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/type_name/style/decl/function.rs: -------------------------------------------------------------------------------- 1 | use super::{Declaration, VisNameMap}; 2 | use crate::type_name::style::{ 3 | path::{FindName, Format}, 4 | utils::write_comma, 5 | Function as Func, Punctuation, StyledType, Syntax, 6 | }; 7 | use rustdoc_types::{Function, FunctionSignature, Generics, Type}; 8 | 9 | impl Declaration for Function { 10 | fn format(&self, map: VisNameMap, buf: &mut StyledType) { 11 | map.vis.format::(buf); 12 | let Function { 13 | sig, 14 | generics, 15 | header, 16 | has_body, 17 | } = self; 18 | header.format::(buf); 19 | buf.write(Func::Fn); 20 | buf.write(map.name); 21 | let Generics { 22 | params, 23 | where_predicates, 24 | } = generics; 25 | params.format::(buf); 26 | sig.format::(buf); 27 | where_predicates.format::(buf); 28 | if !*has_body { 29 | // if a function has no body, it's likely an associated function in trait definition 30 | buf.write(Punctuation::SemiColon); 31 | } 32 | } 33 | } 34 | 35 | impl Format for FunctionSignature { 36 | fn format(&self, buf: &mut StyledType) { 37 | let FunctionSignature { 38 | inputs, 39 | output, 40 | is_c_variadic, 41 | } = self; 42 | // Multiline for args if the count is more than 2. 43 | let multiline = (inputs.len() + *is_c_variadic as usize) > 2; 44 | buf.write_in_parentheses(|buf| { 45 | buf.write_slice( 46 | inputs, 47 | |arg, buf| { 48 | if multiline { 49 | buf.write(Punctuation::NewLine); 50 | buf.write(Punctuation::Indent); 51 | } 52 | fn_argument::(arg, buf); 53 | }, 54 | write_comma, 55 | ); 56 | if *is_c_variadic { 57 | write_comma(buf); 58 | if multiline { 59 | buf.write(Punctuation::NewLine); 60 | buf.write(Punctuation::Indent); 61 | } 62 | buf.write(Syntax::Variadic); 63 | } 64 | if multiline { 65 | buf.write(Punctuation::NewLine); 66 | } 67 | }); 68 | if let Some(ty) = output { 69 | buf.write(Syntax::ReturnArrow); 70 | ty.format::(buf); 71 | } 72 | } 73 | } 74 | 75 | // Special check for self receiver: 76 | // self and Self are strict keywords, meaning they are only allowed to be used 77 | // as receiver the first arguement in methods, so they will never be seen in incorrect context. 78 | // We could check the receiver case in arg slice, but to keep things simple, 79 | // only check self/Self in functions for all arguements. 80 | fn fn_argument(arg @ (name, ty): &(String, Type), buf: &mut StyledType) { 81 | if name == "self" { 82 | match ty { 83 | Type::BorrowedRef { 84 | lifetime, 85 | is_mutable, 86 | type_, 87 | } if matches!(&**type_, Type::Generic(s) if s == "Self") => { 88 | match (lifetime, is_mutable) { 89 | (None, false) => { 90 | // &self 91 | buf.write(Syntax::Reference); 92 | buf.write(Syntax::self_); 93 | } 94 | (None, true) => { 95 | // &mut self 96 | buf.write(Syntax::ReferenceMut); 97 | buf.write(Syntax::self_); 98 | } 99 | (Some(life), false) => { 100 | // &'life self 101 | buf.write(Syntax::Reference); 102 | buf.write(life); 103 | buf.write(Punctuation::WhiteSpace); 104 | buf.write(Syntax::self_); 105 | } 106 | (Some(life), true) => { 107 | // &'life mut self 108 | buf.write(Syntax::Reference); 109 | buf.write(life); 110 | buf.write(Punctuation::WhiteSpace); 111 | buf.write(Syntax::Mut); 112 | buf.write(Syntax::self_); 113 | } 114 | } 115 | } 116 | Type::Generic(s) if s == "Self" => buf.write(Syntax::self_), // self 117 | _ => arg.format::(buf), // self: Type (Box/Rc/Arc/...) 118 | } 119 | } else { 120 | arg.format::(buf) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/type_name/style/decl/mod.rs: -------------------------------------------------------------------------------- 1 | mod function; 2 | mod struct_; 3 | 4 | use super::{ 5 | path::{FindName, Format, Short}, 6 | StyledType, Vis, 7 | }; 8 | use crate::tree::IDMap; 9 | use rustdoc_types::{Id, ItemEnum, Visibility}; 10 | 11 | fn item_styled(id: &Id, map: &IDMap) -> StyledType { 12 | if let Some(item) = map.get_item(id) { 13 | let vis_name_map = VisNameMap { 14 | name: item.name.as_deref().unwrap_or(""), 15 | vis: &item.visibility, 16 | id: *id, 17 | map, 18 | }; 19 | let mut buf = StyledType::with_capacity(48); 20 | match &item.inner { 21 | ItemEnum::Use(reexport) => { 22 | let id = reexport.id.as_ref(); 23 | return id.map(|id| item_styled(id, map)).unwrap_or_default(); 24 | } 25 | ItemEnum::Function(f) => f.format_as_short(vis_name_map, &mut buf), 26 | ItemEnum::Struct(s) => s.format_as_short(vis_name_map, &mut buf), 27 | _ => return StyledType::default(), 28 | }; 29 | return buf; 30 | } 31 | StyledType::default() 32 | } 33 | 34 | impl StyledType { 35 | pub fn new(id: &Id, map: &IDMap) -> Self { 36 | item_styled(id, map) 37 | } 38 | } 39 | 40 | impl Format for Visibility { 41 | fn format(&self, buf: &mut StyledType) { 42 | buf.write(match self { 43 | Visibility::Public => Vis::Pub, 44 | Visibility::Default => Vis::Default, 45 | Visibility::Crate => Vis::PubCrate, 46 | Visibility::Restricted { parent, path } => { 47 | buf.write_vis_scope(*parent, path); 48 | return; 49 | } 50 | }); 51 | } 52 | } 53 | 54 | struct VisNameMap<'a> { 55 | vis: &'a Visibility, 56 | id: Id, 57 | name: &'a str, 58 | map: &'a IDMap, 59 | } 60 | 61 | trait Declaration { 62 | fn format(&self, map: VisNameMap, buf: &mut StyledType); 63 | fn format_as_short(&self, map: VisNameMap, buf: &mut StyledType) { 64 | ::format::(self, map, buf); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/type_name/style/decl/struct_.rs: -------------------------------------------------------------------------------- 1 | use super::{Declaration, VisNameMap}; 2 | use crate::{ 3 | tree::IDMap, 4 | type_name::style::{ 5 | path::{FindName, Format}, 6 | utils::{write_comma, write_comma_without_whitespace}, 7 | Decl, Punctuation, StyledType, Syntax, 8 | }, 9 | }; 10 | use rustdoc_types::{Generics, Id, ItemEnum, Struct, StructKind}; 11 | 12 | impl Declaration for Struct { 13 | fn format(&self, map: VisNameMap, buf: &mut StyledType) { 14 | let Struct { 15 | kind, 16 | generics: 17 | Generics { 18 | params, 19 | where_predicates, 20 | }, 21 | .. 22 | } = self; 23 | let VisNameMap { vis, id, name, map } = map; 24 | vis.format::(buf); 25 | buf.write(Decl::Struct); 26 | buf.write_id_name(id, name); 27 | params.format::(buf); 28 | match kind { 29 | StructKind::Unit => { 30 | where_predicates.format::(buf); 31 | buf.write(Punctuation::SemiColon); 32 | } 33 | StructKind::Tuple(fields) => { 34 | tuple::(fields, map, buf); 35 | where_predicates.format::(buf); 36 | buf.write(Punctuation::SemiColon); 37 | } 38 | StructKind::Plain { 39 | fields, 40 | has_stripped_fields, 41 | } => { 42 | where_predicates.format::(buf); 43 | buf.write(if where_predicates.is_empty() { 44 | Punctuation::WhiteSpace 45 | } else { 46 | Punctuation::NewLine 47 | }); 48 | plain::(fields, *has_stripped_fields, map, buf); 49 | } 50 | }; 51 | } 52 | } 53 | 54 | fn tuple(fields: &[Option], map: &IDMap, buf: &mut StyledType) { 55 | if fields.iter().all(Option::is_none) { 56 | // all fields are private, thus no need to be multiline 57 | buf.write_in_parentheses(|buf| { 58 | buf.write_slice( 59 | itertools::repeat_n(Syntax::Infer, fields.len()), 60 | |t, buf| buf.write(t), 61 | write_comma, 62 | ) 63 | }); 64 | return; 65 | } 66 | buf.write_in_parentheses(|buf| { 67 | // if a tuple has more than one fields, make it multiline. 68 | let multiline = fields.len() > 1; 69 | buf.write_slice( 70 | fields, 71 | |id, buf| { 72 | if multiline { 73 | buf.write(Punctuation::NewLine); 74 | buf.write(Punctuation::Indent); 75 | } 76 | let field = id.and_then(|id| map.get_item(&id).map(|x| &x.inner)); 77 | if let Some(ItemEnum::StructField(f)) = field { 78 | f.format::(buf); 79 | } else { 80 | buf.write(Syntax::Infer); 81 | } 82 | }, 83 | |buf| { 84 | if multiline { 85 | write_comma_without_whitespace(buf) 86 | } else { 87 | write_comma(buf) 88 | } 89 | }, 90 | ); 91 | if multiline { 92 | buf.write(Punctuation::NewLine); 93 | } 94 | }); 95 | } 96 | 97 | fn plain(fields: &[Id], has_private_field: bool, map: &IDMap, buf: &mut StyledType) { 98 | buf.write_in_brace(|buf| { 99 | buf.write_slice( 100 | fields, 101 | |id, buf| { 102 | buf.write(Punctuation::NewLine); 103 | buf.write(Punctuation::Indent); 104 | let Some(field) = map.get_item(id) else { 105 | error!(?id, "field item is not found"); 106 | return; 107 | }; 108 | buf.write(field.name.as_deref().unwrap_or("???")); 109 | buf.write(Punctuation::Colon); 110 | if let ItemEnum::StructField(f) = &field.inner { 111 | f.format::(buf); 112 | } else { 113 | error!(?field, "not a StructField in a struct"); 114 | buf.write(Syntax::Infer); 115 | } 116 | }, 117 | write_comma_without_whitespace, 118 | ); 119 | match (fields.is_empty(), has_private_field) { 120 | (true, true) => { 121 | // no public field + PrivateFields = all PrivateFields 122 | // oneline 123 | buf.write(Punctuation::WhiteSpace); 124 | buf.write(Decl::PrivateFields); 125 | buf.write(Punctuation::WhiteSpace); 126 | } 127 | (false, true) => { 128 | // multiline + some public fields + PrivateFields 129 | buf.write(Punctuation::Comma); 130 | buf.write(Punctuation::NewLine); 131 | buf.write(Punctuation::Indent); 132 | buf.write(Decl::PrivateFields); 133 | buf.write(Punctuation::NewLine); 134 | } 135 | (false, false) => { 136 | // multiline + all public fields 137 | buf.write(Punctuation::NewLine); 138 | } 139 | (true, false) => (), // no public and no PrivateFields = empty fields 140 | } 141 | }); 142 | } 143 | -------------------------------------------------------------------------------- /src/type_name/style/function.rs: -------------------------------------------------------------------------------- 1 | use super::{generics::hrtb, path::*, utils::write_comma, Punctuation, StyledType, Syntax, Tag}; 2 | use rustdoc_types::{Abi, FunctionHeader, FunctionPointer, FunctionSignature, Type}; 3 | 4 | impl Format for FunctionPointer { 5 | fn format(&self, buf: &mut StyledType) { 6 | let FunctionPointer { 7 | sig, 8 | generic_params, // HRTB 9 | header, 10 | } = self; 11 | hrtb::(generic_params, buf); 12 | header.format::(buf); 13 | buf.write(Syntax::FnPointer); 14 | FnPointerDecl(sig).format::(buf); 15 | } 16 | } 17 | 18 | /// Fn pointer contains a default `_` as argument name, but no need to show it. 19 | /// Rust also allows named arguments in fn pointer, so if the name is not `_`, it's shown. 20 | /// Besides, the arguments in a fnpointer are in one line, or rather the whole fnpointer is one-line, 21 | /// whereas lines for arguments in a function item depend. 22 | struct FnPointerDecl<'d>(&'d FunctionSignature); 23 | 24 | impl Format for FnPointerDecl<'_> { 25 | fn format(&self, buf: &mut StyledType) { 26 | let FunctionSignature { 27 | inputs, 28 | output, 29 | is_c_variadic, 30 | } = self.0; 31 | buf.write_in_parentheses(|buf| { 32 | buf.write_slice( 33 | inputs, 34 | |arg, buf| { 35 | let (name, ty) = arg; 36 | if name == "_" { 37 | ty.format::(buf); 38 | } else { 39 | arg.format::(buf); 40 | } 41 | }, 42 | write_comma, 43 | ); 44 | if *is_c_variadic { 45 | buf.write(Syntax::Variadic); 46 | } 47 | }); 48 | if let Some(ty) = output { 49 | buf.write(Syntax::ReturnArrow); 50 | ty.format::(buf); 51 | } 52 | } 53 | } 54 | 55 | impl Format for (String, Type) { 56 | /// Named function inputs for fn items. 57 | /// 58 | /// NOTE: usually some more checks should be performed before calling this: 59 | /// * fn pointers don't need `_` name 60 | /// * fn items don't need `self` name 61 | fn format(&self, buf: &mut StyledType) { 62 | let (name, ty) = self; 63 | buf.write(name); 64 | buf.write(Punctuation::Colon); 65 | ty.format::(buf); 66 | } 67 | } 68 | 69 | impl Format for FunctionHeader { 70 | fn format(&self, buf: &mut StyledType) { 71 | use super::Function; 72 | let FunctionHeader { 73 | is_const, 74 | is_unsafe, 75 | is_async, 76 | abi, 77 | } = self; 78 | if *is_const { 79 | buf.write(Function::Const); 80 | } 81 | if *is_async { 82 | buf.write(Function::Const); 83 | } 84 | if *is_unsafe { 85 | buf.write(Function::Unsafe); 86 | } 87 | abi.format::(buf); 88 | } 89 | } 90 | 91 | impl Format for Abi { 92 | fn format(&self, buf: &mut StyledType) { 93 | use super::Abi as A; 94 | buf.write(match self { 95 | Abi::Rust => A::Rust, 96 | Abi::C { .. } => A::C, 97 | Abi::Cdecl { .. } => A::Cdecl, 98 | Abi::Stdcall { .. } => A::Stdcall, 99 | Abi::Fastcall { .. } => A::Fastcall, 100 | Abi::Aapcs { .. } => A::Aapcs, 101 | Abi::Win64 { .. } => A::Win64, 102 | Abi::SysV64 { .. } => A::SysV64, 103 | Abi::System { .. } => A::System, 104 | Abi::Other(abi) => { 105 | buf.write(A::Other); 106 | buf.write(Tag::UnusualAbi(abi.into())); 107 | return; 108 | } 109 | }); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/type_name/style/path.rs: -------------------------------------------------------------------------------- 1 | use super::StyledType; 2 | use rustdoc_types::{Path, Type}; 3 | 4 | // pub trait TypeName: Copy + FnOnce(&Type, &mut StyledType) {} 5 | // impl TypeName for F where F: Copy + FnOnce(&Type, &mut StyledType) {} 6 | pub trait ResolvePath: Copy + FnOnce(&Path, &mut StyledType) {} 7 | impl ResolvePath for F where F: Copy + FnOnce(&Path, &mut StyledType) {} 8 | 9 | pub trait FindName { 10 | // fn type_name() -> impl TypeName; 11 | fn resolve_path() -> impl ResolvePath; 12 | // fn type_and_path() -> (impl TypeName, impl ResolvePath) { 13 | // (Self::type_name(), Self::resolve_path()) 14 | // } 15 | } 16 | 17 | pub struct Short; 18 | 19 | impl FindName for Short { 20 | // fn type_name() -> impl TypeName { 21 | // short 22 | // } 23 | fn resolve_path() -> impl ResolvePath { 24 | __short_path__ 25 | } 26 | } 27 | 28 | pub struct Long; 29 | 30 | impl FindName for Long { 31 | // fn type_name() -> impl TypeName { 32 | // long 33 | // } 34 | fn resolve_path() -> impl ResolvePath { 35 | __long_path__ 36 | } 37 | } 38 | 39 | // pub fn short(ty: &Type, buf: &mut StyledType) { 40 | // ::format::(ty, buf); 41 | // } 42 | 43 | pub fn long(ty: &Type) -> String { 44 | let mut buf = StyledType::with_capacity(16); 45 | ::format::(ty, &mut buf); 46 | buf.to_non_wrapped_string() 47 | } 48 | 49 | pub fn long_path(p: &Path) -> String { 50 | let mut buf = StyledType::with_capacity(16); 51 | let Path { path, id, args } = p; 52 | buf.write_id_name(*id, path); 53 | if let Some(generic_args) = args.as_deref() { 54 | generic_args.format::(&mut buf); 55 | } 56 | buf.to_non_wrapped_string() 57 | } 58 | 59 | /// Show full names in path. 60 | /// 61 | /// Not guaranteed to always be an absolute path for any Path. 62 | pub fn __long_path__(p: &Path, buf: &mut StyledType) { 63 | let Path { path, id, args } = p; 64 | buf.write_id_name(*id, path); 65 | if let Some(generic_args) = args.as_deref() { 66 | generic_args.format::(buf); 67 | } 68 | } 69 | 70 | /// Only show the last name in path. 71 | pub fn __short_path__(p: &Path, buf: &mut StyledType) { 72 | fn short_name(name: &str) -> &str { 73 | &name[name.rfind(':').map_or(0, |x| x + 1)..] 74 | } 75 | let Path { path, id, args } = p; 76 | let name = short_name(path); 77 | buf.write_span_path_name(|s| s.write_id_name(*id, name)); 78 | if let Some(generic_args) = args.as_deref() { 79 | generic_args.format::(buf); 80 | } 81 | } 82 | 83 | pub trait Format { 84 | fn format(&self, buf: &mut StyledType); 85 | // fn format_as_short(&self, buf: &mut StyledType) { 86 | // self.format::(buf); 87 | // } 88 | } 89 | 90 | impl Format for Path { 91 | fn format(&self, buf: &mut StyledType) { 92 | (Kind::resolve_path())(self, buf); 93 | } 94 | } 95 | 96 | // Turn Format into trait object. 97 | // trait FormatObj { 98 | // fn format_styled(&self, buf: &mut StyledType); 99 | // } 100 | // impl FormatObj for T { 101 | // fn format_styled(&self, buf: &mut StyledType) { 102 | // self.format::(buf); 103 | // } 104 | // } 105 | // fn check(_: &dyn FormatObj) {} 106 | -------------------------------------------------------------------------------- /src/type_name/style/utils.rs: -------------------------------------------------------------------------------- 1 | use super::{Punctuation, StyledType, Tag}; 2 | use itertools::intersperse; 3 | 4 | pub fn write_colon(buf: &mut StyledType) { 5 | buf.write(Punctuation::Colon); 6 | } 7 | 8 | pub fn write_plus(buf: &mut StyledType) { 9 | buf.write(Punctuation::Plus); 10 | } 11 | 12 | pub fn write_comma(buf: &mut StyledType) { 13 | buf.write(Punctuation::Comma); 14 | buf.write(Punctuation::WhiteSpace); 15 | } 16 | 17 | /// Comma separator in multiline doesn't need trailling whitespace. 18 | pub fn write_comma_without_whitespace(buf: &mut StyledType) { 19 | buf.write(Punctuation::Comma); 20 | } 21 | 22 | // pub fn write_nothing(_: &mut StyledType) {} 23 | 24 | impl StyledType { 25 | /// Write a colon and bounds concatenated by `+`. 26 | /// Make sure the iter is non-empty, because this function writes contents anyway. 27 | pub(super) fn write_bounds(&mut self, iter: impl IntoIterator) 28 | where 29 | Tag: From, 30 | { 31 | self.write(Punctuation::Colon); 32 | let iterable = iter.into_iter().map(Tag::from); 33 | for tag in intersperse(iterable, Punctuation::Plus.into()) { 34 | self.write(tag); 35 | } 36 | } 37 | 38 | /// Write multiple `repeat` separated by `sep` if slice is not empty. 39 | /// Won't write anything if slice is empty. 40 | /// 41 | /// Sometimes slice length check is still done before calling this method, 42 | /// say a slice of generic parameter bound needs an extra starting colon and 43 | /// angle brackes if it's non-empty, but does not need them if empty. 44 | pub(super) fn write_slice( 45 | &mut self, 46 | slice: impl IntoIterator, 47 | repeat: impl Fn(T, &mut Self), 48 | sep: impl Fn(&mut Self), 49 | ) { 50 | let mut iter = slice.into_iter(); 51 | let Some(t) = iter.next() else { 52 | return; 53 | }; 54 | repeat(t, self); 55 | for t in iter { 56 | sep(self); 57 | repeat(t, self); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | pub use compact_str::{ 2 | format_compact as xformat, CompactString as XString, CompactStringExt, ToCompactString, 3 | }; 4 | 5 | pub use rustc_hash::FxHashMap as HashMap; 6 | 7 | /// Construct a [`rustc_hash::FxHashMap`]. 8 | pub fn hashmap(cap: usize) -> HashMap { 9 | HashMap::with_capacity_and_hasher(cap, Default::default()) 10 | } 11 | 12 | /// Join a vec of string by `::`. 13 | pub fn join_path(path: &[String]) -> XString { 14 | path.iter().map(|path| path.as_str()).join_compact("::") 15 | } 16 | -------------------------------------------------------------------------------- /tests/compile_doc.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, path::PathBuf}; 2 | use term_rustdoc::tree::{CrateDoc, Show}; 3 | 4 | fn init_logger() { 5 | let file = File::create("./target/test.log").unwrap(); 6 | tracing_subscriber::fmt() 7 | .with_max_level(tracing::Level::DEBUG) 8 | .with_writer(file) 9 | .with_ansi(false) 10 | .without_time() 11 | .init(); 12 | } 13 | 14 | // replace this path on your machine 15 | fn compile(path: &str) -> PathBuf { 16 | rustdoc_json::Builder::default() 17 | .toolchain("nightly") 18 | .target_dir("./target") 19 | .manifest_path(path) 20 | .build() 21 | .unwrap() 22 | } 23 | 24 | #[test] 25 | #[ignore = "stack overflows caused by recursive reexported modules has been solved"] 26 | fn compile_actix_0_13_0() { 27 | init_logger(); 28 | let json_path = 29 | compile("/root/.cargo/registry/src/rsproxy.cn-0dccff568467c15b/actix-0.13.0/Cargo.toml"); 30 | dbg!(&json_path); 31 | let file = File::open(&json_path).unwrap(); 32 | let json: rustdoc_types::Crate = serde_json::from_reader(file).unwrap(); 33 | println!("parse done"); 34 | let crate_doc = CrateDoc::new(json); 35 | dbg!(&crate_doc); 36 | } 37 | 38 | #[test] 39 | #[ignore = "TODO reexport external crate items"] 40 | fn compile_nucleo_0_3_0() { 41 | init_logger(); 42 | let json_path = 43 | compile("/root/.cargo/registry/src/rsproxy.cn-0dccff568467c15b/nucleo-0.3.0/Cargo.toml"); 44 | dbg!(&json_path); 45 | let file = File::open(&json_path).unwrap(); 46 | let json: rustdoc_types::Crate = serde_json::from_reader(file).unwrap(); 47 | println!("parse done"); 48 | let crate_doc = CrateDoc::new(json); 49 | println!("{}", crate_doc.dmodule().show_prettier(&crate_doc)); 50 | } 51 | -------------------------------------------------------------------------------- /tests/integration/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "integration" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | 8 | [lints.rust] 9 | dead_code = "allow" 10 | unexpected_cfgs = { level = "allow", check-cfg = ['cfg(nightly)'] } 11 | -------------------------------------------------------------------------------- /tests/integration/build.rs: -------------------------------------------------------------------------------- 1 | fn main() -> Result<(), Box> { 2 | let output = std::process::Command::new("rustc").arg("-V").output()?; 3 | // set nightly cfg if current toolchain is nightly 4 | if std::str::from_utf8(&output.stdout)?.contains("nightly") { 5 | println!("cargo:rustc-cfg=nightly"); 6 | } 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /tests/integration/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(nightly, feature(c_variadic))] 2 | /// Documentation for struct AUnitStruct. 3 | pub struct AUnitStruct; 4 | 5 | pub trait ATrait {} 6 | impl ATrait for AUnitStruct {} 7 | 8 | struct PrivateUnitStruct; 9 | impl ATrait for PrivateUnitStruct {} 10 | 11 | pub mod submod1 { 12 | pub mod submod2 { 13 | pub use crate::AUnitStruct; 14 | pub use crate::AUnitStruct as AStructAlias; 15 | 16 | pub trait ATraitNeverImplementedForTypes {} 17 | } 18 | 19 | #[derive(Debug)] 20 | pub enum AUnitEnum { 21 | A, 22 | B, 23 | C, 24 | } 25 | 26 | impl AUnitEnum { 27 | pub fn print(&self) { 28 | println!("{self:?}"); 29 | } 30 | } 31 | } 32 | 33 | pub struct FieldsNamedStruct { 34 | pub field1: AUnitStruct, 35 | pub field2: submod1::submod2::AStructAlias, 36 | pub field3: Vec, 37 | private: submod1::AUnitEnum, 38 | } 39 | 40 | impl FieldsNamedStruct { 41 | pub fn new() -> Self { 42 | FieldsNamedStruct { 43 | field1: AUnitStruct, 44 | field2: AUnitStruct, 45 | field3: Vec::new(), 46 | private: submod1::AUnitEnum::A, 47 | } 48 | } 49 | 50 | pub fn consume(self) {} 51 | pub fn by_ref(&self) {} 52 | pub fn by_ref_mut(&mut self) {} 53 | pub fn by_rc(self: std::rc::Rc) {} 54 | } 55 | 56 | impl Default for FieldsNamedStruct { 57 | fn default() -> Self { 58 | Self::new() 59 | } 60 | } 61 | 62 | pub fn func_with_no_args() {} 63 | pub fn func_with_1arg(_: FieldsNamedStruct) {} 64 | pub fn func_with_1arg_and_ret(f: FieldsNamedStruct) -> submod1::AUnitEnum { 65 | f.private 66 | } 67 | pub fn func_dyn_trait(d: &(dyn ATrait + Send + Sync)) -> &dyn ATrait { 68 | d 69 | } 70 | pub fn func_dyn_trait2(_: Box) {} 71 | pub fn func_primitive(s: &str) -> usize { 72 | s.len() 73 | } 74 | pub fn func_tuple_array_slice<'a, 'b>( 75 | a: &'a [u8], 76 | b: &'b mut [u8; 8], 77 | _: &'b mut (dyn 'a + ATrait), 78 | ) -> (&'a [u8], &'b mut [u8; 8]) { 79 | (a, b) 80 | } 81 | pub fn func_with_const(t: T) -> [T; N] { 82 | [t; N] 83 | } 84 | pub fn func_lifetime_bounds<'a, 'b: 'a>() 85 | where 86 | 'a: 'b, 87 | { 88 | } 89 | #[allow(clippy::multiple_bound_locations)] 90 | pub fn func_trait_bounds() 91 | where 92 | T: Clone, 93 | { 94 | } 95 | pub fn func_fn_pointer_impl_trait( 96 | f: fn(*mut u8) -> *const u8, 97 | ) -> impl Copy + Fn(*mut u8) -> *const u8 { 98 | f 99 | } 100 | pub fn func_qualified_path<'a, I: Iterator>(mut iter: I) -> Option 101 | where 102 | I::Item: 'a + std::fmt::Debug + Iterator + ATraitWithGAT = ()>, 103 | { 104 | iter.next() 105 | } 106 | pub fn func_hrtb() 107 | where 108 | for<'a> ::Assoc<'a>: Copy, 109 | { 110 | } 111 | /// # Safety 112 | #[cfg(nightly)] 113 | pub unsafe extern "C" fn variadic(_: *const (), _name: ...) {} 114 | /// # Safety 115 | #[cfg(nightly)] 116 | pub unsafe extern "C" fn variadic_multiline(_: *const (), _: *mut (), _name: ...) {} 117 | pub fn no_synthetic(_: impl Sized) {} 118 | 119 | pub trait ATraitWithGAT { 120 | type Assoc<'a> 121 | where 122 | Self: 'a; 123 | fn return_assoc(&self) -> Self::Assoc<'_>; 124 | } 125 | 126 | pub const ACONSTANT: u8 = 123; 127 | pub const ASTATIC: u8 = 123; 128 | 129 | #[macro_export] 130 | macro_rules! a_decl_macro { 131 | () => {}; 132 | } 133 | 134 | pub mod structs { 135 | pub struct Unit; 136 | pub struct UnitWithBound 137 | where 138 | u8: Copy; 139 | pub struct UnitGeneric; 140 | pub struct UnitGenericWithBound 141 | where 142 | [(); N]:; 143 | 144 | pub struct Tuple((), crate::PrivateUnitStruct, pub crate::FieldsNamedStruct); 145 | pub struct TupleAllPrivate((), (), ()); 146 | pub struct TupleWithBound() 147 | where 148 | u8: Copy; 149 | pub struct TupleGeneric<'a, T: 'a, const N: usize>(pub &'a T, pub [T; N]); 150 | pub struct TupleGenericWithBound<'a, T: 'a, const N: usize>(pub &'a T, [T; N]) 151 | where 152 | [T; N]:, 153 | T: Copy; 154 | 155 | #[allow(clippy::type_complexity)] 156 | pub struct NamedAllPrivateFields { 157 | fut: (), 158 | } 159 | 160 | #[allow(clippy::type_complexity)] 161 | pub struct NamedAllPublicFields { 162 | pub fut: std::pin::Pin< 163 | Box>>>, 164 | >, 165 | } 166 | 167 | #[allow(clippy::type_complexity)] 168 | pub struct Named { 169 | pub fut: std::pin::Pin< 170 | Box>>>, 171 | >, 172 | private: u8, 173 | } 174 | 175 | pub struct NamedGeneric<'a, T, const N: usize> { 176 | pub f1: &'a T, 177 | pub f2: [T; N], 178 | } 179 | 180 | pub struct NamedGenericWithBound<'a, T = (), const N: usize = 1> 181 | where 182 | T: Copy, 183 | { 184 | pub f1: &'a [T], 185 | pub f2: [T; N], 186 | } 187 | 188 | pub struct NamedGenericAllPrivate<'a, T, const N: usize> { 189 | f1: &'a [T; N], 190 | } 191 | 192 | pub struct NamedGenericWithBoundAllPrivate<'a, T, const N: usize> 193 | where 194 | T: Copy, 195 | { 196 | f1: &'a [T; N], 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /tests/parse-json-docs/generate_doc_json.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use std::{ 3 | fs, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | fn registry_path() -> Result> { 8 | let mut cargo = home::cargo_home()?; 9 | cargo.extend(["registry", "src"]); 10 | let mut entries = Vec::new(); 11 | for entry in fs::read_dir(&cargo)? { 12 | let entry = entry?; 13 | entries.push((entry.metadata()?.modified()?, entry.path())); 14 | } 15 | entries.sort_unstable_by_key(|v| v.0); 16 | // choose the lastest modified 17 | Ok(entries.pop().map(|v| v.1)) 18 | } 19 | 20 | fn tokio_path() -> Result { 21 | let mut path = registry_path()?.unwrap(); 22 | let filename = fs::read_dir(&path)? 23 | .filter_map(|entry| { 24 | let file_name = &entry.ok()?.file_name(); 25 | let filename = file_name.to_str()?; 26 | if filename.starts_with("tokio-1.") { 27 | Some(filename.to_owned()) 28 | } else { 29 | None 30 | } 31 | }) 32 | .max() 33 | .unwrap(); 34 | path.extend([filename.as_str(), "Cargo.toml"]); 35 | Ok(path) 36 | } 37 | 38 | #[test] 39 | #[ignore = "make sure tokio is cached in registry src dir"] 40 | fn checkout_tokio_path() -> Result<()> { 41 | let path = tokio_path()?; 42 | dbg!(path); 43 | Ok(()) 44 | } 45 | 46 | #[test] 47 | #[ignore = "make sure tokio is cached in registry src dir"] 48 | fn generate_tokio_json_doc() -> Result<()> { 49 | let dir = PathBuf::from_iter(["target", "deps"]); 50 | fs::create_dir_all(&dir)?; 51 | // like /root/.cargo/registry/src/rsproxy.cn-0dccff568467c15b/tokio-1.35.1/Cargo.toml 52 | let manifest_path = tokio_path()?; 53 | generate(&dir, manifest_path)?; 54 | generate( 55 | &dir, 56 | PathBuf::from_iter(["tests", "integration", "Cargo.toml"]), 57 | )?; 58 | Ok(()) 59 | } 60 | 61 | fn generate(dir: P, manifest_path: Q) -> Result<()> 62 | where 63 | P: AsRef, 64 | Q: AsRef, 65 | { 66 | rustdoc_json::Builder::default() 67 | .toolchain("nightly") 68 | .target_dir(dir.as_ref()) 69 | .all_features(true) 70 | .manifest_path(manifest_path.as_ref()) 71 | .build()?; 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /tests/parse-json-docs/parse.rs: -------------------------------------------------------------------------------- 1 | use crate::{doc, shot, snap}; 2 | use similar_asserts::assert_eq; 3 | use term_rustdoc::tree::{Show, TreeLines}; 4 | 5 | #[test] 6 | fn parse_module() { 7 | let (treelines, empty) = TreeLines::new_with(doc(), |doc| doc.dmodule_show_prettier()); 8 | let doc = treelines.doc(); 9 | let dmod = doc.dmodule(); 10 | snap!("DModule", dmod); 11 | shot!("show-id", dmod.show()); 12 | 13 | let tree = doc.dmodule_show_prettier(); 14 | let display = tree.to_string(); 15 | shot!("show-prettier", display); 16 | 17 | let display_new = treelines.display_as_plain_text(); 18 | assert_eq!(expected: display, actual: display_new); 19 | snap!("flatten-tree", treelines.all_lines()); 20 | shot!("empty-tree-with-same-depth", empty); 21 | 22 | // item tree 23 | shot!("item-tree", dmod.item_tree(&doc)); 24 | 25 | snap!(dmod.current_items_counts(), @r###" 26 | ItemCount { 27 | modules: 1, 28 | structs: 2, 29 | functions: 6, 30 | traits: 1, 31 | constants: 2, 32 | macros_decl: 1, 33 | } 34 | "###); 35 | snap!(dmod.recursive_items_counts(), @r###" 36 | ItemCount { 37 | modules: 2, 38 | structs: 2, 39 | enums: 1, 40 | functions: 6, 41 | traits: 1, 42 | constants: 2, 43 | macros_decl: 1, 44 | } 45 | "###); 46 | 47 | snap!(dmod.current_impls_counts(), @r###" 48 | ImplCounts { 49 | total: ImplCount { 50 | kind: Both, 51 | total: 3, 52 | structs: 3, 53 | }, 54 | inherent: ImplCount { 55 | kind: Inherent, 56 | total: 1, 57 | structs: 1, 58 | }, 59 | trait: ImplCount { 60 | kind: Trait, 61 | total: 2, 62 | structs: 2, 63 | }, 64 | } 65 | "###); 66 | snap!(dmod.recursive_impls_counts(), @r###" 67 | ImplCounts { 68 | total: ImplCount { 69 | kind: Both, 70 | total: 5, 71 | structs: 3, 72 | enums: 2, 73 | }, 74 | inherent: ImplCount { 75 | kind: Both, 76 | total: 2, 77 | structs: 1, 78 | enums: 1, 79 | }, 80 | trait: ImplCount { 81 | kind: Trait, 82 | total: 3, 83 | structs: 2, 84 | enums: 1, 85 | }, 86 | } 87 | "###); 88 | 89 | // struct inner 90 | let (struct_, _) = TreeLines::new_with(treelines.doc(), |doc| { 91 | doc.dmodule().structs[0].show_prettier(doc) 92 | }); 93 | shot!(struct_.display_as_plain_text(), @r###" 94 | integration::AUnitStruct 95 | ├── No Fields! 96 | └── Implementations 97 | ├── Trait Impls 98 | │ └── AUnitStruct: ATrait 99 | ├── Auto Impls 100 | │ ├── AUnitStruct: RefUnwindSafe 101 | │ ├── AUnitStruct: Send 102 | │ ├── AUnitStruct: Sync 103 | │ ├── AUnitStruct: Unpin 104 | │ └── AUnitStruct: UnwindSafe 105 | └── Blanket Impls 106 | ├── T: Any 107 | ├── T: Borrow 108 | ├── T: BorrowMut 109 | ├── T: From 110 | ├── T: Into 111 | ├── T: TryFrom 112 | └── T: TryInto 113 | "###); 114 | } 115 | -------------------------------------------------------------------------------- /tests/parse-json-docs/snapshots/parse_json_docs__fn_item_decl__DeclarationLines-fn-items.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/parse-json-docs/fn_item_decl.rs 3 | expression: DisplaySlice(&lines) 4 | --- 5 | pub fn func_dyn_trait(d: &(dyn ☺️ ATrait#0:5:1788#☺️  + ☺️ Send#2:32833:237#☺️  + ☺️ Sync#2:2997:246#☺️ )) -> &dyn ☺️ ATrait#0:5:1788#☺️ 6 | 7 | pub fn func_dyn_trait2(_: ☺️ Box#5:294:1815#☺️ )☺️ 8 | 9 | pub fn func_fn_pointer_impl_trait(f: fn(*mut u8) -> *const u8) -> impl ☺️ Copy#2:2992:119#☺️  + ☺️ Fn#2:3237:141#☺️ (*mut u8) -> *const u8☺️ 10 | 11 | pub fn func_hrtb()☺️ 12 | where☺️ 13 |  for<'a> ::Assoc<'a>: ☺️ Copy#2:2992:119#☺️ 14 | 15 | pub fn func_lifetime_bounds<'a, 'b: 'a>()☺️ 16 | where☺️ 17 |  'a: 'b☺️ 18 | 19 | pub fn func_primitive(s: &str) -> usize☺️ 20 | 21 | pub fn func_qualified_path<'a, I: ☺️ Iterator#2:8187:179#☺️ >(iter: I) -> ☺️ Option#2:42344:194#☺️ ☺️ 22 | where☺️ 23 |  I::Item: 'a + ☺️ Debug#2:10131:121#☺️  + ☺️ Iterator#2:8187:179#☺️  + ☺️ ATraitWithGAT#0:61:1829#☺️  = ()>☺️ 24 | 25 | pub fn func_trait_bounds()☺️ 26 | where☺️ 27 |  T: ☺️ Clone#2:2487:114#☺️  + ☺️ Copy#2:2992:119#☺️ 28 | 29 | pub fn func_tuple_array_slice<'a, 'b>(☺️ 30 |  a: &'a [u8], ☺️ 31 |  b: &'b mut [u8; 8], ☺️ 32 |  _: &'b mut (dyn 'a + ☺️ ATrait#0:5:1788#☺️ )☺️ 33 | ) -> (&'a [u8], &'b mut [u8; 8])☺️ 34 | 35 | pub fn func_with_1arg(_: ☺️ FieldsNamedStruct#0:17:1800#☺️ )☺️ 36 | 37 | pub fn func_with_1arg_and_ret(f: ☺️ FieldsNamedStruct#0:17:1800#☺️ ) -> ☺️ AUnitEnum#0:143:1794#☺️ 38 | 39 | pub fn func_with_const(t: T) -> [T; N]☺️ 40 | 41 | pub fn func_with_no_args()☺️ 42 | 43 | pub fn no_synthetic(_: impl ☺️ Sized#2:32834:1837#☺️ )☺️ 44 | 45 | pub unsafe extern "C" fn variadic(_: *const (), ...)☺️ 46 | 47 | pub unsafe extern "C" fn variadic_multiline(☺️ 48 |  _: *const (), ☺️ 49 |  _: *mut (), ☺️ 50 |  ...☺️ 51 | )☺️ 52 | -------------------------------------------------------------------------------- /tests/parse-json-docs/snapshots/parse_json_docs__fn_item_decl__DeclarationLines-methods.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/parse-json-docs/fn_item_decl.rs 3 | expression: DisplaySlice(&lines) 4 | --- 5 | pub fn by_rc(self: ☺️ Rc#5:4919:217#☺️ )☺️ 6 | 7 | pub fn by_ref(&self)☺️ 8 | 9 | pub fn by_ref_mut(&mut self)☺️ 10 | 11 | pub fn consume(self)☺️ 12 | 13 | pub fn new() -> Self☺️ 14 | 15 | fn return_assoc(&self) -> Self::Assoc<'_>;☺️ 16 | -------------------------------------------------------------------------------- /tests/parse-json-docs/snapshots/parse_json_docs__fn_item_decl__DeclarationLines-structs.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/parse-json-docs/fn_item_decl.rs 3 | expression: DisplaySlice(&lines) 4 | --- 5 | pub struct ☺️ AUnitStruct#0:3:1787#☺️ ;☺️ 6 | 7 | pub struct ☺️ FieldsNamedStruct#0:17:1800#☺️  {☺️ 8 |  field1: ☺️ AUnitStruct#0:3:1787#☺️ ,☺️ 9 |  field2: ☺️ AStructAlias#0:3:1787#☺️ ,☺️ 10 |  field3: ☺️ Vec#5:7052:263#☺️ <☺️ FieldsNamedStruct#0:17:1800#☺️ >,☺️ 11 |  /* private fields */☺️ 12 | }☺️ 13 | 14 | pub struct ☺️ Named#0:113:1859#☺️  {☺️ 15 |  fut: ☺️ Pin#2:42479:1857#☺️ <☺️ Box#5:294:1815#☺️ >>>>,☺️ 16 |  /* private fields */☺️ 17 | }☺️ 18 | 19 | pub struct ☺️ NamedAllPrivateFields#0:109:1854#☺️  { /* private fields */ }☺️ 20 | 21 | pub struct ☺️ NamedAllPublicFields#0:111:1856#☺️  {☺️ 22 |  fut: ☺️ Pin#2:42479:1857#☺️ <☺️ Box#5:294:1815#☺️ >>>>☺️ 23 | }☺️ 24 | 25 | pub struct ☺️ NamedGeneric#0:116:1860#☺️ <'a, T, const N: usize> {☺️ 26 |  f1: &'a T,☺️ 27 |  f2: [T; N]☺️ 28 | }☺️ 29 | 30 | pub struct ☺️ NamedGenericAllPrivate#0:131:1864#☺️ <'a, T, const N: usize> { /* private fields */ }☺️ 31 | 32 | pub struct ☺️ NamedGenericWithBound#0:123:1863#☺️ <'a, T = (), const N: usize = 1>☺️ 33 | where☺️ 34 |  T: ☺️ Copy#2:2992:119#☺️ ☺️ 35 | {☺️ 36 |  f1: &'a [T],☺️ 37 |  f2: [T; N]☺️ 38 | }☺️ 39 | 40 | pub struct ☺️ NamedGenericWithBoundAllPrivate#0:137:1865#☺️ <'a, T, const N: usize>☺️ 41 | where☺️ 42 |  T: ☺️ Copy#2:2992:119#☺️ ☺️ 43 | { /* private fields */ }☺️ 44 | 45 | pub struct ☺️ Tuple#0:80:1848#☺️ (☺️ 46 |  _,☺️ 47 |  _,☺️ 48 |  ☺️ FieldsNamedStruct#0:17:1800#☺️ ☺️ 49 | );☺️ 50 | 51 | pub struct ☺️ TupleAllPrivate#0:85:1849#☺️ (_, _, _);☺️ 52 | 53 | pub struct ☺️ TupleGeneric#0:92:1851#☺️ <'a, T: 'a, const N: usize>(☺️ 54 |  &'a T,☺️ 55 |  [T; N]☺️ 56 | );☺️ 57 | 58 | pub struct ☺️ TupleGenericWithBound#0:100:1852#☺️ <'a, T, const N: usize>(☺️ 59 |  &'a T,☺️ 60 |  _☺️ 61 | )☺️ 62 | where☺️ 63 |  [T; N]: ,☺️ 64 |  T: ☺️ Copy#2:2992:119#☺️  + 'a;☺️ 65 | 66 | pub struct ☺️ TupleWithBound#0:90:1850#☺️ ()☺️ 67 | where☺️ 68 |  u8: ☺️ Copy#2:2992:119#☺️ ;☺️ 69 | 70 | pub struct ☺️ Unit#0:69:1844#☺️ ;☺️ 71 | 72 | pub struct ☺️ UnitGeneric#0:73:1846#☺️ ;☺️ 73 | 74 | pub struct ☺️ UnitGenericWithBound#0:76:1847#☺️ ☺️ 75 | where☺️ 76 |  [(); N]: ;☺️ 77 | 78 | pub struct ☺️ UnitWithBound#0:71:1845#☺️ ☺️ 79 | where☺️ 80 |  u8: ☺️ Copy#2:2992:119#☺️ ;☺️ 81 | 82 | pub struct ☺️ AUnitStruct#0:3:1787#☺️ ;☺️ 83 | 84 | pub struct ☺️ AUnitStruct#0:3:1787#☺️ ;☺️ 85 | -------------------------------------------------------------------------------- /tests/parse-json-docs/snapshots/parse_json_docs__local_items.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/parse-json-docs/main.rs 3 | expression: "local_items.iter().map(|item| item.to_string()).collect::>()" 4 | --- 5 | [ 6 | "(376) integration [Module]", 7 | "(373) integration::ACONSTANT [Constant]", 8 | "(374) integration::ASTATIC [Constant]", 9 | "(321) integration::ATrait [Trait]", 10 | "(366) integration::ATraitWithGAT [Trait]", 11 | "(001) integration::AUnitStruct [Struct]", 12 | "(116) integration::FieldsNamedStruct [Struct]", 13 | "(375) integration::a_decl_macro [MacroDecl]", 14 | "(353) integration::func_dyn_trait [Function]", 15 | "(354) integration::func_dyn_trait2 [Function]", 16 | "(361) integration::func_fn_pointer_impl_trait [Function]", 17 | "(367) integration::func_hrtb [Function]", 18 | "(358) integration::func_lifetime_bounds [Function]", 19 | "(355) integration::func_primitive [Function]", 20 | "(363) integration::func_qualified_path [Function]", 21 | "(359) integration::func_trait_bounds [Function]", 22 | "(356) integration::func_tuple_array_slice [Function]", 23 | "(351) integration::func_with_1arg [Function]", 24 | "(352) integration::func_with_1arg_and_ret [Function]", 25 | "(357) integration::func_with_const [Function]", 26 | "(350) integration::func_with_no_args [Function]", 27 | "(370) integration::no_synthetic [Function]", 28 | "(306) integration::structs [Module]", 29 | "(230) integration::structs::Named [Struct]", 30 | "(195) integration::structs::NamedAllPrivateFields [Struct]", 31 | "(214) integration::structs::NamedAllPublicFields [Struct]", 32 | "(246) integration::structs::NamedGeneric [Struct]", 33 | "(277) integration::structs::NamedGenericAllPrivate [Struct]", 34 | "(262) integration::structs::NamedGenericWithBound [Struct]", 35 | "(292) integration::structs::NamedGenericWithBoundAllPrivate [Struct]", 36 | "(117) integration::structs::Tuple [Struct]", 37 | "(134) integration::structs::TupleAllPrivate [Struct]", 38 | "(164) integration::structs::TupleGeneric [Struct]", 39 | "(180) integration::structs::TupleGenericWithBound [Struct]", 40 | "(148) integration::structs::TupleWithBound [Struct]", 41 | "(056) integration::structs::Unit [Struct]", 42 | "(085) integration::structs::UnitGeneric [Struct]", 43 | "(099) integration::structs::UnitGenericWithBound [Struct]", 44 | "(070) integration::structs::UnitWithBound [Struct]", 45 | "(055) integration::submod1 [Module]", 46 | "(008) integration::submod1::AUnitEnum [Enum]", 47 | "(005) integration::submod1::AUnitEnum::A [Variant]", 48 | "(006) integration::submod1::AUnitEnum::B [Variant]", 49 | "(007) integration::submod1::AUnitEnum::C [Variant]", 50 | "(004) integration::submod1::submod2 [Module]", 51 | "(003) integration::submod1::submod2::ATraitNeverImplementedForTypes [Trait]", 52 | "(368) integration::variadic [Function]", 53 | "(369) integration::variadic_multiline [Function]", 54 | ] 55 | -------------------------------------------------------------------------------- /tests/parse-json-docs/snapshots/parse_json_docs__parse__empty-tree-with-same-depth.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/parse-json-docs/parse.rs 3 | expression: empty 4 | --- 5 | ├── 6 | │ ├── [Fn] 7 | │ ├── [Fn] 8 | │ ├── [Fn] 9 | │ ├── [Fn] 10 | │ ├── [Fn] 11 | │ └── [Fn] 12 | ├── 13 | │ ├── [Const] 14 | │ └── [Const] 15 | ├── 16 | │ └── [macro decl] 17 | ├── 18 | │ └── [Trait] 19 | │ └── 20 | │ └── 21 | ├── 22 | │ ├── [Struct] 23 | │ │ ├── 24 | │ │ └── 25 | │ │ ├── 26 | │ │ │ └── 27 | │ │ ├── 28 | │ │ │ ├── 29 | │ │ │ ├── 30 | │ │ │ ├── 31 | │ │ │ ├── 32 | │ │ │ └── 33 | │ │ └── 34 | │ │ ├── 35 | │ │ ├── 36 | │ │ ├── 37 | │ │ ├── 38 | │ │ ├── 39 | │ │ ├── 40 | │ │ └── 41 | │ └── [Struct] 42 | │ ├── 43 | │ │ ├── [field] 44 | │ │ ├── [field] 45 | │ │ ├── [field] 46 | │ │ └── 47 | │ └── 48 | │ ├── 49 | │ │ ├── [Fn] 50 | │ │ ├── [Fn] 51 | │ │ ├── [Fn] 52 | │ │ ├── [Fn] 53 | │ │ └── [Fn] 54 | │ ├── 55 | │ │ └── 56 | │ │ └── [Fn] 57 | │ ├── 58 | │ │ ├── 59 | │ │ ├── 60 | │ │ ├── 61 | │ │ ├── 62 | │ │ └── 63 | │ └── 64 | │ ├── 65 | │ ├── 66 | │ ├── 67 | │ ├── 68 | │ ├── 69 | │ ├── 70 | │ └── 71 | └── [Mod] 72 | ├── 73 | │ └── [Enum] 74 | │ ├── 75 | │ │ ├── [variant] 76 | │ │ ├── [variant] 77 | │ │ └── [variant] 78 | │ └── 79 | │ ├── 80 | │ │ └── [Fn] 81 | │ ├── 82 | │ │ └── 83 | │ │ └── [Fn] 84 | │ ├── 85 | │ │ ├── 86 | │ │ ├── 87 | │ │ ├── 88 | │ │ ├── 89 | │ │ └── 90 | │ └── 91 | │ ├── 92 | │ ├── 93 | │ ├── 94 | │ ├── 95 | │ ├── 96 | │ ├── 97 | │ └── 98 | └── [Mod] 99 | ├── 100 | │ └── [Trait] 101 | │ └── 102 | └── 103 | ├── [Struct] 104 | │ ├── 105 | │ └── 106 | │ ├── 107 | │ │ └── 108 | │ ├── 109 | │ │ ├── 110 | │ │ ├── 111 | │ │ ├── 112 | │ │ ├── 113 | │ │ └── 114 | │ └── 115 | │ ├── 116 | │ ├── 117 | │ ├── 118 | │ ├── 119 | │ ├── 120 | │ ├── 121 | │ └── 122 | └── [Struct] 123 | ├── 124 | └── 125 | ├── 126 | │ └── 127 | ├── 128 | │ ├── 129 | │ ├── 130 | │ ├── 131 | │ ├── 132 | │ └── 133 | └── 134 | ├── 135 | ├── 136 | ├── 137 | ├── 138 | ├── 139 | ├── 140 | └── 141 | -------------------------------------------------------------------------------- /tests/parse-json-docs/snapshots/parse_json_docs__parse__item-tree.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/parse-json-docs/parse.rs 3 | expression: dmod.item_tree(&doc) 4 | --- 5 | integration 6 | ├── [Fn] func_dyn_trait 7 | ├── [Fn] func_dyn_trait2 8 | ├── [Fn] func_primitive 9 | ├── [Fn] func_with_1arg 10 | ├── [Fn] func_with_1arg_and_ret 11 | ├── [Fn] func_with_no_args 12 | ├── [Const] ACONSTANT 13 | ├── [Const] ASTATIC 14 | ├── [macro decl] a_decl_macro 15 | ├── [Trait] ATrait 16 | ├── [Struct] AUnitStruct 17 | ├── [Struct] FieldsNamedStruct 18 | └── [Mod] integration::submod1 19 | ├── [Enum] AUnitEnum 20 | └── [Mod] integration::submod1::submod2 21 | ├── [Trait] ATraitNeverImplementedForTypes 22 | ├── [Struct] AStructAlias 23 | └── [Struct] AUnitStruct 24 | -------------------------------------------------------------------------------- /tests/parse-json-docs/syntect_set.rs: -------------------------------------------------------------------------------- 1 | use crate::snap; 2 | use syntect::parsing::SyntaxSet; 3 | 4 | #[test] 5 | fn syntax_set() { 6 | let set = SyntaxSet::load_defaults_newlines(); 7 | let v = set 8 | .syntaxes() 9 | .iter() 10 | .map(|s| (&s.name, &s.file_extensions)) 11 | .collect::>(); 12 | snap!(v); 13 | } 14 | --------------------------------------------------------------------------------