#![feature(map_try_insert)]
#![feature(mapped_lock_guards)]
use std::sync::{Mutex, MutexGuard, OnceLock};
use camino::Utf8PathBuf;
use napi::bindgen_prelude;
use napi_derive::napi;
use pijul_extension::path_state::{PathState, TrackedState};
use repository::OpenRepositories;
mod inline_credit;
mod repository;
mod vscode_sys;
static EXTENSION_STATE: OnceLock<Mutex<ExtensionState>> = OnceLock::new();
pub const PIJUL_SCHEME: &str = "pijul";
struct ExtensionState {
decoration_type: vscode_sys::reference::TextEditorDecorationTypeRef,
decoration_change_event_emitter: vscode_sys::reference::EventEmitterRef,
repositories: OpenRepositories,
}
impl ExtensionState {
fn get() -> Result<MutexGuard<'static, Self>, napi::Error> {
EXTENSION_STATE
.get()
.ok_or_else(|| napi::Error::from_reason("Extension state is not set"))?
.lock()
.map_err(|_error| napi::Error::from_reason("Extension state mutex has been poisoned"))
}
}
fn provide_file_decoration<'env>(
env: &'env napi::Env,
uri: vscode_sys::Uri,
_cancellation_token: bindgen_prelude::Object,
) -> 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);
};
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))
}
fn provide_text_document_content(
env: &napi::Env,
pijul_uri: vscode_sys::Uri,
_cancellation_token: bindgen_prelude::Object,
) -> Result<Option<String>, napi::Error> {
let program_state = ExtensionState::get()?;
let uri_change = vscode_sys::UriWithChange::new(env)?.scheme("file")?;
let uri = pijul_uri.with(uri_change)?;
if let Some((workspace_path, workspace)) =
program_state.repositories.get_open_repository(env, &uri)?
{
let absolute_path = Utf8PathBuf::from(uri.get_fs_path()?);
let relative_path = absolute_path
.strip_prefix(&workspace_path)
.map_err(|error| {
napi::Error::from_reason(format!(
"Failed to strip prefix {workspace_path} from {absolute_path}: {error}"
))
})?;
match workspace.repository.get_open_file(relative_path) {
Some(open_file) => Ok(open_file.tracked_contents().map(str::to_string)),
None => {
tracing::error!(
message = "No tracked file state for {relative_path}",
?workspace_path
);
Ok(None)
}
}
} else {
tracing::debug!(message = "Ignoring URI", uri = uri.to_string()?);
Ok(None)
}
}
fn provide_original_resource<'env>(
env: &'env napi::Env,
uri: vscode_sys::Uri<'env>,
_cancellation_token: bindgen_prelude::Object,
) -> Result<Option<vscode_sys::Uri<'env>>, napi::Error> {
let uri_change = vscode_sys::UriWithChange::new(env)?.scheme(PIJUL_SCHEME)?;
let pijul_uri = uri.with(uri_change)?;
Ok(Some(pijul_uri))
}
#[tracing::instrument(skip_all)]
fn on_did_change_text_document(
env: &napi::Env,
event: vscode_sys::TextDocumentChangeEvent,
) -> Result<(), napi::Error> {
let text_document = event.get_document()?;
let document_uri = text_document.get_uri()?;
if document_uri.get_scheme()? == "output" {
return Ok(());
}
let mut program_state = ExtensionState::get()?;
if let Some((repository_path, open_repository)) = program_state
.repositories
.get_open_repository_mut(env, &document_uri)?
{
let absolute_document_path = Utf8PathBuf::from(document_uri.get_fs_path()?);
tracing::debug!(?absolute_document_path);
let relative_document_path = absolute_document_path
.strip_prefix(&repository_path)
.map_err(|error| napi::Error::from_reason(format!("Failed to strip prefix {repository_path} from {absolute_document_path}: {error}")))?;
let change_events = event.get_content_changes()?;
for change_event in change_events {
let character_offset = change_event.get_range_offset()?;
let characters_replaced = change_event.get_range_length()?;
let replacement_text = change_event.get_text()?;
open_repository
.repository
.update_open_file(
relative_document_path,
character_offset as usize,
characters_replaced as usize,
&replacement_text,
)
.map_err(|error| {
napi::Error::from_reason(format!(
"Unable to update open file {relative_document_path} ({absolute_document_path}): {error}"
))
})?;
}
let text_editor = open_repository
.get_text_editor(env, relative_document_path)?
.ok_or_else(|| {
napi::Error::from_reason(format!(
"no open text editor for {absolute_document_path}"
))
})?;
inline_credit::render(env, &program_state, &text_editor)?;
}
Ok(())
}
#[tracing::instrument(skip_all)]
fn on_did_change_text_editor_selections(
env: &napi::Env,
event: vscode_sys::TextEditorSelectionChangeEvent,
) -> Result<(), napi::Error> {
let editor = event.get_text_editor()?;
let document = editor.get_document()?;
let uri = document.get_uri()?;
if uri.get_scheme()? == "output" {
return Ok(());
}
let program_state = ExtensionState::get()?;
inline_credit::render(env, &program_state, &editor)
}
#[tracing::instrument(skip_all)]
fn on_did_change_workspace_folders(
env: &napi::Env,
event: vscode_sys::WorkspaceFoldersChangeEvent,
) -> Result<(), napi::Error> {
let mut program_state = ExtensionState::get()?;
let added_workspaces = event.get_added()?;
let removed_workspaces = event.get_removed()?;
for added_workspace in added_workspaces {
program_state
.repositories
.open_workspace_folder(env, added_workspace)?;
}
for removed_workspace in removed_workspaces {
program_state
.repositories
.close_workspace_folder(removed_workspace)?;
}
Ok(())
}
#[tracing::instrument(skip_all)]
fn on_did_change_visible_text_editors(
env: &napi::Env,
visible_text_editors: Vec<vscode_sys::TextEditor>,
) -> Result<(), napi::Error> {
let mut extension_state = ExtensionState::get()?;
extension_state
.repositories
.register_text_editors(env, visible_text_editors)
}
#[tracing::instrument(skip_all)]
pub fn handle_fs_watcher_event(env: &napi::Env, uri: vscode_sys::Uri) -> Result<(), napi::Error> {
let mut extension_state = crate::ExtensionState::get()?;
let decoration_change_event_emitter = extension_state
.decoration_change_event_emitter
.get_inner(env)?;
if let Some((repository_path, open_repository)) = extension_state
.repositories
.get_open_repository_mut(env, &uri)?
{
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}"
))
})?
.to_path_buf();
open_repository
.repository
.update_path_state(relative_file_path)
.map_err(|error| {
napi::Error::from_reason(format!("Failed to update path state: {error:?}"))
})?;
decoration_change_event_emitter.fire(uri.inner)?;
open_repository.update_resource_states(env, &repository_path)?;
}
Ok(())
}
#[napi(catch_unwind)]
pub fn activate(
env: &napi::Env,
vscode_object: bindgen_prelude::Object,
extension_context: bindgen_prelude::Object,
) -> Result<(), napi::Error> {
vscode_sys::activate(&vscode_object, &extension_context)?;
vscode_sys::log::init(
env,
"Pijul",
tracing_subscriber::fmt::format::DefaultFields::new(),
)?;
let mut repositories = OpenRepositories::new();
for workspace_folder in vscode_sys::workspace::get_workspace_folders(env)? {
repositories.open_workspace_folder(env, workspace_folder)?;
}
let visible_text_editors = vscode_sys::window::get_visible_text_editors(env)?;
repositories.register_text_editors(env, visible_text_editors)?;
let decoration_type = inline_credit::create_decoration_type(env)?;
let decoration_change_event_emitter = vscode_sys::EventEmitter::new(env)?;
let decoration_change_event = decoration_change_event_emitter.get_event()?;
let extension_state = ExtensionState {
decoration_type: decoration_type.create_ref()?,
decoration_change_event_emitter: decoration_change_event_emitter.create_ref()?,
repositories,
};
if let Some(active_text_editor) = vscode_sys::window::get_active_text_editor(env)? {
inline_credit::render(env, &extension_state, &active_text_editor)?;
}
EXTENSION_STATE
.set(Mutex::new(extension_state))
.map_err(|_existing_object| {
napi::Error::from_reason("Extension state has already been set")
})?;
let mut file_decoration_provider =
vscode_sys::FileDecorationProvider::new(env, provide_file_decoration)?;
file_decoration_provider.set_on_did_change_file_decorations(decoration_change_event)?;
vscode_sys::window::register_file_decoration_provider(
env,
&extension_context,
file_decoration_provider,
)?;
let text_document_provider =
vscode_sys::TextDocumentContentProvider::new(env, provide_text_document_content)?;
vscode_sys::workspace::register_text_document_content_provider(
env,
&extension_context,
PIJUL_SCHEME,
text_document_provider,
)?;
let mut quick_diff_provider = vscode_sys::QuickDiffProvider::new(env)?;
quick_diff_provider.set_provide_original_resource(env, provide_original_resource)?;
let program_state = ExtensionState::get()?;
for (_repository_path, open_repository) in program_state.repositories.iter_repositories() {
let mut source_control = open_repository.source_control.get_inner(env)?;
source_control.set_quick_diff_provider(quick_diff_provider)?;
}
vscode_sys::window::on_did_change_text_editor_selections(
env,
on_did_change_text_editor_selections,
)?;
vscode_sys::window::on_did_change_visible_text_editors(
env,
on_did_change_visible_text_editors,
)?;
vscode_sys::workspace::on_did_change_text_document(env, on_did_change_text_document)?;
vscode_sys::workspace::on_did_change_workspace_folders(env, on_did_change_workspace_folders)?;
let file_system_watcher = vscode_sys::workspace::create_file_system_watcher(env, "**")?;
file_system_watcher.on_did_change(env, handle_fs_watcher_event)?;
file_system_watcher.on_did_create(env, handle_fs_watcher_event)?;
file_system_watcher.on_did_delete(env, handle_fs_watcher_event)?;
tracing::info!("Extension activated");
Ok(())
}