Refactor inline credit to improve hover messages
Dependencies
- [2]
WFWTKCJNCreate initial Visual Studio Code extension
Change contents
- file addition: templates[2.53768]
- file addition: hover[0.21]
- file addition: untracked.html[0.40]
- file addition: tracked.html[0.40]
<strong>{{ title }}</strong>{{ authors }} • {{ relative_timestamp }}<hr /><table><tbody><tr><th>{{ change_id_header }}</th><td><code>{{ change_id }}</code></td></tr><tr><th>{{ absolute_timestamp_header }}</th><td>{{ absolute_timestamp }}</td></tr></tbody></table>{% if let Some(description) = description %}{{ description }}{% endif %} - edit in extensions/vscode/src/vscode_sys/mod.rs at line 91
field_setter! {MarkdownString {"supportHtml": set_support_html(bool);}} - edit in extensions/vscode/src/lib.rs at line 16
// TODO: warn instead of returning an error for non-fatal errors - replacement in extensions/vscode/src/lib.rs at line 27
mod decorations;mod inline_credit; - replacement in extensions/vscode/src/lib.rs at line 211
let selections = text_editor.get_selections()?;decorations::inline_credit::render(env, &program_state, &text_editor, selections)?;inline_credit::render(env, &program_state, &text_editor)?; - replacement in extensions/vscode/src/lib.rs at line 233
let selections = event.get_selections()?;decorations::inline_credit::render(env, &program_state, &editor, selections)inline_credit::render(env, &program_state, &editor) - replacement in extensions/vscode/src/lib.rs at line 302
let decoration_type = decorations::create_decoration_type(env)?;let decoration_type = inline_credit::create_decoration_type(env)?; - replacement in extensions/vscode/src/lib.rs at line 310
let selections = active_text_editor.get_selections()?;decorations::inline_credit::render(env, &extension_state, &active_text_editor, selections)?;inline_credit::render(env, &extension_state, &active_text_editor)?; - file move: decorations → inline_credit
- replacement in extensions/vscode/src/inline_credit/mod.rs at line 1
use crate::vscode_sys;use camino::Utf8PathBuf;use icu_locale::locale;use crate::{ExtensionState, vscode_sys}; - replacement in extensions/vscode/src/inline_credit/mod.rs at line 6
pub mod inline_credit;pub mod hover;pub mod line_annotation; - edit in extensions/vscode/src/inline_credit/mod.rs at line 25
}// TODO: different rendering modes? e.g. render only at boundaries between lines, render only on cursors, etcpub fn render(env: &napi::Env,program_state: &ExtensionState,editor: &vscode_sys::TextEditor,) -> Result<(), napi::Error> {let editor_document = editor.get_document()?;let document_uri = editor_document.get_uri()?;let Some((repository_path, open_repository)) = program_state.repositories.get_open_repository(env, &document_uri)?else {tracing::warn!(message = "No repository for URI",uri = document_uri.to_string()?);return Ok(());};let absolute_file_path = Utf8PathBuf::from(document_uri.get_fs_path()?);let relative_file_path =absolute_file_path.strip_prefix(&repository_path).map_err(|error| {napi::Error::from_reason(format!("Failed to strip prefix {repository_path} from {absolute_file_path}: {error}"))})?;let open_file = open_repository.repository.get_open_file(relative_file_path).ok_or_else(|| {napi::Error::from_reason(format!("unable to get open file for {relative_file_path}"))})?;// TODO: don't initialize herelet localization_context = l10n_embed::Context::new(locale!("en-US"), false);let mut decoration_options_list = Vec::new();for editor_selection in editor.get_selections()? {let start_line = editor_selection.get_start()?.get_line()?;let end_line: u32 = editor_selection.get_end()?.get_line()?;let selected_range = (start_line as usize)..=(end_line as usize);let credits = open_file.credit(selected_range);for credit_span in credits {for line in credit_span.lines {let annotation_range = line_annotation::range(env, open_file, line)?;let line_annotation_text = line_annotation::render(&localization_context,&open_repository.repository,open_file,&credit_span.value,)?;let hover_message = hover::render(env,&localization_context,&open_repository.repository,&credit_span.value,)?;let after = vscode_sys::ThemableDecorationAttachmentRenderOptions::new(env)?.content_text(&line_annotation_text)?;let render_options =vscode_sys::DecorationInstanceRenderOptions::new(env)?.after(after)?;let decoration_options =vscode_sys::DecorationOptions::new(env, &annotation_range)?.hover_message(&hover_message)?.render_options(render_options)?;decoration_options_list.push(decoration_options);}}}let decoration_type = program_state.decoration_type.get_inner(env)?;editor.set_decorations(decoration_type, decoration_options_list)?;Ok(()) - file move: inline_credit.rs → line_annotation.rs
- edit in extensions/vscode/src/inline_credit/line_annotation.rs at line 1
// TODO: better, localized hover messages - edit in extensions/vscode/src/inline_credit/line_annotation.rs at line 2
use std::ops::RangeInclusive; - edit in extensions/vscode/src/inline_credit/line_annotation.rs at line 3
use camino::Utf8PathBuf;use icu_locale::locale; - edit in extensions/vscode/src/inline_credit/line_annotation.rs at line 6
use napi::bindgen_prelude::JsObjectValue; - edit in extensions/vscode/src/inline_credit/line_annotation.rs at line 8
use pijul_extension::file_system::changes::Span; - edit in extensions/vscode/src/inline_credit/line_annotation.rs at line 10
use crate::ExtensionState; - replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 12
#[localize("l10n/**/inline_credit.ftl")]#[localize("l10n/**/inline_credit/line_annotation.ftl")] - replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 23
#[localize("l10n/**/inline_credit.ftl")]enum InlineCreditAnnotation<'change_header> {#[localize("l10n/**/inline_credit/line_annotation.ftl")]enum LineAnnotation<'change_header> { - replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 35
fn get_lines_selected(editor_selections: Vec<vscode_sys::Selection>,) -> Result<Vec<RangeInclusive<usize>>, napi::Error> {let mut credits = Vec::with_capacity(editor_selections.len());pub fn range<'env>(env: &'env napi::Env,open_file: &OpenFile,line: usize,) -> Result<vscode_sys::Range<'env>, napi::Error> {let current_line_contents = open_file.contents.text.get_line(line, ropey::LineType::LF_CR).ok_or_else(|| napi::Error::from_reason(format!("no contents for line {line}")))?;let final_character_offset = current_line_contents.len() as u32; - replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 47
for editor_selection in &editor_selections {let start_line = editor_selection.get_start()?.get_line()?;let end_line: u32 = editor_selection.get_end()?.get_line()?;credits.push((start_line as usize)..=(end_line as usize));}let start = vscode_sys::Position::new(env, line as u32, final_character_offset)?;let end = vscode_sys::Position::new(env, line as u32, final_character_offset)?; - replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 50
Ok(credits)vscode_sys::Range::new(env, &start, &end) - replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 53
fn decoration_options<'env>(env: &napi::Env,pub fn render( - replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 57
credits: impl Iterator<Item = Span<CreditSource>>,) -> Result<Vec<vscode_sys::DecorationOptions<'env>>, napi::Error> {let mut decoration_options_list = Vec::new();for credit in credits {tracing::debug!(?credit);let annotation = match credit.value {CreditSource::Tracked(change_id) => {let change_header = repository.get_change_header(&change_id).ok_or_else(|| {napi::Error::from_reason(format!("unable to get change {change_id:#?}"))})?;let authors = repository.authors_for_change(change_header).into_iter().map(|author_source| match author_source {pijul_extension::author::AuthorSource::Local(_identity) => {AuthorKind::Local}pijul_extension::author::AuthorSource::Remote(identity) => {AuthorKind::Remote {username: &identity.config.author.username,}}pijul_extension::author::AuthorSource::Unknown(public_key_signature) => {AuthorKind::Unknown {public_key_signature,}credit_source: &CreditSource,) -> Result<String, napi::Error> {let annotation = match credit_source {CreditSource::Tracked(change_id) => {let change_header = repository.get_change_header(change_id).ok_or_else(|| {napi::Error::from_reason(format!("unable to get change {change_id:#?}"))})?;let authors = repository.authors_for_change(change_header).into_iter().map(|author_source| match author_source {pijul_extension::author::AuthorSource::Local(_identity) => AuthorKind::Local,pijul_extension::author::AuthorSource::Remote(identity) => AuthorKind::Remote {username: &identity.config.author.username,},pijul_extension::author::AuthorSource::Unknown(public_key_signature) => {AuthorKind::Unknown {public_key_signature, - replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 76
}).collect();}}).collect(); - replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 80
InlineCreditAnnotation::Tracked {authors: l10n_embed::list::AndList::new(authors),timestamp: change_header.timestamp,message: &change_header.message,}LineAnnotation::Tracked {authors: l10n_embed::list::AndList::new(authors),timestamp: change_header.timestamp,message: &change_header.message, - edit in extensions/vscode/src/inline_credit/line_annotation.rs at line 85
CreditSource::Untracked => InlineCreditAnnotation::Untracked {timestamp: Timestamp::try_from(open_file.contents.modified_time).map_err(|error| {napi::Error::from_reason(format!("Failed to parse timestamp {:#?}: {error}",open_file.contents.modified_time))},)?,},};let mut localized_annotation = String::new();annotation.localize(localization_context, &mut localized_annotation);let hover_message_text = match credit.value {CreditSource::Tracked(change_id) => {let change_header = repository.get_change_header(&change_id).ok_or_else(|| {napi::Error::from_reason(format!("unable to get change {change_id:#?}"))})?;match &change_header.description {Some(description) => {format!("<h2>{}</h2><p>{}</p>", change_header.message, description)}None => change_header.message.clone(),}}CreditSource::Untracked => String::new(),};for line in credit.lines {let current_line_contents = open_file.contents.text.get_line(line, ropey::LineType::LF_CR).ok_or_else(|| napi::Error::from_reason(format!("no contents for line {line}")))?;let final_character_offset = current_line_contents.len() as u32;let start = vscode_sys::Position::new(env, line as u32, final_character_offset)?;let end = vscode_sys::Position::new(env, line as u32, final_character_offset)?;let range = vscode_sys::Range::new(env, &start, &end)?;let mut hover_message =vscode_sys::MarkdownString::new(env, &hover_message_text, false)?;hover_message.inner.set_named_property("supportHtml", true)?;let after = vscode_sys::ThemableDecorationAttachmentRenderOptions::new(env)?.content_text(&localized_annotation)?;let render_options =vscode_sys::DecorationInstanceRenderOptions::new(env)?.after(after)?;let decoration_options = vscode_sys::DecorationOptions::new(env, &range)?.hover_message(&hover_message)?.render_options(render_options)?;decoration_options_list.push(decoration_options); - replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 86
}Ok(decoration_options_list)}// TODO: different rendering modes? e.g. render only at boundaries between lines, render only on cursors, etcpub fn render(env: &napi::Env,program_state: &ExtensionState,editor: &vscode_sys::TextEditor,selections: Vec<vscode_sys::Selection>,) -> Result<(), napi::Error> {let editor_document = editor.get_document()?;let document_uri = editor_document.get_uri()?;let Some((repository_path, open_repository)) = program_state.repositories.get_open_repository(env, &document_uri)?else {return Ok(());};let absolute_file_path = Utf8PathBuf::from(document_uri.get_fs_path()?);let relative_file_path =absolute_file_path.strip_prefix(&repository_path).map_err(|error| {CreditSource::Untracked => LineAnnotation::Untracked {// TODO: return the current time if `TextDocument::get_is_dirty()`timestamp: Timestamp::try_from(open_file.contents.modified_time).map_err(|error| { - replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 90
"Failed to strip prefix {repository_path} from {absolute_file_path}: {error}""Failed to parse timestamp {:#?}: {error}",open_file.contents.modified_time - replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 93
})?;let localization_context = l10n_embed::Context::new(locale!("en-US"), false);let lines_selected = get_lines_selected(selections)?;let open_file = open_repository.repository.get_open_file(relative_file_path).ok_or_else(|| {napi::Error::from_reason(format!("unable to get open file for {relative_file_path}"))})?;let credits = lines_selected.into_iter().flat_map(|line_range| open_file.credit(line_range));let decoration_options = decoration_options(env,&localization_context,&open_repository.repository,open_file,credits,)?;})?,},}; - replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 97
let decoration_type = program_state.decoration_type.get_inner(env)?;editor.set_decorations(decoration_type, decoration_options)?;let mut localized_annotation = String::new();annotation.localize(localization_context, &mut localized_annotation); - replacement in extensions/vscode/src/inline_credit/line_annotation.rs at line 100
Ok(())Ok(localized_annotation) - file addition: hover.rs[2.122472]
use std::borrow::Cow;use askama::Template;use icu_datetime::{DateTimeFormatter, DateTimeFormatterPreferences};use jiff::tz::TimeZone;use jiff_icu::ConvertFrom;use l10n_embed::Localize;use l10n_embed_derive::localize;use libpijul::Base32;use pijul_extension::FileSystemRepository;use pijul_extension::author::AuthorSource;use pijul_extension::file_system::changes::CreditSource;use crate::vscode_sys;#[localize("l10n/**/inline_credit/hover.ftl")]enum HoverHeading {AbsoluteTimestamp,ChangeId,}#[derive(askama::Template)]#[template(path = "hover/tracked.html")]struct TrackedHoverTemplate<'change_header> {title: &'change_header str,authors: String,relative_timestamp: String,absolute_timestamp_header: String,absolute_timestamp: String,change_id_header: String,// TODO: color/emphasize unique portion of ID (e.g. *ABC*DEFGHIJKL)change_id: String,description: Option<&'change_header str>,}#[derive(askama::Template)]#[template(path = "hover/untracked.html")]struct UntrackedHoverTemplate;pub fn render<'env>(env: &'env napi::Env,localization_context: &l10n_embed::Context,repository: &FileSystemRepository,credit_source: &CreditSource,) -> Result<vscode_sys::MarkdownString<'env>, napi::Error> {let rendered_html_result = match credit_source {CreditSource::Tracked(change_id) => {let change_header = repository.get_change_header(change_id).ok_or_else(|| {napi::Error::from_reason(format!("unable to get change {change_id:#?}"))})?;let mut authors = String::new();let authors_list = l10n_embed::list::AndList::new(repository.authors_for_change(change_header).into_iter().map(|author_source| match author_source {AuthorSource::Local(identity) | AuthorSource::Remote(identity) => {Cow::from(format!("{} ({})",identity.config.author.display_name,identity.config.author.username))}AuthorSource::Unknown(public_key_hash) => Cow::from(public_key_hash),}).collect(),);authors_list.localize(localization_context, &mut authors);let mut relative_timestamp = String::new();change_header.timestamp.localize(localization_context, &mut relative_timestamp);let mut absolute_timestamp_header = String::new();let mut change_id_header = String::new();HoverHeading::AbsoluteTimestamp.localize(localization_context, &mut absolute_timestamp_header);HoverHeading::ChangeId.localize(localization_context, &mut change_id_header);// TODO: properly localize using l10n_embedlet zoned_timestamp = change_header.timestamp.to_zoned(TimeZone::system());let icu_timestamp = icu_time::ZonedDateTime::<icu_calendar::Iso,icu_time::TimeZoneInfo<icu_time::zone::models::Full>,>::convert_from(&zoned_timestamp);let absolute_timestamp_formatter = DateTimeFormatter::try_new(DateTimeFormatterPreferences::from(&localization_context.locale),icu_datetime::fieldsets::YMDT::medium().with_zone(icu_datetime::fieldsets::zone::SpecificShort),).map_err(|error| {napi::Error::from_reason(format!("Failed to create datetime formatter: {error}"))})?;let absolute_timestamp = absolute_timestamp_formatter.format(&icu_timestamp).to_string();let hover_template = TrackedHoverTemplate {title: &change_header.message,authors,relative_timestamp,absolute_timestamp_header,absolute_timestamp,change_id_header,change_id: change_id.to_base32(),description: change_header.description.as_deref(),};hover_template.render()}CreditSource::Untracked => {let hover_template = UntrackedHoverTemplate;hover_template.render()}};let rendered_html = rendered_html_result.map_err(|error| {napi::Error::from_reason(format!("Failed to render hover message: {error}"))})?;let mut hover_message = vscode_sys::MarkdownString::new(env, &rendered_html, false)?;hover_message.set_support_html(true)?;Ok(hover_message)} - file addition: inline_credit[2.175094]
- file move: inline_credit.ftl → line_annotation.ftl
- file addition: hover.ftl[0.11982]
absolute-timestamp = Timestampchange-id = Change ID - edit in extensions/vscode/Cargo.toml at line 20
askama.workspace = true - edit in extensions/vscode/Cargo.toml at line 22
icu_calendar.workspace = trueicu_datetime.workspace = true - edit in extensions/vscode/Cargo.toml at line 25
icu_time.workspace = true - replacement in extensions/vscode/Cargo.toml at line 27
l10n_embed.workspace = truejiff-icu.workspace = true - replacement in extensions/vscode/Cargo.toml at line 29
napi.workspace = truel10n_embed.workspace = truelibpijul.workspace = true - edit in extensions/vscode/Cargo.toml at line 33
napi.workspace = true - edit in extensions/vscode/Cargo.toml at line 35
tracing-subscriber.workspace = true - edit in extensions/vscode/Cargo.toml at line 37
tracing-subscriber.workspace = true - edit in Cargo.toml at line 24
askama = "0.14" - edit in Cargo.toml at line 29
icu_calendar = "2.0"icu_datetime = "2.0" - edit in Cargo.toml at line 32
icu_time = "2.0" - edit in Cargo.toml at line 34
jiff-icu = "0.2" - edit in Cargo.lock at line 124
[[package]]name = "askama"version = "0.14.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4"dependencies = ["askama_derive","itoa","percent-encoding","serde","serde_json",][[package]]name = "askama_derive"version = "0.14.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f"dependencies = ["askama_parser","basic-toml","memchr","proc-macro2","quote","rustc-hash","serde","serde_derive","syn",] - edit in Cargo.lock at line 156
name = "askama_parser"version = "0.14.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358"dependencies = ["memchr","serde","serde_derive","winnow",][[package]] - edit in Cargo.lock at line 217
[[package]]name = "basic-toml"version = "0.1.10"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"dependencies = ["serde",] - edit in Cargo.lock at line 366
name = "calendrical_calculations"version = "0.2.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7"dependencies = ["core_maths","displaydoc",][[package]] - edit in Cargo.lock at line 590
[[package]]name = "core_maths"version = "0.1.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"dependencies = ["libm",] - edit in Cargo.lock at line 1302
name = "icu_calendar"version = "2.0.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "362941891d17750e05cd8fdfca4a89a86552825d625a937020ee1a65580da1f9"dependencies = ["calendrical_calculations","displaydoc","icu_calendar_data","icu_locale","icu_locale_core","icu_provider","ixdtf","tinystr","writeable","zerovec",][[package]]name = "icu_calendar_data"version = "2.0.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7219c8639ab936713a87b571eed2bc2615aa9137e8af6eb221446ee5644acc18"[[package]] - edit in Cargo.lock at line 1359
"zerovec",][[package]]name = "icu_datetime"version = "2.0.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "eea7ff7a9d7ce8f419930ec75b9821807cdbde7ec5f0157c322773a6ede6125f"dependencies = ["displaydoc","either","fixed_decimal","icu_calendar","icu_datetime_data","icu_decimal","icu_locale","icu_locale_core","icu_pattern","icu_plurals","icu_provider","icu_time","potential_utf","smallvec","tinystr","writeable", - edit in Cargo.lock at line 1386
[[package]]name = "icu_datetime_data"version = "2.0.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "83791ac10bb7b774f130bb81fa89c4059de710dcef53caa0b86e645212d6d54c" - edit in Cargo.lock at line 1609
name = "icu_time"version = "2.0.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b49a81ac920c1a93e3165a5f16d16f5c15d9f19fc5dc1223c00af35f7a7c2bb6"dependencies = ["calendrical_calculations","displaydoc","icu_calendar","icu_locale_core","icu_provider","icu_time_data","ixdtf","serde","tinystr","writeable","zerotrie","zerovec",][[package]]name = "icu_time_data"version = "2.0.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8472be4410d26a03d7208cae3a76c798dd6766e8226ab977cd8b2d349a6dbf08"[[package]] - edit in Cargo.lock at line 1769
name = "ixdtf"version = "0.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8289f7f711a1a51f80e2e368355d023042ca55d8d554fd5e953f01464c15842d"dependencies = ["displaydoc",][[package]] - edit in Cargo.lock at line 1793
name = "jiff-icu"version = "0.2.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0e67c2beaae8b10a82d849b9aabb698a43a682f32b17bcdc035d5ecadb44d646"dependencies = ["icu_calendar","icu_time","jiff",][[package]] - edit in Cargo.lock at line 1924
[[package]]name = "libm"version = "0.2.15"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - edit in Cargo.lock at line 2558
"askama", - edit in Cargo.lock at line 2560
"icu_calendar","icu_datetime", - edit in Cargo.lock at line 2563
"icu_time", - edit in Cargo.lock at line 2565
"jiff-icu", - edit in Cargo.lock at line 2568
"libpijul",