1use std::path::PathBuf;
14
15use strop_core::worker::{CancelReason, Outcome};
16
17use super::git_memory::{BlameGutter, BlameKey, CardKey, GitJob};
18use super::Editor;
19
20impl Editor {
21 pub(crate) fn toggle_blame_gutter(&mut self) {
25 let remote = self.remote_file().cloned();
26 let local_file = remote.is_none() && !self.buf().readonly && self.buf().path.is_some();
27 if !local_file {
28 if remote.is_some() {
29 if !self.remote_window_complete() {
30 self.message = "partial remote snapshot — blame needs a full window".into();
31 return;
32 }
33 } else {
34 return self.blame_line();
35 }
36 }
37 let Some(context) = self.git_context().cloned() else {
38 self.message = "not a git repo".into();
39 return;
40 };
41 if let Some(file) = &remote {
44 if context.repo.endpoint() != Some(file.endpoint()) {
45 self.message = "buffer's repository is not the current context".into();
46 return;
47 }
48 }
49 let key = self.current();
50 if let Some(mut gutter) = self.blame_gutters.remove(&key) {
51 if let Some(ticket) = gutter.request.take() {
54 self.cancel_git_worker(ticket.request, CancelReason::Dismissed);
55 }
56 return;
57 }
58 let doc = self.current();
59 let revision = self.buf().revision();
60 let Some(file) = self.doc(doc).file_target(&self.cwd) else {
61 return;
62 };
63 let Some(ticket) = self.git_ticket(BlameKey {
67 document: doc,
68 revision,
69 file,
70 repo: context.repo.clone(),
71 }) else {
72 return;
73 };
74 self.blame_gutters.insert(
75 key,
76 BlameGutter {
77 lines: Vec::new(),
78 revision,
79 request: Some(ticket.clone()),
80 },
81 );
82 self.spawn_blame_file(ticket);
83 self.blame_line(); }
85
86 fn spawn_blame_file(&mut self, ticket: strop_core::worker::Ticket<BlameKey>) {
87 let repo = ticket.key.repo.clone();
88 let rel = match (&repo, &ticket.key.file) {
89 (strop_git::RepoTarget::Local { .. }, crate::files::FileTarget::Local(path)) => {
90 repo.rel_of(path)
91 }
92 (
93 strop_git::RepoTarget::Remote { endpoint, .. },
94 crate::files::FileTarget::Remote(location),
95 ) if location.endpoint() == endpoint => location
96 .absolute_file()
97 .and_then(|file| repo.rel_of(file.path())),
98 _ => None,
99 };
100 let Some(rel) = rel else {
101 self.send_git_failure(
102 ticket,
103 strop_core::worker::FailureKind::InvalidInput,
104 "blame failed: buffer not under workdir",
105 GitJob::Gutter,
106 );
107 return;
108 };
109 strop_trace::record_with(strop_trace::EventKind::JobStarted, || {
110 serde_json::json!({
111 "service":"git","request":"blame_gutter",
112 "repository":if repo.is_remote() { "remote" } else { "local" },
113 "file": &ticket.key.file,
114 "document":{"slot":ticket.key.document.index(),"generation":ticket.key.document.generation()},
115 "revision":ticket.key.revision.get(),
116 })
117 });
118 let args = ticket.clone();
119 self.launch_git_job(
120 "git-blame-file",
121 "git.blame_gutter",
122 ticket,
123 &args,
124 GitJob::Gutter,
125 move |cancel| {
126 if cancel.is_cancelled() {
127 return Outcome::Cancelled(CancelReason::Superseded);
128 }
129 let exec = strop_git::GitExec::for_target(&repo);
130 match strop_git::memory::blame_file(&exec, &cancel, &rel) {
131 Ok(lines) => Outcome::Success(lines),
132 Err(message) => Outcome::failed(strop_core::worker::FailureKind::Exit, message),
133 }
134 },
135 );
136 }
137
138 pub fn blame_gutter_for(&self, buffer: strop_core::id::DocumentId) -> Option<&BlameGutter> {
143 let document = self.docs.get(buffer)?;
144 let buf = &document.buf;
145 if document
146 .remote_metadata()
147 .is_some_and(|source| !source.window.is_complete())
148 || self.remote_following(buffer)
149 {
150 return None;
151 }
152 let gutter = self.blame_gutters.get(&buffer)?;
153 let content_lines = buf.last_content_line() + 1;
156 (gutter.revision == buf.revision() && gutter.lines.len() == content_lines).then_some(gutter)
157 }
158
159 pub(crate) fn blame_line(&mut self) {
165 match self.remote_file().cloned() {
166 Some(file) => {
167 if !self.remote_window_complete() {
171 self.message = "partial remote snapshot — blame needs a full window".into();
172 return;
173 }
174 self.blame_line_remote(file);
175 }
176 None => match self.buf().path.clone() {
177 Some(path) => self.blame_line_local(path),
178 None => self.message = "blame works on file buffers".into(),
179 },
180 }
181 }
182 fn blame_line_local(&mut self, path: PathBuf) {
183 let Some(context) = self.git_context().cloned() else {
184 self.message = "not a git repo".into();
185 return;
186 };
187 if context.repo.is_remote() {
188 self.message = "buffer's repository is not the current context".into();
189 return;
190 }
191 if let Some(ticket) = self.card_request.take() {
192 self.cancel_git_worker(ticket.request, CancelReason::Superseded);
193 }
194 let doc = self.current();
195 let revision = self.buf().revision();
196 let line = self.buf().line_of(self.head()) + 1;
197 let Some(file) = self.doc(doc).file_target(&self.cwd) else {
198 return;
199 };
200 let Some(ticket) = self.git_ticket(CardKey {
201 origin: BlameKey {
202 document: doc,
203 revision,
204 file,
205 repo: context.repo.clone(),
206 },
207 line,
208 }) else {
209 return;
210 };
211 self.card_request = Some(ticket.clone());
212 strop_trace::record_with(strop_trace::EventKind::JobStarted, || {
213 serde_json::json!({
214 "service":"git","request":"blame_line",
215 "repository":"local",
216 "path":path.to_string_lossy(),"line":line,
217 "document":{"slot":doc.index(),"generation":doc.generation()},
218 "revision":revision.get(),
219 })
220 });
221 let abs = if path.is_absolute() {
222 path.clone()
223 } else {
224 context.workdir().join(&path)
225 };
226 let Some(rel) = context.repo.rel_of(&abs) else {
227 self.send_git_failure(
228 ticket,
229 strop_core::worker::FailureKind::InvalidInput,
230 "blame failed: not under workdir",
231 GitJob::Card,
232 );
233 return;
234 };
235 self.launch_blame_line(ticket, context.repo, rel, line);
236 }
237
238 fn blame_line_remote(&mut self, file: strop_workspace::RemoteFile) {
239 let Some(context) = self.git_context().cloned() else {
240 self.message = "not a git repo".into();
241 return;
242 };
243 if context.repo.endpoint() != Some(file.endpoint()) {
244 self.message = "buffer's repository is not the current context".into();
245 return;
246 }
247 if let Some(ticket) = self.card_request.take() {
248 self.cancel_git_worker(ticket.request, CancelReason::Superseded);
249 }
250 let doc = self.current();
251 let revision = self.buf().revision();
252 let line = self.buf().line_of(self.head()) + 1;
253 let Some(ticket) = self.git_ticket(CardKey {
254 origin: BlameKey {
255 document: doc,
256 revision,
257 file: crate::files::FileTarget::Remote(file.clone().into()),
258 repo: context.repo.clone(),
259 },
260 line,
261 }) else {
262 return;
263 };
264 self.card_request = Some(ticket.clone());
265 strop_trace::record_with(strop_trace::EventKind::JobStarted, || {
266 serde_json::json!({
267 "service":"git","request":"blame_line",
268 "repository":"remote",
269 "path":file.path().to_string_lossy(),"line":line,
270 "document":{"slot":doc.index(),"generation":doc.generation()},
271 "revision":revision.get(),
272 })
273 });
274 let Some(rel) = context.repo.rel_of(file.path()) else {
275 self.send_git_failure(
276 ticket,
277 strop_core::worker::FailureKind::InvalidInput,
278 "blame failed: buffer not under the remote workdir",
279 GitJob::Card,
280 );
281 return;
282 };
283 self.launch_blame_line(ticket, context.repo, rel, line);
284 }
285
286 fn launch_blame_line(
289 &mut self,
290 ticket: strop_core::worker::Ticket<CardKey>,
291 repo: strop_git::RepoTarget,
292 rel: PathBuf,
293 line: usize,
294 ) {
295 let args = ticket.clone();
296 self.launch_git_job(
297 "git-blame-line",
298 "git.blame_line",
299 ticket,
300 &args,
301 GitJob::Card,
302 move |cancel| {
303 if cancel.is_cancelled() {
304 return Outcome::Cancelled(CancelReason::Superseded);
305 }
306 let exec = strop_git::GitExec::for_target(&repo);
307 match strop_git::memory::blame_line(&exec, &cancel, &rel, line) {
308 Ok(card) => Outcome::Success(Box::new(card)),
309 Err(message) => Outcome::failed(strop_core::worker::FailureKind::Exit, message),
310 }
311 },
312 );
313 }
314
315 pub(crate) fn dismiss_card_authority(&mut self) -> Option<strop_git::memory::BlameCard> {
320 if let Some(ticket) = self.card_request.take() {
321 self.cancel_git_worker(ticket.request, CancelReason::Dismissed);
322 }
323 self.blame_card.take()
324 }
325}