Refactor `provide_file_decoration` to return a `Promise` resolved by `event_loop`

finchie
Jan 1, 2026, 4:07 PM
3RNQI5RXZMXF5N2WKYS3O3G2GTD4TTWDNQ3EBVGUVVKNNOTKQTJAC

Dependencies

  • [2] WFWTKCJN Create initial Visual Studio Code extension
  • [3] LQJG2LGQ Refactor `PathState` handling to be updated incrementally
  • [4] 3YGYMEXV Create `event_loop` module
  • [5] LSZTPUQT Upgrade `napi` to latest version
  • [6] NB2MF3MY Add `OpenWorkspaceFolder` event
  • [7] MGJ23FHF Assign repository URI to source control in `OpenWorkspaceFolder` events
  • [8] OUADGWKR Create fully-initialized `SourceControl` object in `event_loop`
  • [9] 2ZAM5V35 Move event handling into modules
  • [10] SLTXBK5G Recursively discover repositories when handling `OpenWorkspaceFolder` event
  • [11] QY4Z5ZXZ Refactor `event_loop/threadsafe_function` file into `event_loop::js_function` module
  • [12] IDY5SNLO Update source control resource states in event loop
  • [13] T4NQUSRP Emit events on file decoration change
  • [14] 72K45XKD Refactor inline credit to improve hover messages
  • [15] TWPZLEGD Return correct file contents for quick diff

Change contents

  • edit in editors/vscode/src/lib.rs at line 19
    [2.109332]
    [2.109332]
    use iri_string::types::UriAbsoluteString;
  • edit in editors/vscode/src/lib.rs at line 22
    [2.109382][3.11100:11160]()
    use pijul_extension::path_state::{PathState, TrackedState};
  • edit in editors/vscode/src/lib.rs at line 53
    [2.110111]
    [2.110111]
    #[tracing::instrument(skip_all)]
  • replacement in editors/vscode/src/lib.rs at line 58
    [2.110247][2.110247:111908]()
    ) -> Result<Option<vscode_sys::FileDecoration<'env>>, napi::Error> {
    let program_state = ExtensionState::get()?;
    let Some((repository_path, open_repository)) =
    program_state.repositories.get_open_repository(env, &uri)?
    else {
    return Ok(None);
    };
    let absolute_file_path = Utf8PathBuf::from(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 Some(path_state) = open_repository
    .repository
    .get_path_state(relative_file_path)
    else {
    return Ok(None);
    };
    // TODO: l10n_embed
    let (badge, tooltip, color_id) = match path_state {
    PathState::Untracked => ("U", "Untracked", "pijul.decorations.path.untracked"),
    PathState::Tracked(modified_state) => match modified_state {
    TrackedState::Added => ("A", "Added", "pijul.decorations.path.added"),
    TrackedState::Removed => ("RM", "Removed", "pijul.decorations.path.removed"),
    TrackedState::Modified => ("M", "Modified", "pijul.decorations.path.modified"),
    TrackedState::Moved => ("MV", "Moved", "pijul.decorations.path.moved"),
    TrackedState::ModifiedAndMoved => (
    "M,MV",
    "Modified, Moved",
    "pijul.decorations.path.modifiedAndMoved",
    ),
    },
    };
    let color = vscode_sys::ThemeColor::new(env, color_id)?;
    [2.110247]
    [2.111908]
    ) -> Result<bindgen_prelude::Object<'env>, napi::Error> {
    let (deferred_promise, promise_object) = env.create_deferred()?;
    let parsed_uri = UriAbsoluteString::try_from(uri.to_string()?)
    .map_err(|error| napi::Error::from_reason(format!("Failed to parse URI: {error}")))?;
    event_loop::send(Event::RequestFileDecoration {
    uri: parsed_uri,
    deferred_promise,
    });
  • replacement in editors/vscode/src/lib.rs at line 67
    [2.111909][2.111909:112028]()
    let path_decoration = vscode_sys::FileDecoration::new(env, badge, tooltip, &color)?;
    Ok(Some(path_decoration))
    [2.111909]
    [2.112028]
    Ok(promise_object)
  • replacement in editors/vscode/src/event_loop/mod.rs at line 5
    [4.198][7.1402:1427]()
    use camino::Utf8PathBuf;
    [4.198]
    [4.198]
    use camino::{Utf8Path, Utf8PathBuf};
    use iri_string::types::UriAbsoluteStr;
  • edit in editors/vscode/src/event_loop/mod.rs at line 27
    [12.179]
    [6.1618]
    }
    struct ExtensionState {
    repositories: HashMap<Utf8PathBuf, Repository>,
    }
    impl ExtensionState {
    #[tracing::instrument(skip(self))]
    fn find_repository<'uri>(
    &'uri self,
    uri: &'uri UriAbsoluteStr,
    ) -> Option<(&'uri Utf8Path, &'uri Repository)> {
    if uri.scheme_str() != "file" {
    return None;
    }
    for ancestor in Utf8Path::new(uri.path_str()).ancestors() {
    if let Some(repository) = self.repositories.get(ancestor) {
    return Some((ancestor, repository));
    }
    }
    None
    }
  • replacement in editors/vscode/src/event_loop/mod.rs at line 61
    [8.2941][8.2941:3018]()
    let mut repositories: HashMap<Utf8PathBuf, Repository> = HashMap::new();
    [8.2941]
    [4.560]
    let mut extension_state = ExtensionState {
    repositories: HashMap::new(),
    };
  • replacement in editors/vscode/src/event_loop/mod.rs at line 81
    [12.470][12.470:513]()
    &mut repositories,
    [12.470]
    [12.513]
    &mut extension_state,
  • edit in editors/vscode/src/event_loop/mod.rs at line 87
    [6.2537]
    [12.639]
    Event::RequestFileDecoration {
    uri,
    deferred_promise,
    } => {
    event::request_file_decoration::handle(uri, deferred_promise, &extension_state)
    .await
    }
  • replacement in editors/vscode/src/event_loop/mod.rs at line 97
    [12.808][12.808:847]()
    &repositories,
    [12.808]
    [12.847]
    &extension_state,
  • edit in editors/vscode/src/event_loop/mod.rs at line 105
    [4.935][4.935:976]()
    tracing::debug!(?event_buffer);
  • edit in editors/vscode/src/event_loop/event/update_resource_states.rs at line 1
    [12.4333][12.4334:4366]()
    use std::collections::HashMap;
  • replacement in editors/vscode/src/event_loop/event/update_resource_states.rs at line 3
    [12.4392][12.4392:4427]()
    use crate::event_loop::Repository;
    [12.4392]
    [12.4427]
    use crate::event_loop::ExtensionState;
  • replacement in editors/vscode/src/event_loop/event/update_resource_states.rs at line 6
    [12.4475][12.4475:4532]()
    #[tracing::instrument(skip(repositories, js_functions))]
    [12.4475]
    [12.4532]
    #[tracing::instrument(skip(extension_state, js_functions))]
  • replacement in editors/vscode/src/event_loop/event/update_resource_states.rs at line 9
    [12.4587][12.4587:4640]()
    repositories: &HashMap<Utf8PathBuf, Repository>,
    [12.4587]
    [12.4640]
    extension_state: &ExtensionState,
  • replacement in editors/vscode/src/event_loop/event/update_resource_states.rs at line 12
    [12.4674][12.4674:4743]()
    let Some(repository) = repositories.get(&repository_path) else {
    [12.4674]
    [12.4743]
    let Some(repository) = extension_state.repositories.get(&repository_path) else {
  • file addition: request_file_decoration.rs (----------)
    [9.332]
    use camino::Utf8Path;
    use iri_string::types::{UriAbsoluteStr, UriAbsoluteString};
    use pijul_extension::path_state::{PathState, TrackedState};
    use crate::event_loop::ExtensionState;
    use crate::vscode_sys;
    pub type DeferredPromise = napi::JsDeferred<
    Option<vscode_sys::reference::FileDecorationRef>,
    Box<
    dyn FnOnce(
    napi::Env,
    )
    -> Result<Option<vscode_sys::reference::FileDecorationRef>, napi::Error>,
    >,
    >;
    #[tracing::instrument(skip(deferred_promise, extension_state))]
    pub async fn handle(
    uri: UriAbsoluteString,
    deferred_promise: DeferredPromise,
    extension_state: &ExtensionState,
    ) {
    let optional_path_state = get_decoration(&uri, extension_state);
    deferred_promise.resolve(Box::new(move |env| {
    let Some(path_state) = optional_path_state else {
    return Ok(None);
    };
    // TODO: l10n_embed
    let (badge, tooltip, color_id) = match path_state {
    PathState::Untracked => ("U", "Untracked", "pijul.decorations.path.untracked"),
    PathState::Tracked(modified_state) => match modified_state {
    TrackedState::Added => ("A", "Added", "pijul.decorations.path.added"),
    TrackedState::Removed => ("RM", "Removed", "pijul.decorations.path.removed"),
    TrackedState::Modified => ("M", "Modified", "pijul.decorations.path.modified"),
    TrackedState::Moved => ("MV", "Moved", "pijul.decorations.path.moved"),
    TrackedState::ModifiedAndMoved => (
    "M,MV",
    "Modified, Moved",
    "pijul.decorations.path.modifiedAndMoved",
    ),
    },
    };
    let color = vscode_sys::ThemeColor::new(&env, color_id)?;
    let path_decoration = vscode_sys::FileDecoration::new(&env, badge, tooltip, &color)?;
    Ok(Some(path_decoration.create_ref()?))
    }));
    }
    fn get_decoration(uri: &UriAbsoluteStr, extension_state: &ExtensionState) -> Option<PathState> {
    if uri.scheme_str() != "file" {
    return None;
    }
    let Some((repository_path, repository)) = extension_state.find_repository(uri) else {
    tracing::info!(message = "No matching repository for path", ?uri);
    return None;
    };
    let decoration_path = match Utf8Path::new(uri.path_str()).strip_prefix(repository_path) {
    Ok(decoration_path) => decoration_path,
    Err(error) => {
    tracing::error!(
    message = "Failed to strip repository prefix from path",
    ?repository_path,
    ?uri,
    ?error
    );
    return None;
    }
    };
    repository.repository.get_path_state(decoration_path)
    }
  • replacement in editors/vscode/src/event_loop/event/open_workspace_folder.rs at line 9
    [11.2114][12.5269:5313]()
    use crate::event_loop::{Event, Repository};
    [11.2114]
    [9.581]
    use crate::event_loop::{Event, ExtensionState, Repository};
  • replacement in editors/vscode/src/event_loop/event/open_workspace_folder.rs at line 11
    [9.582][11.2115:2172]()
    #[tracing::instrument(skip(repositories, js_functions))]
    [9.582]
    [9.647]
    #[tracing::instrument(skip(extension_state, js_functions, sender))]
  • replacement in editors/vscode/src/event_loop/event/open_workspace_folder.rs at line 14
    [12.5335][9.687:744](),[9.687][9.687:744]()
    repositories: &mut HashMap<Utf8PathBuf, Repository>,
    [12.5335]
    [11.2173]
    extension_state: &mut ExtensionState,
  • replacement in editors/vscode/src/event_loop/event/open_workspace_folder.rs at line 82
    [10.1860][10.1860:1916]()
    repositories.entry(repository_path.clone())
    [10.1860]
    [10.1916]
    extension_state.repositories.entry(repository_path.clone())
  • edit in editors/vscode/src/event_loop/event/mod.rs at line 2
    [12.5705]
    [12.5705]
    use iri_string::types::UriAbsoluteString;
  • edit in editors/vscode/src/event_loop/event/mod.rs at line 5
    [9.3099]
    [12.5707]
    pub mod request_file_decoration;
  • edit in editors/vscode/src/event_loop/event/mod.rs at line 8
    [9.3100][9.3100:3117]()
    #[derive(Debug)]
  • replacement in editors/vscode/src/event_loop/event/mod.rs at line 9
    [9.3134][9.3134:3175](),[9.3175][12.5740:5799]()
    OpenWorkspaceFolder { uri: String },
    UpdateResourceStates { repository_path: Utf8PathBuf },
    [9.3134]
    [9.3175]
    OpenWorkspaceFolder {
    uri: String,
    },
    RequestFileDecoration {
    uri: UriAbsoluteString,
    deferred_promise: request_file_decoration::DeferredPromise,
    },
    UpdateResourceStates {
    repository_path: Utf8PathBuf,
    },
  • replacement in Cargo.toml at line 42
    [2.177003][5.896:945]()
    napi = { version = "3.7", features = ["napi5"] }
    [2.177003]
    [2.177052]
    napi = { version = "3.7", features = ["napi5", "deferred_trace"] }