use std::collections::HashMap;
use std::rc::Rc;
use std::sync::OnceLock;
use camino::{Utf8Path, Utf8PathBuf};
use iri_string::types::UriAbsoluteStr;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
use crate::vscode_sys;
pub use event::{EditorContentsChange, Event};
mod event;
mod js_function;
static EVENT_SENDER: OnceLock<UnboundedSender<Event>> = OnceLock::new();
const EVENT_BATCH_LIMIT: usize = 8;
struct Repository {
repository: pijul_extension::FileSystemRepository,
source_control: vscode_sys::reference::SourceControlRef,
open_editors: HashMap<Utf8PathBuf, Rc<vscode_sys::reference::TextEditorRef>>,
unrecorded_changes: Rc<vscode_sys::reference::SourceControlResourceGroupRef>,
untracked_paths: Rc<vscode_sys::reference::SourceControlResourceGroupRef>,
}
struct ExtensionState {
decoration_change_event_emitter: Rc<vscode_sys::reference::EventEmitterRef>,
decoration_type: Rc<vscode_sys::reference::TextEditorDecorationTypeRef>,
localization_context: Rc<l10n_embed::Context>,
quick_diff_provider: Rc<vscode_sys::reference::QuickDiffProviderRef>,
repositories: HashMap<Utf8PathBuf, Repository>,
}
impl ExtensionState {
#[tracing::instrument(skip(self))]
fn find_repository_root<'uri>(
&self,
uri: &'uri UriAbsoluteStr,
) -> Option<(&'uri Utf8Path, &'uri Utf8Path)> {
if uri.scheme_str() != "file" {
return None;
}
let uri_path = Utf8Path::new(uri.path_str());
for ancestor in uri_path.ancestors() {
if self.repositories.contains_key(ancestor) {
return Some((ancestor, uri_path.strip_prefix(ancestor).unwrap()));
}
}
None
}
#[tracing::instrument(skip(self))]
fn get_repository<'uri>(
&self,
uri: &'uri UriAbsoluteStr,
) -> Option<(&'uri Utf8Path, &'uri Utf8Path, &Repository)> {
self.find_repository_root(uri)
.map(|(repository_path, relative_path)| {
(
repository_path,
relative_path,
self.repositories.get(repository_path).unwrap(),
)
})
}
#[tracing::instrument(skip(self))]
fn get_repository_mut<'uri>(
&mut self,
uri: &'uri UriAbsoluteStr,
) -> Option<(&'uri Utf8Path, &'uri Utf8Path, &mut Repository)> {
self.find_repository_root(uri)
.map(|(repository_path, relative_path)| {
(
repository_path,
relative_path,
self.repositories.get_mut(repository_path).unwrap(),
)
})
}
}
#[tracing::instrument(skip_all)]
async fn event_loop(
decoration_change_event_emitter: vscode_sys::reference::EventEmitterRef,
decoration_type: vscode_sys::reference::TextEditorDecorationTypeRef,
quick_diff_provider: vscode_sys::reference::QuickDiffProviderRef,
js_functions: js_function::Functions,
mut sender: UnboundedSender<Event>,
mut receiver: UnboundedReceiver<Event>,
) {
tracing::info!("Starting event loop");
let mut extension_state = ExtensionState {
decoration_change_event_emitter: Rc::new(decoration_change_event_emitter),
decoration_type: Rc::new(decoration_type),
localization_context: Rc::new(l10n_embed::Context::new(
icu_locale::locale!("en-US"),
false,
)),
quick_diff_provider: Rc::new(quick_diff_provider),
repositories: HashMap::new(),
};
let mut event_buffer = Vec::with_capacity(EVENT_BATCH_LIMIT);
loop {
let events_received = receiver
.recv_many(&mut event_buffer, EVENT_BATCH_LIMIT)
.await;
if events_received == 0 {
tracing::info!("Shutting down event loop");
break;
};
for event in event_buffer.drain(..) {
match event {
Event::OpenWorkspaceFolder { workspace_uri } => {
event::open_workspace_folder::handle(
workspace_uri,
&mut extension_state,
&js_functions,
&mut sender,
)
.await
}
Event::OpenTextEditor { uri, text_editor } => {
event::open_text_editor::handle(
uri,
text_editor,
&mut extension_state,
&js_functions,
)
.await
}
Event::ChangeEditorContents { uri, changes } => {
event::change_editor_contents::handle(uri, changes, &mut extension_state).await
}
Event::MovePath { old_uri, new_uri } => {
event::move_path::handle(old_uri, new_uri, &mut extension_state).await
}
Event::ChangedFilesystemContents { uri } => {
event::changed_filesystem_contents::handle(
uri,
&mut extension_state,
&js_functions,
)
.await
}
Event::RequestInlineCredit { uri } => {
event::request_inline_credit::handle(uri, &extension_state, &js_functions).await
}
Event::RequestTrackedContents {
uri,
deferred_promise,
} => {
event::request_tracked_contents::handle(uri, deferred_promise, &extension_state)
.await
}
Event::RequestFileDecoration {
uri,
deferred_promise,
} => {
event::request_file_decoration::handle(uri, deferred_promise, &extension_state)
.await
}
Event::UpdateResourceStates { repository_path } => {
event::update_resource_states::handle(
repository_path,
&extension_state,
&js_functions,
)
.await
}
}
}
event_buffer.clear();
}
}
#[tracing::instrument(skip_all)]
pub fn start(
env: &napi::Env,
vscode_object: &napi::bindgen_prelude::Object,
decoration_change_event_emitter: vscode_sys::EventEmitter,
quick_diff_provider: vscode_sys::QuickDiffProvider,
) -> Result<(), napi::Error> {
let decoration_change_event_emitter_ref = decoration_change_event_emitter.create_ref()?;
let decoration_type = crate::inline_credit::create_decoration_type(env)?.create_ref()?;
let quick_diff_provider_ref = quick_diff_provider.create_ref()?;
let js_functions = js_function::Functions::get(env, vscode_object)?;
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
if let Err(_existing_value) = EVENT_SENDER.set(sender.clone()) {
return Err(napi::Error::from_reason(
"Event sender has already been initialized",
));
}
let runtime = tokio::runtime::Runtime::new().map_err(|error| {
napi::Error::from_reason(format!("Failed to create Tokio runtime: {error}"))
})?;
std::thread::spawn(move || {
runtime.block_on(event_loop(
decoration_change_event_emitter_ref,
decoration_type,
quick_diff_provider_ref,
js_functions,
sender,
receiver,
))
});
Ok(())
}
pub fn send(event: Event) {
EVENT_SENDER
.get()
.expect("EVENT_SENDER should be set")
.send(event)
.expect("Receiver should be open");
}