···11-use std::{borrow::Borrow, collections::HashMap, hash::Hash, io::Write, sync::Arc};
11+use std::{collections::HashMap, io::Write};
2233-use anyhow::{Context, Result};
33+use anyhow::{
44+ Context,
55+ // not shadowing Result because it is linked from docs
66+ Result as Result2,
77+};
48use clap::{Parser, Subcommand};
59use console::colors_enabled_stderr;
610use log::LevelFilter;
77-use lsp_types::Position;
811use mdbook_preprocessor::PreprocessorContext;
912use tap::{Pipe, TapFallible};
1010-use tokio::task::JoinSet;
11131214use mdbookkit::{
1315 book::{BookConfigHelper, BookHelper, book_from_stdin, string_from_stdin},
1416 diagnostics::Issue,
1515- log_debug, log_warning,
1616- logging::{ConsoleLogger, is_logging, spinner},
1717- styled,
1717+ log_warning,
1818+ logging::{ConsoleLogger, is_logging},
1819};
19202021use self::{
2122 cache::{Cache, FileCache},
2223 client::Client,
2324 env::{Config, Environment, RustAnalyzer},
2424- item::Item,
2525- link::ItemLinks,
2625 page::Pages,
2727- url::UrlToPath,
2626+ resolver::Resolver,
2827};
29283029mod cache;
···3433mod link;
3534mod markdown;
3635mod page;
3636+mod resolver;
3737mod sync;
3838#[cfg(test)]
3939mod tests;
4040mod url;
41414242-/// Type that can provide links.
4343-///
4444-/// Resolvers should modify the provided [`Pages`] in place.
4545-///
4646-/// This is currently an abstraction over two sources of links:
4747-///
4848-/// - [`Client`], which invokes rust-analyzer
4949-/// - [`Cache`] implementations
5050-///
5151-/// [`Cache`]: crate::bin::rustdoc_link::cache::Cache
5252-trait Resolver {
5353- async fn resolve<K>(&self, pages: &mut Pages<'_, K>) -> Result<()>
5454- where
5555- K: Eq + Hash;
5656-}
5757-5858-impl Resolver for Client {
5959- async fn resolve<K>(&self, pages: &mut Pages<'_, K>) -> Result<()>
6060- where
6161- K: Eq + Hash,
6262- {
6363- let request = pages.items();
6464-6565- if request.is_empty() {
6666- return Ok(());
6767- }
6868-6969- let main = std::fs::read_to_string(self.env().entrypoint.to_path()?)?;
7070-7171- let (context, request) = {
7272- let mut context = format!("{main}\nfn {UNIQUE_ID} () {{\n");
7373-7474- let line = context.chars().filter(|&c| c == '\n').count();
7575-7676- let request = request
7777- .iter()
7878- .scan(line, |line, (key, item)| {
7979- build(&mut context, line, item).map(|cursors| (key.clone(), cursors))
8080- })
8181- .collect::<Vec<_>>();
8282-8383- fn build(context: &mut String, line: &mut usize, item: &Item) -> Option<Vec<Position>> {
8484- use std::fmt::Write;
8585- let _ = writeln!(context, "{}", item.stmt);
8686- let cursors = item
8787- .cursor
8888- .as_ref()
8989- .iter()
9090- .map(|&col| Position::new(*line as _, col as _))
9191- .collect::<Vec<_>>();
9292- *line += 1;
9393- Some(cursors)
9494- }
9595-9696- context.push('}');
9797-9898- (context, request)
9999- };
100100-101101- log::debug!("request context\n\n{context}\n");
102102-103103- let document = self
104104- .open(self.env().entrypoint.clone(), context)
105105- .await?
106106- .pipe(Arc::new);
107107-108108- spinner().create("resolve", Some(request.len() as _));
109109-110110- let tasks: JoinSet<Option<(String, ItemLinks)>> = request
111111- .into_iter()
112112- .map(|(key, pos)| {
113113- let key = key.to_string();
114114- let doc = document.clone();
115115- resolve(doc, key, pos)
116116- })
117117- .collect();
118118-119119- async fn resolve(
120120- doc: Arc<client::OpenDocument>,
121121- key: String,
122122- pos: Vec<Position>,
123123- ) -> Option<(String, ItemLinks)> {
124124- let _task = spinner().task("resolve", &key);
125125- for p in pos {
126126- let resolved = doc
127127- .resolve(p)
128128- .await
129129- .with_context(|| format!("{p:?}"))
130130- .context("failed to resolve symbol:")
131131- .tap_err(log_debug!())
132132- .ok();
133133- if let Some(resolved) = resolved {
134134- return Some((key, resolved));
135135- }
136136- }
137137- None
138138- }
139139-140140- let resolved = tasks
141141- .join_all()
142142- .await
143143- .into_iter()
144144- .flatten()
145145- .collect::<HashMap<_, _>>();
146146-147147- spinner().finish("resolve", styled!(("done").green()));
148148-149149- pages.apply(&resolved);
150150-151151- Ok(())
152152- }
153153-}
154154-155155-impl<K> Resolver for HashMap<K, ItemLinks>
156156-where
157157- K: Borrow<str> + Eq + Hash,
158158-{
159159- async fn resolve<P>(&self, pages: &mut Pages<'_, P>) -> Result<()>
160160- where
161161- P: Eq + Hash,
162162- {
163163- pages.apply(self);
164164- Ok(())
165165- }
166166-}
167167-16842#[tokio::main]
169169-async fn main() -> Result<()> {
4343+async fn main() -> Result2<()> {
17044 ConsoleLogger::install(PREPROCESSOR_NAME);
17145 match Program::parse().command {
17246 Some(Command::Supports { .. }) => Ok(()),
···20377 Describe,
20478}
20579206206-async fn mdbook() -> Result<()> {
8080+async fn mdbook() -> Result2<()> {
20781 let (ctx, mut book) = book_from_stdin().context("failed to read from mdbook")?;
2088220983 let config = config(&ctx).context("failed to read preprocessor config from book.toml")?;
···275149 Ok(())
276150}
277151278278-async fn markdown(config: Config) -> Result<()> {
152152+async fn markdown(config: Config) -> Result2<()> {
279153 let client = Environment::new(config)
280154 .context("failed to initialize")?
281155 .pipe(Client::new);
···321195 Ok(())
322196}
323197324324-fn which() -> Result<()> {
198198+fn which() -> Result2<()> {
325199 let env = Environment::new(Default::default())?;
326200327201 match env.which() {
···337211}
338212339213#[cfg(feature = "_testing")]
340340-fn describe() -> Result<()> {
214214+fn describe() -> Result2<()> {
341215 print!("{}", mdbookkit::docs::describe_preprocessor::<Config>()?);
342216 Ok(())
343217}
344218345345-fn config(ctx: &PreprocessorContext) -> Result<Config> {
219219+fn config(ctx: &PreprocessorContext) -> Result2<Config> {
346220 let mut config = ctx.config.preprocessor::<Config>(PREPROCESSOR_NAME)?;
347221348222 if let Some(path) = config.manifest_dir {
+1-1
crates/mdbook-rustdoc-links/src/markdown.rs
···1919///
2020/// Links that are "broken" that aren't actually doc links won't show up in the output,
2121/// because the preprocessor ignores links that cannot be parsed and is capable of
2222-/// emitting only changed links, see [`PatchStream`][crate::markdown::PatchStream].
2222+/// emitting only changed links, see [`PatchStream`][mdbookkit::markdown::PatchStream].
2323pub struct ItemLinks;
24242525impl ItemLinks {
···3333 fn level(&self) -> Level;
3434}
35353636-/// A collection of [`Problem`]s associated with a Markdown file.
3636+/// A collection of [`IssueItem`]s associated with a Markdown file.
3737pub struct Diagnostics<'a, K, P> {
3838 text: &'a str,
3939 name: K,
+4
crates/mdbookkit/src/lib.rs
···1717pub mod markdown;
1818#[cfg(feature = "_testing")]
1919pub mod testing;
2020+2121+// referenced in docs
2222+#[doc(hidden)]
2323+pub use diagnostics::Diagnostics;
+1-5
crates/mdbookkit/src/markdown.rs
···1313/// [`pulldown_cmark::Event`] stream, whitespace is NOT preserved. This is problematic
1414/// for mdBook preprocessors, because preprocessors downstream may need to work on
1515/// syntax that is whitespace-sensitive. Normalizing all whitespace could cause such
1616-/// usage to no longer be recognized. An example is [`mdbook-alerts`][alerts]
1717-/// which works on GitHub's ["alerts"][gh-alerts] syntax.
1818-///
1919-/// [alerts]: https://crates.io/crates/mdbook-alerts
2020-/// [gh-alerts]: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts
1616+/// usage to no longer be recognized.
2117pub struct PatchStream<'a, S> {
2218 source: &'a str,
2319 stream: S,
···11+pub use anyhow::Context;
1233+pub use mdbookkit::Diagnostics;
44+55+pub mod error {
66+ pub use mdbookkit::error::is_ci;
77+}
+1-1
docs/src/link-forever/continuous-integration.md
···68686969[^ci-true]:
7070 Specifically, when `CI` is anything other than `""`, `"0"`, or `"false"`. The logic
7171- is encapsulated in the [`is_ci`][crate::env::is_ci] function.
7171+ is encapsulated in the [`is_ci`][crate::error::is_ci] function.
72727373[^stderr]:
7474 Specifically, when stderr is redirected to something that isn't a terminal, such as
+8-19
docs/src/rustdoc-link/caching.md
···11# Caching
2233By default, `mdbook-rustdoc-link` spawns a fresh `rust-analyzer` process every time it
44-is run. rust-analyzer then reindexes your entire project before resolving links.
44+runs. rust-analyzer then reindexes your entire project before resolving links.
5566-This significantly impacts the responsiveness of `mdbook serve` — it is as if for every
77-live reload, you had to reopen your editor, and it gets even worse the more dependencies
88-your project has.
99-1010-To mitigate this, there is an experimental caching feature, disabled by default.
66+This makes the `mdbook serve` command significantly slower, more so if your project
77+contains a large number of dependencies. It is as if for every live reload, you had to
88+reopen your editor.
1191212-<details class="toc" open>
1313- <summary>Sections</summary>
1414-1515-- [Enabling caching](#enabling-caching)
1616-- [How it works](#how-it-works)
1717-- [Help wanted 🙌](#help-wanted-)
1818- - [Cache priming and progress tracking](#cache-priming-and-progress-tracking)
1919- - [Using `ra-multiplex`](#using-ra-multiplex)
2020- - [Postscript](#postscript)
2121-2222-</details>
1010+To mitigate this, there is an experimental caching feature. The feature is disabled by
1111+default.
23122413## Enabling caching
2514···5342> [!NOTE]
5443>
5544> The following are implementation details. See
5656-> [rustdoc_link/cache.rs](/crates/mdbookkit/src/bin/rustdoc_link/cache.rs).
4545+> [cache.rs](/crates/mdbook-rustdoc-links/src/cache.rs).
57465847The effectiveness of this mechanism is based on the following assumptions:
5948···9786cache priming, before actually sending out external docs requests. This requires parsing
9887non-structured log messages that rust-analyzer sends out and some debouncing/throttling
9988logic, which is not ideal, see
100100-[client.rs](/crates/mdbookkit/src/bin/rustdoc_link/client.rs#L142).
8989+[client.rs](/crates/mdbook-rustdoc-links/src/client.rs#L153).
1019010291Not waiting for indexing to finish and sending out requests too early causes
10392rust-analyzer to respond with empty results.
+1-1
docs/src/rustdoc-link/continuous-integration.md
···73737474[^ci-true]:
7575 Specifically, when `CI` is anything other than `""`, `"0"`, or `"false"`. The logic
7676- is encapsulated in the [`is_ci`][crate::env::is_ci] function.
7676+ is encapsulated in the [`is_ci`][crate::error::is_ci] function.
77777878[^stderr]:
7979 Specifically, when stderr is redirected to something that isn't a terminal, such as
+17-17
docs/src/rustdoc-link/name-resolution.md
···1212An item must be **in scope in the entrypoint** for the proprocessor to generate a link
1313for it.
14141515-Let's say you have the following as your `lib.rs`:
1515+For example, with the following as `lib.rs`:
16161717<figure>
18181919```rs
2020use anyhow::Context;
2121-/// Type that can provide links.
2222-pub trait Resolver {}
2323-mod env {
2424- /// Options for the preprocessor.
2525- pub struct Config {}
2121+pub struct Diagnostics {}
2222+mod error {
2323+ pub fn is_ci() {}
2624}
2725```
2826···3129Items in the entrypoint can be linked to with just their names:
32303331> ```md
3434-> [`Resolver`] — Type that can provide links.
3232+> [`Diagnostics`] encapsulates possible issues detected within Markdown sources.
3533>
3636-> This crate also uses the [`Context`] trait from [`anyhow`].
3434+> This crate uses the [`Context`] trait from [`anyhow`].
3735> ```
3836>
3939-> [`Resolver`] — Type that can provide links.
3737+> [`Diagnostics`] encapsulates possible issues detected within Markdown sources.
4038>
4141-> This crate also uses the [`Context`] trait from [`anyhow`].
3939+> This crate uses the [`Context`] trait from [`anyhow`].
42404343-This includes items from the prelude (unless you are using `#![no_implicit_prelude]`):
4141+This includes items from the prelude:
44424543> ```md
4644> [`FromIterator`] is in the prelude starting from Rust 2021.
···5250from others in your Markdown source, you can write `crate::*`:
53515452> ```md
5555-> [Configurations](configuration.md) for the preprocessor is defined in the
5656-> [`Config`][crate::env::Config] type.
5353+> The [`is_ci`][crate::error::is_ci] function detects whether the preprocessor is
5454+> running in a [continuous integration](continuous-integration.md) environment, such
5555+> that warnings may be promoted to errors.
5756> ```
5857>
5959-> [Configurations](configuration.md) for the preprocessor is defined in the
6060-> [`Config`][crate::env::Config] type.
5858+> The [`is_ci`][crate::error::is_ci] function detects whether the preprocessor is
5959+> running in a [continuous integration](continuous-integration.md) environment, such
6060+> that warnings may be promoted to errors.
61616262For everything else, provide its full path, as if you were writing a `use` declaration:
63636464> ```md
6565-> [`JoinSet`][tokio::task::JoinSet] is analogous to `asyncio.as_completed`.
6565+> [`JoinSet`][tokio::task::JoinSet] is analogous to Python's `asyncio.as_completed`.
6666> ```
6767>
6868-> [`JoinSet`][tokio::task::JoinSet] is analogous to `asyncio.as_completed`.
6868+> [`JoinSet`][tokio::task::JoinSet] is analogous to Python's `asyncio.as_completed`.
69697070> [!TIP]
7171>