use std::path::PathBuf;
use strop_core::worker::{CancelReason, Outcome};
use super::git_memory::{BlameGutter, BlameKey, CardKey, GitJob};
use super::Editor;
impl Editor {
pub(crate) fn toggle_blame_gutter(&mut self) {
let remote = self.remote_file().cloned();
let local_file = remote.is_none() && !self.buf().readonly && self.buf().path.is_some();
if !local_file {
if remote.is_some() {
if !self.remote_window_complete() {
self.message = "partial remote snapshot — blame needs a full window".into();
return;
}
} else {
return self.blame_line();
}
}
let Some(context) = self.git_context().cloned() else {
self.message = "not a git repo".into();
return;
};
if let Some(file) = &remote {
if context.repo.endpoint() != Some(file.endpoint()) {
self.message = "buffer's repository is not the current context".into();
return;
}
}
let key = self.current();
if let Some(mut gutter) = self.blame_gutters.remove(&key) {
if let Some(ticket) = gutter.request.take() {
self.cancel_git_worker(ticket.request, CancelReason::Dismissed);
}
return;
}
let doc = self.current();
let revision = self.buf().revision();
let Some(file) = self.doc(doc).file_target(&self.cwd) else {
return;
};
let Some(ticket) = self.git_ticket(BlameKey {
document: doc,
revision,
file,
repo: context.repo.clone(),
}) else {
return;
};
self.blame_gutters.insert(
key,
BlameGutter {
lines: Vec::new(),
revision,
request: Some(ticket.clone()),
},
);
self.spawn_blame_file(ticket);
self.blame_line(); }
fn spawn_blame_file(&mut self, ticket: strop_core::worker::Ticket<BlameKey>) {
let repo = ticket.key.repo.clone();
let rel = match (&repo, &ticket.key.file) {
(strop_git::RepoTarget::Local { .. }, crate::files::FileTarget::Local(path)) => {
repo.rel_of(path)
}
(
strop_git::RepoTarget::Remote { endpoint, .. },
crate::files::FileTarget::Remote(location),
) if location.endpoint() == endpoint => location
.absolute_file()
.and_then(|file| repo.rel_of(file.path())),
_ => None,
};
let Some(rel) = rel else {
self.send_git_failure(
ticket,
strop_core::worker::FailureKind::InvalidInput,
"blame failed: buffer not under workdir",
GitJob::Gutter,
);
return;
};
strop_trace::record_with(strop_trace::EventKind::JobStarted, || {
serde_json::json!({
"service":"git","request":"blame_gutter",
"repository":if repo.is_remote() { "remote" } else { "local" },
"file": &ticket.key.file,
"document":{"slot":ticket.key.document.index(),"generation":ticket.key.document.generation()},
"revision":ticket.key.revision.get(),
})
});
let args = ticket.clone();
self.launch_git_job(
"git-blame-file",
"git.blame_gutter",
ticket,
&args,
GitJob::Gutter,
move |cancel| {
if cancel.is_cancelled() {
return Outcome::Cancelled(CancelReason::Superseded);
}
let exec = strop_git::GitExec::for_target(&repo);
match strop_git::memory::blame_file(&exec, &cancel, &rel) {
Ok(lines) => Outcome::Success(lines),
Err(message) => Outcome::failed(strop_core::worker::FailureKind::Exit, message),
}
},
);
}
pub fn blame_gutter_for(&self, buffer: strop_core::id::DocumentId) -> Option<&BlameGutter> {
let document = self.docs.get(buffer)?;
let buf = &document.buf;
if document
.remote_metadata()
.is_some_and(|source| !source.window.is_complete())
|| self.remote_following(buffer)
{
return None;
}
let gutter = self.blame_gutters.get(&buffer)?;
let content_lines = buf.last_content_line() + 1;
(gutter.revision == buf.revision() && gutter.lines.len() == content_lines).then_some(gutter)
}
pub(crate) fn blame_line(&mut self) {
match self.remote_file().cloned() {
Some(file) => {
if !self.remote_window_complete() {
self.message = "partial remote snapshot — blame needs a full window".into();
return;
}
self.blame_line_remote(file);
}
None => match self.buf().path.clone() {
Some(path) => self.blame_line_local(path),
None => self.message = "blame works on file buffers".into(),
},
}
}
fn blame_line_local(&mut self, path: PathBuf) {
let Some(context) = self.git_context().cloned() else {
self.message = "not a git repo".into();
return;
};
if context.repo.is_remote() {
self.message = "buffer's repository is not the current context".into();
return;
}
if let Some(ticket) = self.card_request.take() {
self.cancel_git_worker(ticket.request, CancelReason::Superseded);
}
let doc = self.current();
let revision = self.buf().revision();
let line = self.buf().line_of(self.head()) + 1;
let Some(file) = self.doc(doc).file_target(&self.cwd) else {
return;
};
let Some(ticket) = self.git_ticket(CardKey {
origin: BlameKey {
document: doc,
revision,
file,
repo: context.repo.clone(),
},
line,
}) else {
return;
};
self.card_request = Some(ticket.clone());
strop_trace::record_with(strop_trace::EventKind::JobStarted, || {
serde_json::json!({
"service":"git","request":"blame_line",
"repository":"local",
"path":path.to_string_lossy(),"line":line,
"document":{"slot":doc.index(),"generation":doc.generation()},
"revision":revision.get(),
})
});
let abs = if path.is_absolute() {
path.clone()
} else {
context.workdir().join(&path)
};
let Some(rel) = context.repo.rel_of(&abs) else {
self.send_git_failure(
ticket,
strop_core::worker::FailureKind::InvalidInput,
"blame failed: not under workdir",
GitJob::Card,
);
return;
};
self.launch_blame_line(ticket, context.repo, rel, line);
}
fn blame_line_remote(&mut self, file: strop_workspace::RemoteFile) {
let Some(context) = self.git_context().cloned() else {
self.message = "not a git repo".into();
return;
};
if context.repo.endpoint() != Some(file.endpoint()) {
self.message = "buffer's repository is not the current context".into();
return;
}
if let Some(ticket) = self.card_request.take() {
self.cancel_git_worker(ticket.request, CancelReason::Superseded);
}
let doc = self.current();
let revision = self.buf().revision();
let line = self.buf().line_of(self.head()) + 1;
let Some(ticket) = self.git_ticket(CardKey {
origin: BlameKey {
document: doc,
revision,
file: crate::files::FileTarget::Remote(file.clone().into()),
repo: context.repo.clone(),
},
line,
}) else {
return;
};
self.card_request = Some(ticket.clone());
strop_trace::record_with(strop_trace::EventKind::JobStarted, || {
serde_json::json!({
"service":"git","request":"blame_line",
"repository":"remote",
"path":file.path().to_string_lossy(),"line":line,
"document":{"slot":doc.index(),"generation":doc.generation()},
"revision":revision.get(),
})
});
let Some(rel) = context.repo.rel_of(file.path()) else {
self.send_git_failure(
ticket,
strop_core::worker::FailureKind::InvalidInput,
"blame failed: buffer not under the remote workdir",
GitJob::Card,
);
return;
};
self.launch_blame_line(ticket, context.repo, rel, line);
}
fn launch_blame_line(
&mut self,
ticket: strop_core::worker::Ticket<CardKey>,
repo: strop_git::RepoTarget,
rel: PathBuf,
line: usize,
) {
let args = ticket.clone();
self.launch_git_job(
"git-blame-line",
"git.blame_line",
ticket,
&args,
GitJob::Card,
move |cancel| {
if cancel.is_cancelled() {
return Outcome::Cancelled(CancelReason::Superseded);
}
let exec = strop_git::GitExec::for_target(&repo);
match strop_git::memory::blame_line(&exec, &cancel, &rel, line) {
Ok(card) => Outcome::Success(Box::new(card)),
Err(message) => Outcome::failed(strop_core::worker::FailureKind::Exit, message),
}
},
);
}
pub(crate) fn dismiss_card_authority(&mut self) -> Option<strop_git::memory::BlameCard> {
if let Some(ticket) = self.card_request.take() {
self.cancel_git_worker(ticket.request, CancelReason::Dismissed);
}
self.blame_card.take()
}
}