Detailed changes
@@ -3295,6 +3295,8 @@ dependencies = [
"sum_tree",
"text",
"theme",
+ "time",
+ "time_format",
"tree-sitter-html",
"tree-sitter-rust",
"tree-sitter-typescript",
@@ -3942,6 +3944,7 @@ dependencies = [
"collections",
"fsevent",
"futures 0.3.28",
+ "git",
"git2",
"gpui",
"lazy_static",
@@ -4249,14 +4252,21 @@ dependencies = [
name = "git"
version = "0.1.0"
dependencies = [
+ "anyhow",
"clock",
+ "collections",
"git2",
"lazy_static",
"log",
+ "pretty_assertions",
+ "serde",
+ "serde_json",
"smol",
"sum_tree",
"text",
+ "time",
"unindent",
+ "url",
]
[[package]]
@@ -7178,6 +7188,7 @@ dependencies = [
"fs",
"futures 0.3.28",
"fuzzy",
+ "git",
"git2",
"globset",
"gpui",
@@ -280,6 +280,8 @@ tempfile = "3.9.0"
thiserror = "1.0.29"
tiktoken-rs = "0.5.7"
time = { version = "0.3", features = [
+ "macros",
+ "parsing",
"serde",
"serde-well-known",
"formatting",
@@ -366,6 +366,7 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::ExpandProjectEntry>)
.add_request_handler(forward_mutating_project_request::<proto::OnTypeFormatting>)
.add_request_handler(forward_mutating_project_request::<proto::SaveBuffer>)
+ .add_request_handler(forward_mutating_project_request::<proto::BlameBuffer>)
.add_message_handler(create_buffer_for_peer)
.add_request_handler(update_buffer)
.add_message_handler(broadcast_project_message_from_host::<proto::RefreshInlayHints>)
@@ -23,6 +23,7 @@ use rpc::RECEIVE_TIMEOUT;
use serde_json::json;
use settings::SettingsStore;
use std::{
+ ops::Range,
path::Path,
sync::{
atomic::{self, AtomicBool, AtomicUsize},
@@ -1986,6 +1987,187 @@ struct Row10;"#};
struct Row1220;"#});
}
+#[gpui::test(iterations = 10)]
+async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+ let mut server = TestServer::start(cx_a.executor()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ client_a
+ .fs()
+ .insert_tree(
+ "/my-repo",
+ json!({
+ ".git": {},
+ "file.txt": "line1\nline2\nline3\nline\n",
+ }),
+ )
+ .await;
+
+ let blame = git::blame::Blame {
+ entries: vec![
+ blame_entry("1b1b1b", 0..1),
+ blame_entry("0d0d0d", 1..2),
+ blame_entry("3a3a3a", 2..3),
+ blame_entry("4c4c4c", 3..4),
+ ],
+ permalinks: [
+ ("1b1b1b", "http://example.com/codehost/idx-0"),
+ ("0d0d0d", "http://example.com/codehost/idx-1"),
+ ("3a3a3a", "http://example.com/codehost/idx-2"),
+ ("4c4c4c", "http://example.com/codehost/idx-3"),
+ ]
+ .into_iter()
+ .map(|(sha, url)| (sha.parse().unwrap(), url.parse().unwrap()))
+ .collect(),
+ messages: [
+ ("1b1b1b", "message for idx-0"),
+ ("0d0d0d", "message for idx-1"),
+ ("3a3a3a", "message for idx-2"),
+ ("4c4c4c", "message for idx-3"),
+ ]
+ .into_iter()
+ .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
+ .collect(),
+ };
+ client_a.fs().set_blame_for_repo(
+ Path::new("/my-repo/.git"),
+ vec![(Path::new("file.txt"), blame)],
+ );
+
+ let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ // Create editor_a
+ let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
+ let editor_a = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "file.txt"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ // Join the project as client B.
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+ let editor_b = workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "file.txt"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ // client_b now requests git blame for the open buffer
+ editor_b.update(cx_b, |editor_b, cx| {
+ assert!(editor_b.blame().is_none());
+ editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, cx);
+ });
+
+ cx_a.executor().run_until_parked();
+ cx_b.executor().run_until_parked();
+
+ editor_b.update(cx_b, |editor_b, cx| {
+ let blame = editor_b.blame().expect("editor_b should have blame now");
+ let entries = blame.update(cx, |blame, cx| {
+ blame
+ .blame_for_rows((0..4).map(Some), cx)
+ .collect::<Vec<_>>()
+ });
+
+ assert_eq!(
+ entries,
+ vec![
+ Some(blame_entry("1b1b1b", 0..1)),
+ Some(blame_entry("0d0d0d", 1..2)),
+ Some(blame_entry("3a3a3a", 2..3)),
+ Some(blame_entry("4c4c4c", 3..4)),
+ ]
+ );
+
+ blame.update(cx, |blame, _| {
+ for (idx, entry) in entries.iter().flatten().enumerate() {
+ assert_eq!(
+ blame.permalink_for_entry(entry).unwrap().to_string(),
+ format!("http://example.com/codehost/idx-{}", idx)
+ );
+ assert_eq!(
+ blame.message_for_entry(entry).unwrap(),
+ format!("message for idx-{}", idx)
+ );
+ }
+ });
+ });
+
+ // editor_b updates the file, which gets sent to client_a, which updates git blame,
+ // which gets back to client_b.
+ editor_b.update(cx_b, |editor_b, cx| {
+ editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
+ });
+
+ cx_a.executor().run_until_parked();
+ cx_b.executor().run_until_parked();
+
+ editor_b.update(cx_b, |editor_b, cx| {
+ let blame = editor_b.blame().expect("editor_b should have blame now");
+ let entries = blame.update(cx, |blame, cx| {
+ blame
+ .blame_for_rows((0..4).map(Some), cx)
+ .collect::<Vec<_>>()
+ });
+
+ assert_eq!(
+ entries,
+ vec![
+ None,
+ Some(blame_entry("0d0d0d", 1..2)),
+ Some(blame_entry("3a3a3a", 2..3)),
+ Some(blame_entry("4c4c4c", 3..4)),
+ ]
+ );
+ });
+
+ // Now editor_a also updates the file
+ editor_a.update(cx_a, |editor_a, cx| {
+ editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
+ });
+
+ cx_a.executor().run_until_parked();
+ cx_b.executor().run_until_parked();
+
+ editor_b.update(cx_b, |editor_b, cx| {
+ let blame = editor_b.blame().expect("editor_b should have blame now");
+ let entries = blame.update(cx, |blame, cx| {
+ blame
+ .blame_for_rows((0..4).map(Some), cx)
+ .collect::<Vec<_>>()
+ });
+
+ assert_eq!(
+ entries,
+ vec![
+ None,
+ None,
+ Some(blame_entry("3a3a3a", 2..3)),
+ Some(blame_entry("4c4c4c", 3..4)),
+ ]
+ );
+ });
+}
+
fn extract_hint_labels(editor: &Editor) -> Vec<String> {
let mut labels = Vec::new();
for hint in editor.inlay_hint_cache().hints() {
@@ -1996,3 +2178,11 @@ fn extract_hint_labels(editor: &Editor) -> Vec<String> {
}
labels
}
+
+fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
+ git::blame::BlameEntry {
+ sha: sha.parse().unwrap(),
+ range,
+ ..Default::default()
+ }
+}
@@ -61,6 +61,8 @@ smol.workspace = true
snippet.workspace = true
sum_tree.workspace = true
text.workspace = true
+time.workspace = true
+time_format.workspace = true
theme.workspace = true
tree-sitter-html = { workspace = true, optional = true }
tree-sitter-rust = { workspace = true, optional = true }
@@ -244,6 +244,7 @@ gpui::actions!(
SplitSelectionIntoLines,
Tab,
TabPrev,
+ ToggleGitBlame,
ToggleInlayHints,
ToggleLineNumbers,
ToggleSoftWrap,
@@ -38,6 +38,7 @@ mod editor_tests;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
use ::git::diff::{DiffHunk, DiffHunkStatus};
+use ::git::permalink::{build_permalink, BuildPermalinkParams};
pub(crate) use actions::*;
use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context as _, Result};
@@ -56,6 +57,7 @@ pub use element::{
};
use futures::FutureExt;
use fuzzy::{StringMatch, StringMatchCandidate};
+use git::blame::GitBlame;
use git::diff_hunk_to_display;
use gpui::{
div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action,
@@ -92,8 +94,7 @@ pub use multi_buffer::{
use ordered_float::OrderedFloat;
use parking_lot::{Mutex, RwLock};
use project::project_settings::{GitGutterSetting, ProjectSettings};
-use project::Item;
-use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
+use project::{FormatTrigger, Item, Location, Project, ProjectPath, ProjectTransaction};
use rand::prelude::*;
use rpc::proto::*;
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
@@ -432,6 +433,9 @@ pub struct Editor {
editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
use_autoclose: bool,
auto_replace_emoji_shortcode: bool,
+ show_git_blame: bool,
+ blame: Option<Model<GitBlame>>,
+ blame_subscription: Option<Subscription>,
custom_context_menu: Option<
Box<
dyn 'static
@@ -443,6 +447,7 @@ pub struct Editor {
pub struct EditorSnapshot {
pub mode: EditorMode,
show_gutter: bool,
+ show_git_blame: bool,
pub display_snapshot: DisplaySnapshot,
pub placeholder_text: Option<Arc<str>>,
is_focused: bool,
@@ -450,11 +455,14 @@ pub struct EditorSnapshot {
ongoing_scroll: OngoingScroll,
}
+const GIT_BLAME_GUTTER_WIDTH_CHARS: f32 = 53.;
+
pub struct GutterDimensions {
pub left_padding: Pixels,
pub right_padding: Pixels,
pub width: Pixels,
pub margin: Pixels,
+ pub git_blame_entries_width: Option<Pixels>,
}
impl Default for GutterDimensions {
@@ -464,6 +472,7 @@ impl Default for GutterDimensions {
right_padding: Pixels::ZERO,
width: Pixels::ZERO,
margin: Pixels::ZERO,
+ git_blame_entries_width: None,
}
}
}
@@ -1471,6 +1480,9 @@ impl Editor {
vim_replace_map: Default::default(),
show_inline_completions: mode == EditorMode::Full,
custom_context_menu: None,
+ show_git_blame: false,
+ blame: None,
+ blame_subscription: None,
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe(&buffer, Self::on_buffer_event),
@@ -1616,6 +1628,10 @@ impl Editor {
EditorSnapshot {
mode: self.mode,
show_gutter: self.show_gutter,
+ show_git_blame: self
+ .blame
+ .as_ref()
+ .map_or(false, |blame| blame.read(cx).has_generated_entries()),
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
scroll_anchor: self.scroll_manager.anchor(),
ongoing_scroll: self.scroll_manager.ongoing_scroll(),
@@ -8832,9 +8848,42 @@ impl Editor {
}
}
- fn get_permalink_to_line(&mut self, cx: &mut ViewContext<Self>) -> Result<url::Url> {
- use git::permalink::{build_permalink, BuildPermalinkParams};
+ pub fn toggle_git_blame(&mut self, _: &ToggleGitBlame, cx: &mut ViewContext<Self>) {
+ if !self.show_git_blame {
+ if let Err(error) = self.show_git_blame_internal(cx) {
+ log::error!("failed to toggle on 'git blame': {}", error);
+ return;
+ }
+ self.show_git_blame = true
+ } else {
+ self.blame_subscription.take();
+ self.blame.take();
+ self.show_git_blame = false
+ }
+
+ cx.notify();
+ }
+
+ fn show_git_blame_internal(&mut self, cx: &mut ViewContext<Self>) -> Result<()> {
+ if let Some(project) = self.project.as_ref() {
+ let Some(buffer) = self.buffer().read(cx).as_singleton() else {
+ anyhow::bail!("git blame not available in multi buffers")
+ };
+ let project = project.clone();
+ let blame = cx.new_model(|cx| GitBlame::new(buffer, project, cx));
+ self.blame_subscription = Some(cx.observe(&blame, |_, _, cx| cx.notify()));
+ self.blame = Some(blame);
+ }
+
+ Ok(())
+ }
+
+ pub fn blame(&self) -> Option<&Model<GitBlame>> {
+ self.blame.as_ref()
+ }
+
+ fn get_permalink_to_line(&mut self, cx: &mut ViewContext<Self>) -> Result<url::Url> {
let (path, repo) = maybe!({
let project_handle = self.project.as_ref()?.clone();
let project = project_handle.read(cx);
@@ -8867,7 +8916,12 @@ impl Editor {
remote_url: &origin_url,
sha: &sha,
path: &path,
- selection: selection.map(|selection| selection.range()),
+ selection: selection.map(|selection| {
+ let range = selection.range();
+ let start = range.start.row;
+ let end = range.end.row;
+ start..end
+ }),
})
}
@@ -9978,7 +10032,12 @@ impl EditorSnapshot {
0.0.into()
};
- let left_padding = if gutter_settings.code_actions {
+ let git_blame_entries_width = self
+ .show_git_blame
+ .then_some(em_width * GIT_BLAME_GUTTER_WIDTH_CHARS);
+
+ let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO);
+ left_padding += if gutter_settings.code_actions {
em_width * 3.0
} else if show_git_gutter && gutter_settings.line_numbers {
em_width * 2.0
@@ -10003,6 +10062,7 @@ impl EditorSnapshot {
right_padding,
width: line_gutter_width + left_padding + right_padding,
margin: -descent,
+ git_blame_entries_width,
}
}
}
@@ -4,12 +4,12 @@ use crate::{
TransformBlock,
},
editor_settings::{DoubleClickInMultibuffer, MultiCursorModifier, ShowScrollbar},
- git::{diff_hunk_to_display, DisplayDiffHunk},
+ git::{blame::GitBlame, diff_hunk_to_display, DisplayDiffHunk},
hover_popover::{
self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
},
items::BufferSearchHighlights,
- mouse_context_menu,
+ mouse_context_menu::{self, MouseContextMenu},
scroll::scroll_amount::ScrollAmount,
CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode,
EditorSettings, EditorSnapshot, EditorStyle, GutterDimensions, HalfPageDown, HalfPageUp,
@@ -18,15 +18,15 @@ use crate::{
};
use anyhow::Result;
use collections::{BTreeMap, HashMap};
-use git::diff::DiffHunkStatus;
+use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
use gpui::{
div, fill, outline, overlay, point, px, quad, relative, size, svg, transparent_black, Action,
- AnchorCorner, AnyElement, AvailableSpace, Bounds, ContentMask, Corners, CursorStyle,
- DispatchPhase, Edges, Element, ElementContext, ElementInputHandler, Entity, Hitbox, Hsla,
- InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, MouseDownEvent,
- MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine,
- SharedString, Size, Stateful, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle,
- TextStyleRefinement, View, ViewContext, WindowContext,
+ AnchorCorner, AnyElement, AnyView, AvailableSpace, Bounds, ClipboardItem, ContentMask, Corners,
+ CursorStyle, DispatchPhase, Edges, Element, ElementContext, ElementInputHandler, Entity,
+ Hitbox, Hsla, InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton,
+ MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollDelta,
+ ScrollWheelEvent, ShapedLine, SharedString, Size, Stateful, StatefulInteractiveElement, Style,
+ Styled, TextRun, TextStyle, TextStyleRefinement, View, ViewContext, WindowContext,
};
use itertools::Itertools;
use language::language_settings::ShowWhitespaceSetting;
@@ -49,8 +49,8 @@ use std::{
};
use sum_tree::Bias;
use theme::{ActiveTheme, PlayerColor};
-use ui::prelude::*;
-use ui::{h_flex, ButtonLike, ButtonStyle, Tooltip};
+use ui::{h_flex, ButtonLike, ButtonStyle, ContextMenu, Tooltip};
+use ui::{prelude::*, tooltip_container};
use util::ResultExt;
use workspace::item::Item;
@@ -301,6 +301,7 @@ impl EditorElement {
register_action(view, cx, Editor::copy_highlight_json);
register_action(view, cx, Editor::copy_permalink_to_line);
register_action(view, cx, Editor::open_permalink_to_line);
+ register_action(view, cx, Editor::toggle_git_blame);
register_action(view, cx, |editor, action, cx| {
if let Some(task) = editor.format(action, cx) {
task.detach_and_log_err(cx);
@@ -1082,6 +1083,64 @@ impl EditorElement {
.collect()
}
+ #[allow(clippy::too_many_arguments)]
+ fn layout_blame_entries(
+ &self,
+ buffer_rows: impl Iterator<Item = Option<u32>>,
+ em_width: Pixels,
+ scroll_position: gpui::Point<f32>,
+ line_height: Pixels,
+ gutter_hitbox: &Hitbox,
+ max_width: Option<Pixels>,
+ cx: &mut ElementContext,
+ ) -> Option<Vec<AnyElement>> {
+ let Some(blame) = self.editor.read(cx).blame.as_ref().cloned() else {
+ return None;
+ };
+
+ let blamed_rows: Vec<_> = blame.update(cx, |blame, cx| {
+ blame.blame_for_rows(buffer_rows, cx).collect()
+ });
+
+ let width = if let Some(max_width) = max_width {
+ AvailableSpace::Definite(max_width)
+ } else {
+ AvailableSpace::MaxContent
+ };
+ let scroll_top = scroll_position.y * line_height;
+ let start_x = em_width * 1;
+
+ let mut last_used_color: Option<(PlayerColor, Oid)> = None;
+
+ let shaped_lines = blamed_rows
+ .into_iter()
+ .enumerate()
+ .flat_map(|(ix, blame_entry)| {
+ if let Some(blame_entry) = blame_entry {
+ let mut element = render_blame_entry(
+ ix,
+ &blame,
+ blame_entry,
+ &mut last_used_color,
+ self.editor.clone(),
+ cx,
+ );
+
+ let start_y = ix as f32 * line_height - (scroll_top % line_height);
+ let absolute_offset = gutter_hitbox.origin + point(start_x, start_y);
+
+ element.layout(absolute_offset, size(width, AvailableSpace::MinContent), cx);
+
+ Some(element)
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ Some(shaped_lines)
+ }
+
fn layout_code_actions_indicator(
&self,
line_height: Pixels,
@@ -1108,19 +1167,26 @@ impl EditorElement {
);
let indicator_size = button.measure(available_space, cx);
- let mut x = Pixels::ZERO;
+ let blame_width = gutter_dimensions
+ .git_blame_entries_width
+ .unwrap_or(Pixels::ZERO);
+
+ let mut x = blame_width;
+ let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
+ - indicator_size.width
+ - blame_width;
+ x += available_width / 2.;
+
let mut y = newest_selection_head.row() as f32 * line_height - scroll_pixel_position.y;
- // Center indicator.
- x +=
- (gutter_dimensions.margin + gutter_dimensions.left_padding - indicator_size.width) / 2.;
y += (line_height - indicator_size.height) / 2.;
+
button.layout(gutter_hitbox.origin + point(x, y), available_space, cx);
Some(button)
}
fn calculate_relative_line_numbers(
&self,
- snapshot: &EditorSnapshot,
+ buffer_rows: Vec<Option<u32>>,
rows: &Range<u32>,
relative_to: Option<u32>,
) -> HashMap<u32, u32> {
@@ -1130,12 +1196,6 @@ impl EditorElement {
};
let start = rows.start.min(relative_to);
- let end = rows.end.max(relative_to);
-
- let buffer_rows = snapshot
- .buffer_rows(start)
- .take(1 + (end - start) as usize)
- .collect::<Vec<_>>();
let head_idx = relative_to - start;
let mut delta = 1;
@@ -1171,6 +1231,7 @@ impl EditorElement {
fn layout_line_numbers(
&self,
rows: Range<u32>,
+ buffer_rows: impl Iterator<Item = Option<u32>>,
active_rows: &BTreeMap<u32, bool>,
newest_selection_head: Option<DisplayPoint>,
snapshot: &EditorSnapshot,
@@ -1209,13 +1270,11 @@ impl EditorElement {
None
};
- let relative_rows = self.calculate_relative_line_numbers(&snapshot, &rows, relative_to);
+ let buffer_rows = buffer_rows.collect::<Vec<_>>();
+ let relative_rows =
+ self.calculate_relative_line_numbers(buffer_rows.clone(), &rows, relative_to);
- for (ix, row) in snapshot
- .buffer_rows(rows.start)
- .take((rows.end - rows.start) as usize)
- .enumerate()
- {
+ for (ix, row) in buffer_rows.into_iter().enumerate() {
let display_row = rows.start + ix as u32;
let (active, color) = if active_rows.contains_key(&display_row) {
(true, cx.theme().colors().editor_active_line_number)
@@ -1986,6 +2045,10 @@ impl EditorElement {
Self::paint_diff_hunks(layout, cx);
}
+ if layout.blamed_display_rows.is_some() {
+ self.paint_blamed_display_rows(layout, cx);
+ }
+
for (ix, line) in layout.line_numbers.iter().enumerate() {
if let Some(line) = line {
let line_origin = layout.gutter_hitbox.origin
@@ -2119,6 +2182,18 @@ impl EditorElement {
})
}
+ fn paint_blamed_display_rows(&self, layout: &mut EditorLayout, cx: &mut ElementContext) {
+ let Some(blamed_display_rows) = layout.blamed_display_rows.take() else {
+ return;
+ };
+
+ cx.paint_layer(layout.gutter_hitbox.bounds, |cx| {
+ for mut blame_element in blamed_display_rows.into_iter() {
+ blame_element.paint(cx);
+ }
+ })
+ }
+
fn paint_text(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) {
cx.with_content_mask(
Some(ContentMask {
@@ -2766,6 +2841,188 @@ impl EditorElement {
}
}
+fn render_blame_entry(
+ ix: usize,
+ blame: &gpui::Model<GitBlame>,
+ blame_entry: BlameEntry,
+ last_used_color: &mut Option<(PlayerColor, Oid)>,
+ editor: View<Editor>,
+ cx: &mut ElementContext<'_>,
+) -> AnyElement {
+ let mut sha_color = cx
+ .theme()
+ .players()
+ .color_for_participant(blame_entry.sha.into());
+ // If the last color we used is the same as the one we get for this line, but
+ // the commit SHAs are different, then we try again to get a different color.
+ match *last_used_color {
+ Some((color, sha)) if sha != blame_entry.sha && color.cursor == sha_color.cursor => {
+ let index: u32 = blame_entry.sha.into();
+ sha_color = cx.theme().players().color_for_participant(index + 1);
+ }
+ _ => {}
+ };
+ last_used_color.replace((sha_color, blame_entry.sha));
+
+ let relative_timestamp = match blame_entry.author_offset_date_time() {
+ Ok(timestamp) => time_format::format_localized_timestamp(
+ timestamp,
+ time::OffsetDateTime::now_utc(),
+ cx.local_timezone(),
+ time_format::TimestampFormat::Relative,
+ ),
+ Err(_) => "Error parsing date".to_string(),
+ };
+
+ let pretty_commit_id = format!("{}", blame_entry.sha);
+ let short_commit_id = pretty_commit_id.clone().chars().take(6).collect::<String>();
+
+ let name = blame_entry.author.as_deref().unwrap_or("<no name>");
+ let name = if name.len() > 20 {
+ format!("{}...", &name[..16])
+ } else {
+ name.to_string()
+ };
+
+ let permalink = blame.read(cx).permalink_for_entry(&blame_entry);
+ let commit_message = blame.read(cx).message_for_entry(&blame_entry);
+
+ h_flex()
+ .id(("blame", ix))
+ .children([
+ div()
+ .text_color(sha_color.cursor)
+ .child(short_commit_id)
+ .mr_2(),
+ div()
+ .text_color(cx.theme().status().hint)
+ .child(format!("{:20} {: >14}", name, relative_timestamp)),
+ ])
+ .on_mouse_down(MouseButton::Right, {
+ let blame_entry = blame_entry.clone();
+ move |event, cx| {
+ deploy_blame_entry_context_menu(&blame_entry, editor.clone(), event.position, cx);
+ }
+ })
+ .hover(|style| style.bg(cx.theme().colors().element_hover))
+ .when_some(permalink, |this, url| {
+ let url = url.clone();
+ this.cursor_pointer().on_click(move |_, cx| {
+ cx.stop_propagation();
+ cx.open_url(url.as_str())
+ })
+ })
+ .tooltip(move |cx| {
+ BlameEntryTooltip::new(
+ sha_color.cursor,
+ commit_message.clone(),
+ blame_entry.clone(),
+ cx,
+ )
+ })
+ .into_any()
+}
+
+fn deploy_blame_entry_context_menu(
+ blame_entry: &BlameEntry,
+ editor: View<Editor>,
+ position: gpui::Point<Pixels>,
+ cx: &mut WindowContext<'_>,
+) {
+ let context_menu = ContextMenu::build(cx, move |this, _| {
+ let sha = format!("{}", blame_entry.sha);
+ this.entry("Copy commit SHA", None, move |cx| {
+ cx.write_to_clipboard(ClipboardItem::new(sha.clone()));
+ })
+ });
+
+ editor.update(cx, move |editor, cx| {
+ editor.mouse_context_menu = Some(MouseContextMenu::new(position, context_menu, cx));
+ cx.notify();
+ });
+}
+
+struct BlameEntryTooltip {
+ color: Hsla,
+ commit_message: Option<String>,
+ blame_entry: BlameEntry,
+}
+
+impl BlameEntryTooltip {
+ fn new(
+ color: Hsla,
+ commit_message: Option<String>,
+ blame_entry: BlameEntry,
+ cx: &mut WindowContext,
+ ) -> AnyView {
+ cx.new_view(|_cx| Self {
+ color,
+ commit_message,
+ blame_entry,
+ })
+ .into()
+ }
+}
+
+impl Render for BlameEntryTooltip {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ let author = self
+ .blame_entry
+ .author
+ .clone()
+ .unwrap_or("<no name>".to_string());
+ let author_email = self.blame_entry.author_mail.clone().unwrap_or_default();
+ let absolute_timestamp = match self.blame_entry.author_offset_date_time() {
+ Ok(timestamp) => time_format::format_localized_timestamp(
+ timestamp,
+ time::OffsetDateTime::now_utc(),
+ cx.local_timezone(),
+ time_format::TimestampFormat::Absolute,
+ ),
+ Err(_) => "Error parsing date".to_string(),
+ };
+
+ let message = match &self.commit_message {
+ Some(message) => message.clone(),
+ None => {
+ println!("can't find commit message");
+ self.blame_entry.summary.clone().unwrap_or_default()
+ }
+ };
+
+ let pretty_commit_id = format!("{}", self.blame_entry.sha);
+
+ tooltip_container(cx, move |this, cx| {
+ this.occlude()
+ .on_mouse_move(|_, cx| cx.stop_propagation())
+ .child(
+ v_flex()
+ .child(
+ h_flex()
+ .child(
+ div()
+ .text_color(cx.theme().colors().text_muted)
+ .child("Commit")
+ .pr_2(),
+ )
+ .child(
+ div().text_color(self.color).child(pretty_commit_id.clone()),
+ ),
+ )
+ .child(
+ div()
+ .child(format!(
+ "{} {} - {}",
+ author, author_email, absolute_timestamp
+ ))
+ .text_color(cx.theme().colors().text_muted),
+ )
+ .child(div().child(message)),
+ )
+ })
+ }
+}
+
#[derive(Debug)]
pub(crate) struct LineWithInvisibles {
pub line: ShapedLine,
@@ -3124,6 +3381,10 @@ impl Element for EditorElement {
let end_row =
1 + cmp::min((scroll_position.y + height_in_lines).ceil() as u32, max_row);
+ let buffer_rows = snapshot
+ .buffer_rows(start_row)
+ .take((start_row..end_row).len());
+
let start_anchor = if start_row == 0 {
Anchor::min()
} else {
@@ -3165,6 +3426,7 @@ impl Element for EditorElement {
let (line_numbers, fold_statuses) = self.layout_line_numbers(
start_row..end_row,
+ buffer_rows.clone(),
&active_rows,
newest_selection_head,
&snapshot,
@@ -3173,6 +3435,16 @@ impl Element for EditorElement {
let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot);
+ let blamed_display_rows = self.layout_blame_entries(
+ buffer_rows,
+ em_width,
+ scroll_position,
+ line_height,
+ &gutter_hitbox,
+ gutter_dimensions.git_blame_entries_width,
+ cx,
+ );
+
let mut max_visible_line_width = Pixels::ZERO;
let line_layouts =
self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx);
@@ -3400,6 +3672,7 @@ impl Element for EditorElement {
redacted_ranges,
line_numbers,
display_hunks,
+ blamed_display_rows,
folds,
blocks,
cursors,
@@ -3486,6 +3759,7 @@ pub struct EditorLayout {
highlighted_rows: BTreeMap<u32, Hsla>,
line_numbers: Vec<Option<ShapedLine>>,
display_hunks: Vec<DisplayDiffHunk>,
+ blamed_display_rows: Option<Vec<AnyElement>>,
folds: Vec<FoldLayout>,
blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
@@ -3958,6 +4232,7 @@ mod tests {
element
.layout_line_numbers(
0..6,
+ (0..6).map(Some),
&Default::default(),
Some(DisplayPoint::new(0, 0)),
&snapshot,
@@ -3969,12 +4244,8 @@ mod tests {
.unwrap();
assert_eq!(layouts.len(), 6);
- let relative_rows = window
- .update(cx, |editor, cx| {
- let snapshot = editor.snapshot(cx);
- element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3))
- })
- .unwrap();
+ let relative_rows =
+ element.calculate_relative_line_numbers((0..6).map(Some).collect(), &(0..6), Some(3));
assert_eq!(relative_rows[&0], 3);
assert_eq!(relative_rows[&1], 2);
assert_eq!(relative_rows[&2], 1);
@@ -3983,26 +4254,16 @@ mod tests {
assert_eq!(relative_rows[&5], 2);
// works if cursor is before screen
- let relative_rows = window
- .update(cx, |editor, cx| {
- let snapshot = editor.snapshot(cx);
-
- element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1))
- })
- .unwrap();
+ let relative_rows =
+ element.calculate_relative_line_numbers((0..6).map(Some).collect(), &(3..6), Some(1));
assert_eq!(relative_rows.len(), 3);
assert_eq!(relative_rows[&3], 2);
assert_eq!(relative_rows[&4], 3);
assert_eq!(relative_rows[&5], 4);
// works if cursor is after screen
- let relative_rows = window
- .update(cx, |editor, cx| {
- let snapshot = editor.snapshot(cx);
-
- element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6))
- })
- .unwrap();
+ let relative_rows =
+ element.calculate_relative_line_numbers((0..6).map(Some).collect(), &(0..3), Some(6));
assert_eq!(relative_rows.len(), 3);
assert_eq!(relative_rows[&0], 5);
assert_eq!(relative_rows[&1], 4);
@@ -1,4 +1,4 @@
-pub mod permalink;
+pub mod blame;
use std::ops::Range;
@@ -0,0 +1,706 @@
+use anyhow::Result;
+use collections::HashMap;
+use git::{
+ blame::{Blame, BlameEntry},
+ Oid,
+};
+use gpui::{Model, ModelContext, Subscription, Task};
+use language::{Bias, Buffer, BufferSnapshot, Edit};
+use project::{Item, Project};
+use smallvec::SmallVec;
+use sum_tree::SumTree;
+use url::Url;
+
+#[derive(Clone, Debug, Default)]
+pub struct GitBlameEntry {
+ pub rows: u32,
+ pub blame: Option<BlameEntry>,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct GitBlameEntrySummary {
+ rows: u32,
+}
+
+impl sum_tree::Item for GitBlameEntry {
+ type Summary = GitBlameEntrySummary;
+
+ fn summary(&self) -> Self::Summary {
+ GitBlameEntrySummary { rows: self.rows }
+ }
+}
+
+impl sum_tree::Summary for GitBlameEntrySummary {
+ type Context = ();
+
+ fn add_summary(&mut self, summary: &Self, _cx: &()) {
+ self.rows += summary.rows;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, GitBlameEntrySummary> for u32 {
+ fn add_summary(&mut self, summary: &'a GitBlameEntrySummary, _cx: &()) {
+ *self += summary.rows;
+ }
+}
+
+pub struct GitBlame {
+ project: Model<Project>,
+ buffer: Model<Buffer>,
+ entries: SumTree<GitBlameEntry>,
+ permalinks: HashMap<Oid, Url>,
+ messages: HashMap<Oid, String>,
+ buffer_snapshot: BufferSnapshot,
+ buffer_edits: text::Subscription,
+ task: Task<Result<()>>,
+ generated: bool,
+ _refresh_subscription: Subscription,
+}
+
+impl GitBlame {
+ pub fn new(
+ buffer: Model<Buffer>,
+ project: Model<Project>,
+ cx: &mut ModelContext<Self>,
+ ) -> Self {
+ let entries = SumTree::from_item(
+ GitBlameEntry {
+ rows: buffer.read(cx).max_point().row + 1,
+ blame: None,
+ },
+ &(),
+ );
+
+ let refresh_subscription = cx.subscribe(&project, {
+ let buffer = buffer.clone();
+
+ move |this, _, event, cx| match event {
+ project::Event::WorktreeUpdatedEntries(_, updated) => {
+ let project_entry_id = buffer.read(cx).entry_id(cx);
+ if updated
+ .iter()
+ .any(|(_, entry_id, _)| project_entry_id == Some(*entry_id))
+ {
+ log::debug!("Updated buffers. Regenerating blame data...",);
+ this.generate(cx);
+ }
+ }
+ project::Event::WorktreeUpdatedGitRepositories => {
+ log::debug!("Status of git repositories updated. Regenerating blame data...",);
+ this.generate(cx);
+ }
+ _ => {}
+ }
+ });
+
+ let buffer_snapshot = buffer.read(cx).snapshot();
+ let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
+
+ let mut this = Self {
+ project,
+ buffer,
+ buffer_snapshot,
+ entries,
+ buffer_edits,
+ permalinks: HashMap::default(),
+ messages: HashMap::default(),
+ task: Task::ready(Ok(())),
+ generated: false,
+ _refresh_subscription: refresh_subscription,
+ };
+ this.generate(cx);
+ this
+ }
+
+ pub fn has_generated_entries(&self) -> bool {
+ self.generated
+ }
+
+ pub fn permalink_for_entry(&self, entry: &BlameEntry) -> Option<Url> {
+ self.permalinks.get(&entry.sha).cloned()
+ }
+
+ pub fn message_for_entry(&self, entry: &BlameEntry) -> Option<String> {
+ self.messages.get(&entry.sha).cloned()
+ }
+
+ pub fn blame_for_rows<'a>(
+ &'a mut self,
+ rows: impl 'a + IntoIterator<Item = Option<u32>>,
+ cx: &mut ModelContext<Self>,
+ ) -> impl 'a + Iterator<Item = Option<BlameEntry>> {
+ self.sync(cx);
+
+ let mut cursor = self.entries.cursor::<u32>();
+ rows.into_iter().map(move |row| {
+ let row = row?;
+ cursor.seek_forward(&row, Bias::Right, &());
+ cursor.item()?.blame.clone()
+ })
+ }
+
+ fn sync(&mut self, cx: &mut ModelContext<Self>) {
+ let edits = self.buffer_edits.consume();
+ let new_snapshot = self.buffer.read(cx).snapshot();
+
+ let mut row_edits = edits
+ .into_iter()
+ .map(|edit| {
+ let old_point_range = self.buffer_snapshot.offset_to_point(edit.old.start)
+ ..self.buffer_snapshot.offset_to_point(edit.old.end);
+ let new_point_range = new_snapshot.offset_to_point(edit.new.start)
+ ..new_snapshot.offset_to_point(edit.new.end);
+
+ if old_point_range.start.column
+ == self.buffer_snapshot.line_len(old_point_range.start.row)
+ && (new_snapshot.chars_at(edit.new.start).next() == Some('\n')
+ || self.buffer_snapshot.line_len(old_point_range.end.row) == 0)
+ {
+ Edit {
+ old: old_point_range.start.row + 1..old_point_range.end.row + 1,
+ new: new_point_range.start.row + 1..new_point_range.end.row + 1,
+ }
+ } else if old_point_range.start.column == 0
+ && old_point_range.end.column == 0
+ && new_point_range.end.column == 0
+ {
+ Edit {
+ old: old_point_range.start.row..old_point_range.end.row,
+ new: new_point_range.start.row..new_point_range.end.row,
+ }
+ } else {
+ Edit {
+ old: old_point_range.start.row..old_point_range.end.row + 1,
+ new: new_point_range.start.row..new_point_range.end.row + 1,
+ }
+ }
+ })
+ .peekable();
+
+ let mut new_entries = SumTree::new();
+ let mut cursor = self.entries.cursor::<u32>();
+
+ while let Some(mut edit) = row_edits.next() {
+ while let Some(next_edit) = row_edits.peek() {
+ if edit.old.end >= next_edit.old.start {
+ edit.old.end = next_edit.old.end;
+ edit.new.end = next_edit.new.end;
+ row_edits.next();
+ } else {
+ break;
+ }
+ }
+
+ new_entries.append(cursor.slice(&edit.old.start, Bias::Right, &()), &());
+
+ if edit.new.start > new_entries.summary().rows {
+ new_entries.push(
+ GitBlameEntry {
+ rows: edit.new.start - new_entries.summary().rows,
+ blame: cursor.item().and_then(|entry| entry.blame.clone()),
+ },
+ &(),
+ );
+ }
+
+ cursor.seek(&edit.old.end, Bias::Right, &());
+ if !edit.new.is_empty() {
+ new_entries.push(
+ GitBlameEntry {
+ rows: edit.new.len() as u32,
+ blame: None,
+ },
+ &(),
+ );
+ }
+
+ let old_end = cursor.end(&());
+ if row_edits
+ .peek()
+ .map_or(true, |next_edit| next_edit.old.start >= old_end)
+ {
+ if let Some(entry) = cursor.item() {
+ if old_end > edit.old.end {
+ new_entries.push(
+ GitBlameEntry {
+ rows: cursor.end(&()) - edit.old.end,
+ blame: entry.blame.clone(),
+ },
+ &(),
+ );
+ }
+
+ cursor.next(&());
+ }
+ }
+ }
+ new_entries.append(cursor.suffix(&()), &());
+ drop(cursor);
+
+ self.buffer_snapshot = new_snapshot;
+ self.entries = new_entries;
+ }
+
+ #[cfg(test)]
+ fn check_invariants(&mut self, cx: &mut ModelContext<Self>) {
+ self.sync(cx);
+ assert_eq!(
+ self.entries.summary().rows,
+ self.buffer.read(cx).max_point().row + 1
+ );
+ }
+
+ fn generate(&mut self, cx: &mut ModelContext<Self>) {
+ let buffer_edits = self.buffer.update(cx, |buffer, _| buffer.subscribe());
+ let snapshot = self.buffer.read(cx).snapshot();
+ let blame = self.project.read(cx).blame_buffer(&self.buffer, None, cx);
+
+ self.task = cx.spawn(|this, mut cx| async move {
+ let (entries, permalinks, messages) = cx
+ .background_executor()
+ .spawn({
+ let snapshot = snapshot.clone();
+ async move {
+ let Blame {
+ entries,
+ permalinks,
+ messages,
+ } = blame.await?;
+
+ let mut current_row = 0;
+ let mut entries = SumTree::from_iter(
+ entries.into_iter().flat_map(|entry| {
+ let mut entries = SmallVec::<[GitBlameEntry; 2]>::new();
+
+ if entry.range.start > current_row {
+ let skipped_rows = entry.range.start - current_row;
+ entries.push(GitBlameEntry {
+ rows: skipped_rows,
+ blame: None,
+ });
+ }
+ entries.push(GitBlameEntry {
+ rows: entry.range.len() as u32,
+ blame: Some(entry.clone()),
+ });
+
+ current_row = entry.range.end;
+ entries
+ }),
+ &(),
+ );
+
+ let max_row = snapshot.max_point().row;
+ if max_row >= current_row {
+ entries.push(
+ GitBlameEntry {
+ rows: (max_row + 1) - current_row,
+ blame: None,
+ },
+ &(),
+ );
+ }
+
+ anyhow::Ok((entries, permalinks, messages))
+ }
+ })
+ .await?;
+
+ this.update(&mut cx, |this, cx| {
+ this.buffer_edits = buffer_edits;
+ this.buffer_snapshot = snapshot;
+ this.entries = entries;
+ this.permalinks = permalinks;
+ this.messages = messages;
+ this.generated = true;
+ cx.notify();
+ })
+ });
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::Context;
+ use language::{Point, Rope};
+ use project::FakeFs;
+ use rand::prelude::*;
+ use serde_json::json;
+ use settings::SettingsStore;
+ use std::{cmp, env, ops::Range, path::Path};
+ use unindent::Unindent as _;
+ use util::RandomCharIter;
+
+ macro_rules! assert_blame_rows {
+ ($blame:expr, $rows:expr, $expected:expr, $cx:expr) => {
+ assert_eq!(
+ $blame
+ .blame_for_rows($rows.map(Some), $cx)
+ .collect::<Vec<_>>(),
+ $expected
+ );
+ };
+ }
+
+ fn init_test(cx: &mut gpui::TestAppContext) {
+ cx.update(|cx| {
+ let settings = SettingsStore::test(cx);
+ cx.set_global(settings);
+
+ theme::init(theme::LoadThemes::JustBase, cx);
+
+ language::init(cx);
+ client::init_settings(cx);
+ workspace::init_settings(cx);
+ Project::init_settings(cx);
+
+ crate::init(cx);
+ });
+ }
+
+ #[gpui::test]
+ async fn test_blame_for_rows(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ "/my-repo",
+ json!({
+ ".git": {},
+ "file.txt": r#"
+ AAA Line 1
+ BBB Line 2 - Modified 1
+ CCC Line 3 - Modified 2
+ modified in memory 1
+ modified in memory 1
+ DDD Line 4 - Modified 2
+ EEE Line 5 - Modified 1
+ FFF Line 6 - Modified 2
+ "#
+ .unindent()
+ }),
+ )
+ .await;
+
+ fs.set_blame_for_repo(
+ Path::new("/my-repo/.git"),
+ vec![(
+ Path::new("file.txt"),
+ Blame {
+ entries: vec![
+ blame_entry("1b1b1b", 0..1),
+ blame_entry("0d0d0d", 1..2),
+ blame_entry("3a3a3a", 2..3),
+ blame_entry("3a3a3a", 5..6),
+ blame_entry("0d0d0d", 6..7),
+ blame_entry("3a3a3a", 7..8),
+ ],
+ ..Default::default()
+ },
+ )],
+ );
+ let project = Project::test(fs, ["/my-repo".as_ref()], cx).await;
+ let buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/my-repo/file.txt", cx)
+ })
+ .await
+ .unwrap();
+
+ let git_blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project, cx));
+
+ cx.executor().run_until_parked();
+
+ git_blame.update(cx, |blame, cx| {
+ // All lines
+ assert_eq!(
+ blame
+ .blame_for_rows((0..8).map(Some), cx)
+ .collect::<Vec<_>>(),
+ vec![
+ Some(blame_entry("1b1b1b", 0..1)),
+ Some(blame_entry("0d0d0d", 1..2)),
+ Some(blame_entry("3a3a3a", 2..3)),
+ None,
+ None,
+ Some(blame_entry("3a3a3a", 5..6)),
+ Some(blame_entry("0d0d0d", 6..7)),
+ Some(blame_entry("3a3a3a", 7..8)),
+ ]
+ );
+ // Subset of lines
+ assert_eq!(
+ blame
+ .blame_for_rows((1..4).map(Some), cx)
+ .collect::<Vec<_>>(),
+ vec![
+ Some(blame_entry("0d0d0d", 1..2)),
+ Some(blame_entry("3a3a3a", 2..3)),
+ None
+ ]
+ );
+ // Subset of lines, with some not displayed
+ assert_eq!(
+ blame
+ .blame_for_rows(vec![Some(1), None, None], cx)
+ .collect::<Vec<_>>(),
+ vec![Some(blame_entry("0d0d0d", 1..2)), None, None]
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_blame_for_rows_with_edits(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ "/my-repo",
+ json!({
+ ".git": {},
+ "file.txt": r#"
+ Line 1
+ Line 2
+ Line 3
+ "#
+ .unindent()
+ }),
+ )
+ .await;
+
+ fs.set_blame_for_repo(
+ Path::new("/my-repo/.git"),
+ vec![(
+ Path::new("file.txt"),
+ Blame {
+ entries: vec![blame_entry("1b1b1b", 0..4)],
+ ..Default::default()
+ },
+ )],
+ );
+
+ let project = Project::test(fs, ["/my-repo".as_ref()], cx).await;
+ let buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/my-repo/file.txt", cx)
+ })
+ .await
+ .unwrap();
+
+ let git_blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project, cx));
+
+ cx.executor().run_until_parked();
+
+ git_blame.update(cx, |blame, cx| {
+ // Sanity check before edits: make sure that we get the same blame entry for all
+ // lines.
+ assert_blame_rows!(
+ blame,
+ (0..4),
+ vec![
+ Some(blame_entry("1b1b1b", 0..4)),
+ Some(blame_entry("1b1b1b", 0..4)),
+ Some(blame_entry("1b1b1b", 0..4)),
+ Some(blame_entry("1b1b1b", 0..4)),
+ ],
+ cx
+ );
+ });
+
+ // Modify a single line, at the start of the line
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit([(Point::new(0, 0)..Point::new(0, 0), "X")], None, cx);
+ });
+ git_blame.update(cx, |blame, cx| {
+ assert_blame_rows!(
+ blame,
+ (0..2),
+ vec![None, Some(blame_entry("1b1b1b", 0..4))],
+ cx
+ );
+ });
+ // Modify a single line, in the middle of the line
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit([(Point::new(1, 2)..Point::new(1, 2), "X")], None, cx);
+ });
+ git_blame.update(cx, |blame, cx| {
+ assert_blame_rows!(
+ blame,
+ (1..4),
+ vec![
+ None,
+ Some(blame_entry("1b1b1b", 0..4)),
+ Some(blame_entry("1b1b1b", 0..4))
+ ],
+ cx
+ );
+ });
+
+ // Before we insert a newline at the end, sanity check:
+ git_blame.update(cx, |blame, cx| {
+ assert_blame_rows!(blame, (3..4), vec![Some(blame_entry("1b1b1b", 0..4))], cx);
+ });
+ // Insert a newline at the end
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit([(Point::new(3, 6)..Point::new(3, 6), "\n")], None, cx);
+ });
+ // Only the new line is marked as edited:
+ git_blame.update(cx, |blame, cx| {
+ assert_blame_rows!(
+ blame,
+ (3..5),
+ vec![Some(blame_entry("1b1b1b", 0..4)), None],
+ cx
+ );
+ });
+
+ // Before we insert a newline at the start, sanity check:
+ git_blame.update(cx, |blame, cx| {
+ assert_blame_rows!(blame, (2..3), vec![Some(blame_entry("1b1b1b", 0..4)),], cx);
+ });
+
+ // Usage example
+ // Insert a newline at the start of the row
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "\n")], None, cx);
+ });
+ // Only the new line is marked as edited:
+ git_blame.update(cx, |blame, cx| {
+ assert_blame_rows!(
+ blame,
+ (2..4),
+ vec![None, Some(blame_entry("1b1b1b", 0..4)),],
+ cx
+ );
+ });
+ }
+
+ #[gpui::test(iterations = 100)]
+ async fn test_blame_random(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
+ let operations = env::var("OPERATIONS")
+ .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+ .unwrap_or(10);
+ let max_edits_per_operation = env::var("MAX_EDITS_PER_OPERATION")
+ .map(|i| {
+ i.parse()
+ .expect("invalid `MAX_EDITS_PER_OPERATION` variable")
+ })
+ .unwrap_or(5);
+
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ let buffer_initial_text_len = rng.gen_range(5..15);
+ let mut buffer_initial_text = Rope::from(
+ RandomCharIter::new(&mut rng)
+ .take(buffer_initial_text_len)
+ .collect::<String>()
+ .as_str(),
+ );
+
+ let mut newline_ixs = (0..buffer_initial_text_len).choose_multiple(&mut rng, 5);
+ newline_ixs.sort_unstable();
+ for newline_ix in newline_ixs.into_iter().rev() {
+ let newline_ix = buffer_initial_text.clip_offset(newline_ix, Bias::Right);
+ buffer_initial_text.replace(newline_ix..newline_ix, "\n");
+ }
+ log::info!("initial buffer text: {:?}", buffer_initial_text);
+
+ fs.insert_tree(
+ "/my-repo",
+ json!({
+ ".git": {},
+ "file.txt": buffer_initial_text.to_string()
+ }),
+ )
+ .await;
+
+ let blame_entries = gen_blame_entries(buffer_initial_text.max_point().row, &mut rng);
+ log::info!("initial blame entries: {:?}", blame_entries);
+ fs.set_blame_for_repo(
+ Path::new("/my-repo/.git"),
+ vec![(
+ Path::new("file.txt"),
+ Blame {
+ entries: blame_entries,
+ ..Default::default()
+ },
+ )],
+ );
+
+ let project = Project::test(fs.clone(), ["/my-repo".as_ref()], cx).await;
+ let buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/my-repo/file.txt", cx)
+ })
+ .await
+ .unwrap();
+
+ let git_blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project, cx));
+ cx.executor().run_until_parked();
+ git_blame.update(cx, |blame, cx| blame.check_invariants(cx));
+
+ for _ in 0..operations {
+ match rng.gen_range(0..100) {
+ 0..=19 => {
+ log::info!("quiescing");
+ cx.executor().run_until_parked();
+ }
+ 20..=69 => {
+ log::info!("editing buffer");
+ buffer.update(cx, |buffer, cx| {
+ buffer.randomly_edit(&mut rng, max_edits_per_operation, cx);
+ log::info!("buffer text: {:?}", buffer.text());
+ });
+
+ let blame_entries = gen_blame_entries(
+ buffer.read_with(cx, |buffer, _| buffer.max_point().row),
+ &mut rng,
+ );
+ log::info!("regenerating blame entries: {:?}", blame_entries);
+
+ fs.set_blame_for_repo(
+ Path::new("/my-repo/.git"),
+ vec![(
+ Path::new("file.txt"),
+ Blame {
+ entries: blame_entries,
+ ..Default::default()
+ },
+ )],
+ );
+ }
+ _ => {
+ git_blame.update(cx, |blame, cx| blame.check_invariants(cx));
+ }
+ }
+ }
+
+ git_blame.update(cx, |blame, cx| blame.check_invariants(cx));
+ }
+
+ fn gen_blame_entries(max_row: u32, rng: &mut StdRng) -> Vec<BlameEntry> {
+ let mut last_row = 0;
+ let mut blame_entries = Vec::new();
+ for ix in 0..5 {
+ if last_row < max_row {
+ let row_start = rng.gen_range(last_row..max_row);
+ let row_end = rng.gen_range(row_start + 1..cmp::min(row_start + 3, max_row) + 1);
+ blame_entries.push(blame_entry(&ix.to_string(), row_start..row_end));
+ last_row = row_end;
+ } else {
+ break;
+ }
+ }
+ blame_entries
+ }
+
+ fn blame_entry(sha: &str, range: Range<u32>) -> BlameEntry {
+ BlameEntry {
+ sha: sha.parse().unwrap(),
+ range,
+ ..Default::default()
+ }
+ }
+}
@@ -10,6 +10,31 @@ pub struct MouseContextMenu {
_subscription: Subscription,
}
+impl MouseContextMenu {
+ pub(crate) fn new(
+ position: Point<Pixels>,
+ context_menu: View<ui::ContextMenu>,
+ cx: &mut ViewContext<Editor>,
+ ) -> Self {
+ let context_menu_focus = context_menu.focus_handle(cx);
+ cx.focus(&context_menu_focus);
+
+ let _subscription =
+ cx.subscribe(&context_menu, move |this, _, _event: &DismissEvent, cx| {
+ this.mouse_context_menu.take();
+ if context_menu_focus.contains_focused(cx) {
+ this.focus(cx);
+ }
+ });
+
+ Self {
+ position,
+ context_menu,
+ _subscription,
+ }
+ }
+}
+
pub fn deploy_context_menu(
editor: &mut Editor,
position: Point<Pixels>,
@@ -60,21 +85,8 @@ pub fn deploy_context_menu(
.action("Reveal in Finder", Box::new(RevealInFinder))
})
};
- let context_menu_focus = context_menu.focus_handle(cx);
- cx.focus(&context_menu_focus);
-
- let _subscription = cx.subscribe(&context_menu, move |this, _, _event: &DismissEvent, cx| {
- this.mouse_context_menu.take();
- if context_menu_focus.contains_focused(cx) {
- this.focus(cx);
- }
- });
-
- editor.mouse_context_menu = Some(MouseContextMenu {
- position,
- context_menu,
- _subscription,
- });
+ let mouse_context_menu = MouseContextMenu::new(position, context_menu, cx);
+ editor.mouse_context_menu = Some(mouse_context_menu);
cx.notify();
}
@@ -449,7 +449,7 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
let cache_dir = root_dir.join("target");
let gleam_extension_dir = root_dir.join("extensions").join("gleam");
- let fs = Arc::new(RealFs);
+ let fs = Arc::new(RealFs::default());
let extensions_dir = temp_tree(json!({
"installed": {},
"work": {}
@@ -36,7 +36,7 @@ async fn main() -> Result<()> {
env_logger::init();
let args = Args::parse();
- let fs = Arc::new(RealFs);
+ let fs = Arc::new(RealFs::default());
let engine = wasmtime::Engine::default();
let mut wasm_store = WasmStore::new(engine)?;
@@ -26,6 +26,7 @@ tempfile.workspace = true
lazy_static.workspace = true
parking_lot.workspace = true
smol.workspace = true
+git.workspace = true
git2.workspace = true
serde.workspace = true
serde_derive.workspace = true
@@ -9,7 +9,7 @@ use async_tar::Archive;
use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt};
use git2::Repository as LibGitRepository;
use parking_lot::Mutex;
-use repository::GitRepository;
+use repository::{GitRepository, RealGitRepository};
use rope::Rope;
#[cfg(any(test, feature = "test-support"))]
use smol::io::AsyncReadExt;
@@ -111,7 +111,16 @@ pub struct Metadata {
pub is_dir: bool,
}
-pub struct RealFs;
+#[derive(Default)]
+pub struct RealFs {
+ git_binary_path: Option<PathBuf>,
+}
+
+impl RealFs {
+ pub fn new(git_binary_path: Option<PathBuf>) -> Self {
+ Self { git_binary_path }
+ }
+}
#[async_trait::async_trait]
impl Fs for RealFs {
@@ -431,7 +440,10 @@ impl Fs for RealFs {
LibGitRepository::open(dotgit_path)
.log_err()
.map::<Arc<Mutex<dyn GitRepository>>, _>(|libgit_repository| {
- Arc::new(Mutex::new(libgit_repository))
+ Arc::new(Mutex::new(RealGitRepository::new(
+ libgit_repository,
+ self.git_binary_path.clone(),
+ )))
})
}
@@ -824,6 +836,17 @@ impl FakeFs {
});
}
+ pub fn set_blame_for_repo(&self, dot_git: &Path, blames: Vec<(&Path, git::blame::Blame)>) {
+ self.with_git_state(dot_git, true, |state| {
+ state.blames.clear();
+ state.blames.extend(
+ blames
+ .into_iter()
+ .map(|(path, blame)| (path.to_path_buf(), blame)),
+ );
+ });
+ }
+
pub fn set_status_for_repo_via_working_copy_change(
&self,
dot_git: &Path,
@@ -1,7 +1,9 @@
-use anyhow::Result;
+use anyhow::{Context, Result};
use collections::HashMap;
+use git::blame::Blame;
use git2::{BranchType, StatusShow};
use parking_lot::Mutex;
+use rope::Rope;
use serde_derive::{Deserialize, Serialize};
use std::{
cmp::Ordering,
@@ -53,6 +55,8 @@ pub trait GitRepository: Send {
fn branches(&self) -> Result<Vec<Branch>>;
fn change_branch(&self, _: &str) -> Result<()>;
fn create_branch(&self, _: &str) -> Result<()>;
+
+ fn blame(&self, path: &Path, content: Rope) -> Result<git::blame::Blame>;
}
impl std::fmt::Debug for dyn GitRepository {
@@ -61,9 +65,23 @@ impl std::fmt::Debug for dyn GitRepository {
}
}
-impl GitRepository for LibGitRepository {
+pub struct RealGitRepository {
+ pub repository: LibGitRepository,
+ pub git_binary_path: PathBuf,
+}
+
+impl RealGitRepository {
+ pub fn new(repository: LibGitRepository, git_binary_path: Option<PathBuf>) -> Self {
+ Self {
+ repository,
+ git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
+ }
+ }
+}
+
+impl GitRepository for RealGitRepository {
fn reload_index(&self) {
- if let Ok(mut index) = self.index() {
+ if let Ok(mut index) = self.repository.index() {
_ = index.read(false);
}
}
@@ -85,7 +103,7 @@ impl GitRepository for LibGitRepository {
Ok(Some(String::from_utf8(content)?))
}
- match logic(self, relative_file_path) {
+ match logic(&self.repository, relative_file_path) {
Ok(value) => return value,
Err(err) => log::error!("Error loading head text: {:?}", err),
}
@@ -93,18 +111,18 @@ impl GitRepository for LibGitRepository {
}
fn remote_url(&self, name: &str) -> Option<String> {
- let remote = self.find_remote(name).ok()?;
+ let remote = self.repository.find_remote(name).ok()?;
remote.url().map(|url| url.to_string())
}
fn branch_name(&self) -> Option<String> {
- let head = self.head().log_err()?;
+ let head = self.repository.head().log_err()?;
let branch = String::from_utf8_lossy(head.shorthand_bytes());
Some(branch.to_string())
}
fn head_sha(&self) -> Option<String> {
- let head = self.head().ok()?;
+ let head = self.repository.head().ok()?;
head.target().map(|oid| oid.to_string())
}
@@ -115,7 +133,7 @@ impl GitRepository for LibGitRepository {
options.pathspec(path_prefix);
options.show(StatusShow::Index);
- if let Some(statuses) = self.statuses(Some(&mut options)).log_err() {
+ if let Some(statuses) = self.repository.statuses(Some(&mut options)).log_err() {
for status in statuses.iter() {
let path = RepoPath(PathBuf::try_from_bytes(status.path_bytes()).unwrap());
let status = status.status();
@@ -132,7 +150,7 @@ impl GitRepository for LibGitRepository {
fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus> {
// If the file has not changed since it was added to the index, then
// there can't be any changes.
- if matches_index(self, path, mtime) {
+ if matches_index(&self.repository, path, mtime) {
return None;
}
@@ -144,7 +162,7 @@ impl GitRepository for LibGitRepository {
options.include_unmodified(true);
options.show(StatusShow::Workdir);
- let statuses = self.statuses(Some(&mut options)).log_err()?;
+ let statuses = self.repository.statuses(Some(&mut options)).log_err()?;
let status = statuses.get(0).and_then(|s| read_status(s.status()));
status
}
@@ -160,17 +178,17 @@ impl GitRepository for LibGitRepository {
// If the file has not changed since it was added to the index, then
// there's no need to examine the working directory file: just compare
// the blob in the index to the one in the HEAD commit.
- if matches_index(self, path, mtime) {
+ if matches_index(&self.repository, path, mtime) {
options.show(StatusShow::Index);
}
- let statuses = self.statuses(Some(&mut options)).log_err()?;
+ let statuses = self.repository.statuses(Some(&mut options)).log_err()?;
let status = statuses.get(0).and_then(|s| read_status(s.status()));
status
}
fn branches(&self) -> Result<Vec<Branch>> {
- let local_branches = self.branches(Some(BranchType::Local))?;
+ let local_branches = self.repository.branches(Some(BranchType::Local))?;
let valid_branches = local_branches
.filter_map(|branch| {
branch.ok().and_then(|(branch, _)| {
@@ -192,11 +210,11 @@ impl GitRepository for LibGitRepository {
Ok(valid_branches)
}
fn change_branch(&self, name: &str) -> Result<()> {
- let revision = self.find_branch(name, BranchType::Local)?;
+ let revision = self.repository.find_branch(name, BranchType::Local)?;
let revision = revision.get();
let as_tree = revision.peel_to_tree()?;
- self.checkout_tree(as_tree.as_object(), None)?;
- self.set_head(
+ self.repository.checkout_tree(as_tree.as_object(), None)?;
+ self.repository.set_head(
revision
.name()
.ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?,
@@ -204,11 +222,29 @@ impl GitRepository for LibGitRepository {
Ok(())
}
fn create_branch(&self, name: &str) -> Result<()> {
- let current_commit = self.head()?.peel_to_commit()?;
- self.branch(name, ¤t_commit, false)?;
+ let current_commit = self.repository.head()?.peel_to_commit()?;
+ self.repository.branch(name, ¤t_commit, false)?;
Ok(())
}
+
+ fn blame(&self, path: &Path, content: Rope) -> Result<git::blame::Blame> {
+ let git_dir_path = self.repository.path();
+ let working_directory = git_dir_path.parent().with_context(|| {
+ format!("failed to get git working directory for {:?}", git_dir_path)
+ })?;
+
+ const REMOTE_NAME: &str = "origin";
+ let remote_url = self.remote_url(REMOTE_NAME);
+
+ git::blame::Blame::for_path(
+ &self.git_binary_path,
+ working_directory,
+ path,
+ &content,
+ remote_url,
+ )
+ }
}
fn matches_index(repo: &LibGitRepository, path: &RepoPath, mtime: SystemTime) -> bool {
@@ -251,6 +287,7 @@ pub struct FakeGitRepository {
#[derive(Debug, Clone, Default)]
pub struct FakeGitRepositoryState {
pub index_contents: HashMap<PathBuf, String>,
+ pub blames: HashMap<PathBuf, Blame>,
pub worktree_statuses: HashMap<RepoPath, GitFileStatus>,
pub branch_name: Option<String>,
}
@@ -317,6 +354,15 @@ impl GitRepository for FakeGitRepository {
state.branch_name = Some(name.to_owned());
Ok(())
}
+
+ fn blame(&self, path: &Path, _content: Rope) -> Result<git::blame::Blame> {
+ let state = self.state.lock();
+ state
+ .blames
+ .get(path)
+ .with_context(|| format!("failed to get blame for {:?}", path))
+ .cloned()
+ }
}
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
@@ -12,16 +12,23 @@ workspace = true
path = "src/git.rs"
[dependencies]
+anyhow.workspace = true
clock.workspace = true
+collections.workspace = true
git2.workspace = true
lazy_static.workspace = true
log.workspace = true
smol.workspace = true
sum_tree.workspace = true
text.workspace = true
+time.workspace = true
+url.workspace = true
+serde.workspace = true
[dev-dependencies]
unindent.workspace = true
+serde_json.workspace = true
+pretty_assertions.workspace = true
[features]
test-support = []
@@ -0,0 +1,358 @@
+use crate::commit::get_messages;
+use crate::permalink::{build_commit_permalink, parse_git_remote_url, BuildCommitPermalinkParams};
+use crate::Oid;
+use anyhow::{anyhow, Context, Result};
+use collections::{HashMap, HashSet};
+use serde::{Deserialize, Serialize};
+use std::io::Write;
+use std::process::{Command, Stdio};
+use std::{ops::Range, path::Path};
+use text::Rope;
+use time;
+use time::macros::format_description;
+use time::OffsetDateTime;
+use time::UtcOffset;
+use url::Url;
+
+pub use git2 as libgit;
+
+#[derive(Debug, Clone, Default)]
+pub struct Blame {
+ pub entries: Vec<BlameEntry>,
+ pub messages: HashMap<Oid, String>,
+ pub permalinks: HashMap<Oid, Url>,
+}
+
+impl Blame {
+ pub fn for_path(
+ git_binary: &Path,
+ working_directory: &Path,
+ path: &Path,
+ content: &Rope,
+ remote_url: Option<String>,
+ ) -> Result<Self> {
+ let output = run_git_blame(git_binary, working_directory, path, &content)?;
+ let mut entries = parse_git_blame(&output)?;
+ entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start));
+
+ let mut permalinks = HashMap::default();
+ let mut unique_shas = HashSet::default();
+ let parsed_remote_url = remote_url.as_deref().and_then(parse_git_remote_url);
+
+ for entry in entries.iter_mut() {
+ unique_shas.insert(entry.sha);
+ if let Some(remote) = parsed_remote_url.as_ref() {
+ permalinks.entry(entry.sha).or_insert_with(|| {
+ build_commit_permalink(BuildCommitPermalinkParams {
+ remote,
+ sha: entry.sha.to_string().as_str(),
+ })
+ });
+ }
+ }
+
+ let shas = unique_shas.into_iter().collect::<Vec<_>>();
+ let messages =
+ get_messages(&working_directory, &shas).context("failed to get commit messages")?;
+
+ Ok(Self {
+ entries,
+ permalinks,
+ messages,
+ })
+ }
+}
+
+fn run_git_blame(
+ git_binary: &Path,
+ working_directory: &Path,
+ path: &Path,
+ contents: &Rope,
+) -> Result<String> {
+ let child = Command::new(git_binary)
+ .current_dir(working_directory)
+ .arg("blame")
+ .arg("--incremental")
+ .arg("--contents")
+ .arg("-")
+ .arg(path.as_os_str())
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .spawn()
+ .map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
+
+ let mut stdin = child
+ .stdin
+ .as_ref()
+ .context("failed to get pipe to stdin of git blame command")?;
+
+ for chunk in contents.chunks() {
+ stdin.write_all(chunk.as_bytes())?;
+ }
+ stdin.flush()?;
+
+ let output = child
+ .wait_with_output()
+ .map_err(|e| anyhow!("Failed to read git blame output: {}", e))?;
+
+ if !output.status.success() {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ return Err(anyhow!("git blame process failed: {}", stderr));
+ }
+
+ Ok(String::from_utf8(output.stdout)?)
+}
+
+#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
+pub struct BlameEntry {
+ pub sha: Oid,
+
+ pub range: Range<u32>,
+
+ pub original_line_number: u32,
+
+ pub author: Option<String>,
+ pub author_mail: Option<String>,
+ pub author_time: Option<i64>,
+ pub author_tz: Option<String>,
+
+ pub committer: Option<String>,
+ pub committer_mail: Option<String>,
+ pub committer_time: Option<i64>,
+ pub committer_tz: Option<String>,
+
+ pub summary: Option<String>,
+
+ pub previous: Option<String>,
+ pub filename: String,
+}
+
+impl BlameEntry {
+ // Returns a BlameEntry by parsing the first line of a `git blame --incremental`
+ // entry. The line MUST have this format:
+ //
+ // <40-byte-hex-sha1> <sourceline> <resultline> <num-lines>
+ fn new_from_blame_line(line: &str) -> Result<BlameEntry> {
+ let mut parts = line.split_whitespace();
+
+ let sha = parts
+ .next()
+ .and_then(|line| line.parse::<Oid>().ok())
+ .ok_or_else(|| anyhow!("failed to parse sha"))?;
+
+ let original_line_number = parts
+ .next()
+ .and_then(|line| line.parse::<u32>().ok())
+ .ok_or_else(|| anyhow!("Failed to parse original line number"))?;
+ let final_line_number = parts
+ .next()
+ .and_then(|line| line.parse::<u32>().ok())
+ .ok_or_else(|| anyhow!("Failed to parse final line number"))?;
+
+ let line_count = parts
+ .next()
+ .and_then(|line| line.parse::<u32>().ok())
+ .ok_or_else(|| anyhow!("Failed to parse final line number"))?;
+
+ let start_line = final_line_number.saturating_sub(1);
+ let end_line = start_line + line_count;
+ let range = start_line..end_line;
+
+ Ok(Self {
+ sha,
+ range,
+ original_line_number,
+ ..Default::default()
+ })
+ }
+
+ pub fn author_offset_date_time(&self) -> Result<time::OffsetDateTime> {
+ if let (Some(author_time), Some(author_tz)) = (self.author_time, &self.author_tz) {
+ let format = format_description!("[offset_hour][offset_minute]");
+ let offset = UtcOffset::parse(author_tz, &format)?;
+ let date_time_utc = OffsetDateTime::from_unix_timestamp(author_time)?;
+
+ Ok(date_time_utc.to_offset(offset))
+ } else {
+ // Directly return current time in UTC if there's no committer time or timezone
+ Ok(time::OffsetDateTime::now_utc())
+ }
+ }
+}
+
+// parse_git_blame parses the output of `git blame --incremental`, which returns
+// all the blame-entries for a given path incrementally, as it finds them.
+//
+// Each entry *always* starts with:
+//
+// <40-byte-hex-sha1> <sourceline> <resultline> <num-lines>
+//
+// Each entry *always* ends with:
+//
+// filename <whitespace-quoted-filename-goes-here>
+//
+// Line numbers are 1-indexed.
+//
+// A `git blame --incremental` entry looks like this:
+//
+// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 2 2 1
+// author Joe Schmoe
+// author-mail <joe.schmoe@example.com>
+// author-time 1709741400
+// author-tz +0100
+// committer Joe Schmoe
+// committer-mail <joe.schmoe@example.com>
+// committer-time 1709741400
+// committer-tz +0100
+// summary Joe's cool commit
+// previous 486c2409237a2c627230589e567024a96751d475 index.js
+// filename index.js
+//
+// If the entry has the same SHA as an entry that was already printed then no
+// signature information is printed:
+//
+// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 3 4 1
+// previous 486c2409237a2c627230589e567024a96751d475 index.js
+// filename index.js
+//
+// More about `--incremental` output: https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-blame.html
+fn parse_git_blame(output: &str) -> Result<Vec<BlameEntry>> {
+ let mut entries: Vec<BlameEntry> = Vec::new();
+ let mut index: HashMap<Oid, usize> = HashMap::default();
+
+ let mut current_entry: Option<BlameEntry> = None;
+
+ for line in output.lines() {
+ let mut done = false;
+
+ match &mut current_entry {
+ None => {
+ let mut new_entry = BlameEntry::new_from_blame_line(line)?;
+
+ if let Some(existing_entry) = index
+ .get(&new_entry.sha)
+ .and_then(|slot| entries.get(*slot))
+ {
+ new_entry.author = existing_entry.author.clone();
+ new_entry.author_mail = existing_entry.author_mail.clone();
+ new_entry.author_time = existing_entry.author_time;
+ new_entry.author_tz = existing_entry.author_tz.clone();
+ new_entry.committer = existing_entry.committer.clone();
+ new_entry.committer_mail = existing_entry.committer_mail.clone();
+ new_entry.committer_time = existing_entry.committer_time;
+ new_entry.committer_tz = existing_entry.committer_tz.clone();
+ new_entry.summary = existing_entry.summary.clone();
+ }
+
+ current_entry.replace(new_entry);
+ }
+ Some(entry) => {
+ let Some((key, value)) = line.split_once(' ') else {
+ continue;
+ };
+ let is_committed = !entry.sha.is_zero();
+ match key {
+ "filename" => {
+ entry.filename = value.into();
+ done = true;
+ }
+ "previous" => entry.previous = Some(value.into()),
+
+ "summary" if is_committed => entry.summary = Some(value.into()),
+ "author" if is_committed => entry.author = Some(value.into()),
+ "author-mail" if is_committed => entry.author_mail = Some(value.into()),
+ "author-time" if is_committed => {
+ entry.author_time = Some(value.parse::<i64>()?)
+ }
+ "author-tz" if is_committed => entry.author_tz = Some(value.into()),
+
+ "committer" if is_committed => entry.committer = Some(value.into()),
+ "committer-mail" if is_committed => entry.committer_mail = Some(value.into()),
+ "committer-time" if is_committed => {
+ entry.committer_time = Some(value.parse::<i64>()?)
+ }
+ "committer-tz" if is_committed => entry.committer_tz = Some(value.into()),
+ _ => {}
+ }
+ }
+ };
+
+ if done {
+ if let Some(entry) = current_entry.take() {
+ index.insert(entry.sha, entries.len());
+
+ // We only want annotations that have a commit.
+ if !entry.sha.is_zero() {
+ entries.push(entry);
+ }
+ }
+ }
+ }
+
+ Ok(entries)
+}
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use super::parse_git_blame;
+ use super::BlameEntry;
+
+ fn read_test_data(filename: &str) -> String {
+ let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ path.push("test_data");
+ path.push(filename);
+
+ std::fs::read_to_string(&path)
+ .unwrap_or_else(|_| panic!("Could not read test data at {:?}. Is it generated?", path))
+ }
+
+ fn assert_eq_golden(entries: &Vec<BlameEntry>, golden_filename: &str) {
+ let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ path.push("test_data");
+ path.push("golden");
+ path.push(format!("{}.json", golden_filename));
+
+ let have_json =
+ serde_json::to_string_pretty(&entries).expect("could not serialize entries to JSON");
+
+ let update = std::env::var("UPDATE_GOLDEN")
+ .map(|val| val.to_ascii_lowercase() == "true")
+ .unwrap_or(false);
+
+ if update {
+ std::fs::create_dir_all(path.parent().unwrap())
+ .expect("could not create golden test data directory");
+ std::fs::write(&path, have_json).expect("could not write out golden data");
+ } else {
+ let want_json =
+ std::fs::read_to_string(&path).unwrap_or_else(|_| {
+ panic!("could not read golden test data file at {:?}. Did you run the test with UPDATE_GOLDEN=true before?", path);
+ });
+
+ pretty_assertions::assert_eq!(have_json, want_json, "wrong blame entries");
+ }
+ }
+
+ #[test]
+ fn test_parse_git_blame_not_committed() {
+ let output = read_test_data("blame_incremental_not_committed");
+ let entries = parse_git_blame(&output).unwrap();
+ assert_eq_golden(&entries, "blame_incremental_not_committed");
+ }
+
+ #[test]
+ fn test_parse_git_blame_simple() {
+ let output = read_test_data("blame_incremental_simple");
+ let entries = parse_git_blame(&output).unwrap();
+ assert_eq_golden(&entries, "blame_incremental_simple");
+ }
+
+ #[test]
+ fn test_parse_git_blame_complex() {
+ let output = read_test_data("blame_incremental_complex");
+ let entries = parse_git_blame(&output).unwrap();
+ assert_eq_golden(&entries, "blame_incremental_complex");
+ }
+}
@@ -0,0 +1,35 @@
+use crate::Oid;
+use anyhow::{anyhow, Result};
+use collections::HashMap;
+use std::path::Path;
+use std::process::Command;
+
+pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
+ const MARKER: &'static str = "<MARKER>";
+
+ let output = Command::new("git")
+ .current_dir(working_directory)
+ .arg("show")
+ .arg("-s")
+ .arg(format!("--format=%B{}", MARKER))
+ .args(shas.iter().map(ToString::to_string))
+ .output()
+ .map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
+
+ anyhow::ensure!(
+ output.status.success(),
+ "'git show' failed with error {:?}",
+ output.status
+ );
+
+ Ok(shas
+ .iter()
+ .cloned()
+ .zip(
+ String::from_utf8_lossy(&output.stdout)
+ .trim()
+ .split_terminator(MARKER)
+ .map(|str| String::from(str.trim())),
+ )
+ .collect::<HashMap<Oid, String>>())
+}
@@ -1,11 +1,107 @@
+use anyhow::{anyhow, Context, Result};
+use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
+use std::fmt;
+use std::str::FromStr;
pub use git2 as libgit;
pub use lazy_static::lazy_static;
+pub mod blame;
+pub mod commit;
pub mod diff;
+pub mod permalink;
lazy_static! {
pub static ref DOT_GIT: &'static OsStr = OsStr::new(".git");
pub static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
}
+
+#[derive(Clone, Copy, Eq, Hash, PartialEq)]
+pub struct Oid(libgit::Oid);
+
+impl Oid {
+ pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
+ let oid = libgit::Oid::from_bytes(bytes).context("failed to parse bytes into git oid")?;
+ Ok(Self(oid))
+ }
+
+ pub fn as_bytes(&self) -> &[u8] {
+ self.0.as_bytes()
+ }
+
+ pub(crate) fn is_zero(&self) -> bool {
+ self.0.is_zero()
+ }
+}
+
+impl FromStr for Oid {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> {
+ libgit::Oid::from_str(s)
+ .map_err(|error| anyhow!("failed to parse git oid: {}", error))
+ .map(|oid| Self(oid))
+ }
+}
+
+impl fmt::Debug for Oid {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Display::fmt(self, f)
+ }
+}
+
+impl fmt::Display for Oid {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl Serialize for Oid {
+ fn serialize<S>(&self, serializer: S) -> std::prelude::v1::Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(&self.0.to_string())
+ }
+}
+
+impl<'de> Deserialize<'de> for Oid {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let s = String::deserialize(deserializer)?;
+ s.parse::<Oid>().map_err(serde::de::Error::custom)
+ }
+}
+
+impl Default for Oid {
+ fn default() -> Self {
+ Self(libgit::Oid::zero())
+ }
+}
+
+impl From<Oid> for u32 {
+ fn from(oid: Oid) -> Self {
+ let bytes = oid.0.as_bytes();
+ debug_assert!(bytes.len() > 4);
+
+ let mut u32_bytes: [u8; 4] = [0; 4];
+ u32_bytes.copy_from_slice(&bytes[..4]);
+
+ u32::from_ne_bytes(u32_bytes)
+ }
+}
+
+impl From<Oid> for usize {
+ fn from(oid: Oid) -> Self {
+ let bytes = oid.0.as_bytes();
+ debug_assert!(bytes.len() > 8);
+
+ let mut u64_bytes: [u8; 8] = [0; 8];
+ u64_bytes.copy_from_slice(&bytes[..8]);
+
+ u64::from_ne_bytes(u64_bytes) as usize
+ }
+}
@@ -1,10 +1,9 @@
use std::ops::Range;
use anyhow::{anyhow, Result};
-use language::Point;
use url::Url;
-enum GitHostingProvider {
+pub(crate) enum GitHostingProvider {
Github,
Gitlab,
Gitee,
@@ -29,9 +28,9 @@ impl GitHostingProvider {
/// Returns the fragment portion of the URL for the selected lines in
/// the representation the [`GitHostingProvider`] expects.
- fn line_fragment(&self, selection: &Range<Point>) -> String {
- if selection.start.row == selection.end.row {
- let line = selection.start.row + 1;
+ fn line_fragment(&self, selection: &Range<u32>) -> String {
+ if selection.start == selection.end {
+ let line = selection.start + 1;
match self {
Self::Github | Self::Gitlab | Self::Gitee | Self::Sourcehut | Self::Codeberg => {
@@ -40,8 +39,8 @@ impl GitHostingProvider {
Self::Bitbucket => format!("lines-{}", line),
}
} else {
- let start_line = selection.start.row + 1;
- let end_line = selection.end.row + 1;
+ let start_line = selection.start + 1;
+ let end_line = selection.end + 1;
match self {
Self::Github | Self::Codeberg => format!("L{}-L{}", start_line, end_line),
@@ -58,7 +57,7 @@ pub struct BuildPermalinkParams<'a> {
pub remote_url: &'a str,
pub sha: &'a str,
pub path: &'a str,
- pub selection: Option<Range<Point>>,
+ pub selection: Option<Range<u32>>,
}
pub fn build_permalink(params: BuildPermalinkParams) -> Result<Url> {
@@ -88,17 +87,42 @@ pub fn build_permalink(params: BuildPermalinkParams) -> Result<Url> {
let mut permalink = provider.base_url().join(&path).unwrap();
permalink.set_fragment(line_fragment.as_deref());
-
Ok(permalink)
}
-struct ParsedGitRemote<'a> {
+pub(crate) struct ParsedGitRemote<'a> {
pub provider: GitHostingProvider,
pub owner: &'a str,
pub repo: &'a str,
}
-fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
+pub(crate) struct BuildCommitPermalinkParams<'a> {
+ pub remote: &'a ParsedGitRemote<'a>,
+ pub sha: &'a str,
+}
+
+pub(crate) fn build_commit_permalink(params: BuildCommitPermalinkParams) -> Url {
+ let BuildCommitPermalinkParams { sha, remote } = params;
+
+ let ParsedGitRemote {
+ provider,
+ owner,
+ repo,
+ } = remote;
+
+ let path = match provider {
+ GitHostingProvider::Github => format!("{owner}/{repo}/commits/{sha}"),
+ GitHostingProvider::Gitlab => format!("{owner}/{repo}/-/commit/{sha}"),
+ GitHostingProvider::Gitee => format!("{owner}/{repo}/commit/{sha}"),
+ GitHostingProvider::Bitbucket => format!("{owner}/{repo}/commits/{sha}"),
+ GitHostingProvider::Sourcehut => format!("~{owner}/{repo}/commit/{sha}"),
+ GitHostingProvider::Codeberg => format!("{owner}/{repo}/commit/{sha}"),
+ };
+
+ provider.base_url().join(&path).unwrap()
+}
+
+pub(crate) fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
if url.starts_with("git@github.com:") || url.starts_with("https://github.com/") {
let repo_with_owner = url
.trim_start_matches("git@github.com:")
@@ -217,7 +241,7 @@ mod tests {
remote_url: "git@github.com:zed-industries/zed.git",
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
- selection: Some(Point::new(6, 1)..Point::new(6, 10)),
+ selection: Some(6..6),
})
.unwrap();
@@ -231,7 +255,7 @@ mod tests {
remote_url: "git@github.com:zed-industries/zed.git",
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
- selection: Some(Point::new(23, 1)..Point::new(47, 10)),
+ selection: Some(23..47),
})
.unwrap();
@@ -259,7 +283,7 @@ mod tests {
remote_url: "https://github.com/zed-industries/zed.git",
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
path: "crates/zed/src/main.rs",
- selection: Some(Point::new(6, 1)..Point::new(6, 10)),
+ selection: Some(6..6),
})
.unwrap();
@@ -273,7 +297,7 @@ mod tests {
remote_url: "https://github.com/zed-industries/zed.git",
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
path: "crates/zed/src/main.rs",
- selection: Some(Point::new(23, 1)..Point::new(47, 10)),
+ selection: Some(23..47),
})
.unwrap();
@@ -301,7 +325,7 @@ mod tests {
remote_url: "git@gitlab.com:zed-industries/zed.git",
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
- selection: Some(Point::new(6, 1)..Point::new(6, 10)),
+ selection: Some(6..6),
})
.unwrap();
@@ -315,7 +339,7 @@ mod tests {
remote_url: "git@gitlab.com:zed-industries/zed.git",
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
- selection: Some(Point::new(23, 1)..Point::new(47, 10)),
+ selection: Some(23..47),
})
.unwrap();
@@ -343,7 +367,7 @@ mod tests {
remote_url: "https://gitlab.com/zed-industries/zed.git",
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
path: "crates/zed/src/main.rs",
- selection: Some(Point::new(6, 1)..Point::new(6, 10)),
+ selection: Some(6..6),
})
.unwrap();
@@ -357,7 +381,7 @@ mod tests {
remote_url: "https://gitlab.com/zed-industries/zed.git",
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
path: "crates/zed/src/main.rs",
- selection: Some(Point::new(23, 1)..Point::new(47, 10)),
+ selection: Some(23..47),
})
.unwrap();
@@ -385,7 +409,7 @@ mod tests {
remote_url: "git@gitee.com:libkitten/zed.git",
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
path: "crates/editor/src/git/permalink.rs",
- selection: Some(Point::new(6, 1)..Point::new(6, 10)),
+ selection: Some(6..6),
})
.unwrap();
@@ -399,7 +423,7 @@ mod tests {
remote_url: "git@gitee.com:libkitten/zed.git",
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
path: "crates/editor/src/git/permalink.rs",
- selection: Some(Point::new(23, 1)..Point::new(47, 10)),
+ selection: Some(23..47),
})
.unwrap();
@@ -427,7 +451,7 @@ mod tests {
remote_url: "https://gitee.com/libkitten/zed.git",
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
path: "crates/zed/src/main.rs",
- selection: Some(Point::new(6, 1)..Point::new(6, 10)),
+ selection: Some(6..6),
})
.unwrap();
@@ -441,7 +465,7 @@ mod tests {
remote_url: "https://gitee.com/libkitten/zed.git",
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
path: "crates/zed/src/main.rs",
- selection: Some(Point::new(23, 1)..Point::new(47, 10)),
+ selection: Some(23..47),
})
.unwrap();
let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs#L24-48";
@@ -495,7 +519,7 @@ mod tests {
remote_url: "git@bitbucket.org:thorstenzed/testingrepo.git",
sha: "f00b4r",
path: "main.rs",
- selection: Some(Point::new(6, 1)..Point::new(6, 10)),
+ selection: Some(6..6),
})
.unwrap();
@@ -510,7 +534,7 @@ mod tests {
remote_url: "git@bitbucket.org:thorstenzed/testingrepo.git",
sha: "f00b4r",
path: "main.rs",
- selection: Some(Point::new(23, 1)..Point::new(47, 10)),
+ selection: Some(23..47),
})
.unwrap();
@@ -553,7 +577,7 @@ mod tests {
remote_url: "git@git.sr.ht:~rajveermalviya/zed",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
- selection: Some(Point::new(6, 1)..Point::new(6, 10)),
+ selection: Some(6..6),
})
.unwrap();
@@ -567,7 +591,7 @@ mod tests {
remote_url: "git@git.sr.ht:~rajveermalviya/zed",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
- selection: Some(Point::new(23, 1)..Point::new(47, 10)),
+ selection: Some(23..47),
})
.unwrap();
@@ -595,7 +619,7 @@ mod tests {
remote_url: "https://git.sr.ht/~rajveermalviya/zed",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/zed/src/main.rs",
- selection: Some(Point::new(6, 1)..Point::new(6, 10)),
+ selection: Some(6..6),
})
.unwrap();
@@ -609,7 +633,7 @@ mod tests {
remote_url: "https://git.sr.ht/~rajveermalviya/zed",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/zed/src/main.rs",
- selection: Some(Point::new(23, 1)..Point::new(47, 10)),
+ selection: Some(23..47),
})
.unwrap();
@@ -637,7 +661,7 @@ mod tests {
remote_url: "git@codeberg.org:rajveermalviya/zed.git",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
- selection: Some(Point::new(6, 1)..Point::new(6, 10)),
+ selection: Some(6..6),
})
.unwrap();
@@ -651,7 +675,7 @@ mod tests {
remote_url: "git@codeberg.org:rajveermalviya/zed.git",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
- selection: Some(Point::new(23, 1)..Point::new(47, 10)),
+ selection: Some(23..47),
})
.unwrap();
@@ -679,7 +703,7 @@ mod tests {
remote_url: "https://codeberg.org/rajveermalviya/zed.git",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/zed/src/main.rs",
- selection: Some(Point::new(6, 1)..Point::new(6, 10)),
+ selection: Some(6..6),
})
.unwrap();
@@ -693,7 +717,7 @@ mod tests {
remote_url: "https://codeberg.org/rajveermalviya/zed.git",
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/zed/src/main.rs",
- selection: Some(Point::new(23, 1)..Point::new(47, 10)),
+ selection: Some(23..47),
})
.unwrap();
@@ -0,0 +1,194 @@
+5c4f3c0ceaa0b2270c8f4fc8ee32b85c70810206 6 6 3
+author Mahdy M. Karam
+author-mail <64036912+mmkaram@users.noreply.github.com>
+author-time 1708621949
+author-tz -0800
+committer GitHub
+committer-mail <noreply@github.com>
+committer-time 1708621949
+committer-tz -0700
+summary Add option to either use system clipboard or vim clipboard (#7936)
+previous c6826a61a0a947acf09d65ada568c9c4e4494cb2 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+5c4f3c0ceaa0b2270c8f4fc8ee32b85c70810206 12 12 2
+previous c6826a61a0a947acf09d65ada568c9c4e4494cb2 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+5c4f3c0ceaa0b2270c8f4fc8ee32b85c70810206 18 18 1
+previous c6826a61a0a947acf09d65ada568c9c4e4494cb2 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+5c4f3c0ceaa0b2270c8f4fc8ee32b85c70810206 21 21 7
+previous c6826a61a0a947acf09d65ada568c9c4e4494cb2 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+5c4f3c0ceaa0b2270c8f4fc8ee32b85c70810206 31 31 1
+previous c6826a61a0a947acf09d65ada568c9c4e4494cb2 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+5c4f3c0ceaa0b2270c8f4fc8ee32b85c70810206 34 34 1
+previous c6826a61a0a947acf09d65ada568c9c4e4494cb2 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+5c4f3c0ceaa0b2270c8f4fc8ee32b85c70810206 86 86 16
+previous c6826a61a0a947acf09d65ada568c9c4e4494cb2 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+3635d2dcedd83f1b6702f33ca2673317f7fa4695 1 1 2
+author Conrad Irwin
+author-mail <conrad@zed.dev>
+author-time 1707520689
+author-tz -0700
+committer GitHub
+committer-mail <noreply@github.com>
+committer-time 1707520689
+committer-tz -0700
+summary Highlight selections on vim yank (#7638)
+previous efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+3635d2dcedd83f1b6702f33ca2673317f7fa4695 4 4 1
+previous efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+3635d2dcedd83f1b6702f33ca2673317f7fa4695 7 10 2
+previous efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+3635d2dcedd83f1b6702f33ca2673317f7fa4695 10 14 4
+previous efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+3635d2dcedd83f1b6702f33ca2673317f7fa4695 15 19 2
+previous efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+3635d2dcedd83f1b6702f33ca2673317f7fa4695 19 28 3
+previous efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+3635d2dcedd83f1b6702f33ca2673317f7fa4695 22 32 2
+previous efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+3635d2dcedd83f1b6702f33ca2673317f7fa4695 25 35 2
+previous efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+3635d2dcedd83f1b6702f33ca2673317f7fa4695 31 41 1
+previous efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+3635d2dcedd83f1b6702f33ca2673317f7fa4695 57 67 5
+previous efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+3635d2dcedd83f1b6702f33ca2673317f7fa4695 78 102 18
+previous efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+b65cf6d2d9576171edb407f5bbaa231c33af1f71 3 5 1
+author Max Brunsfeld
+author-mail <maxbrunsfeld@gmail.com>
+author-time 1705619094
+author-tz -0800
+committer Max Brunsfeld
+committer-mail <maxbrunsfeld@gmail.com>
+committer-time 1705619205
+committer-tz -0800
+summary Merge branch 'main' into language-api-docs
+previous 6457ccf9ece3b36a37e675783abee9748a443115 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+b65cf6d2d9576171edb407f5bbaa231c33af1f71 51 121 8
+previous 6457ccf9ece3b36a37e675783abee9748a443115 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+b02bd9bce1db3a68dcd606718fa02709020860af 29 61 1
+author Conrad Irwin
+author-mail <conrad@zed.dev>
+author-time 1694798044
+author-tz -0600
+committer Conrad Irwin
+committer-mail <conrad@zed.dev>
+committer-time 1694798044
+committer-tz -0600
+summary Fix Y on last line with no trailing new line
+previous 7c77baa7c17eea106330622e70513ea9389d50a1 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+b02bd9bce1db3a68dcd606718fa02709020860af 33 65 1
+previous 7c77baa7c17eea106330622e70513ea9389d50a1 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+e4794e3134b6449e36ed2771a8849046489cc252 13 45 1
+author Conrad Irwin
+author-mail <conrad@zed.dev>
+author-time 1692855942
+author-tz -0600
+committer Conrad Irwin
+committer-mail <conrad@zed.dev>
+committer-time 1692856812
+committer-tz -0600
+summary vim: Fix linewise copy of last line with no trailing newline
+previous 26c3312049a9c73bc3350528c1defd3820a7a8c7 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+e4794e3134b6449e36ed2771a8849046489cc252 21 53 8
+previous 26c3312049a9c73bc3350528c1defd3820a7a8c7 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+e4794e3134b6449e36ed2771a8849046489cc252 29 62 3
+previous 26c3312049a9c73bc3350528c1defd3820a7a8c7 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+e4794e3134b6449e36ed2771a8849046489cc252 33 66 1
+previous 26c3312049a9c73bc3350528c1defd3820a7a8c7 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+e4794e3134b6449e36ed2771a8849046489cc252 37 75 3
+previous 26c3312049a9c73bc3350528c1defd3820a7a8c7 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+33d7fe02ee560f6ed57d1425c43e60aef3b66e64 10 43 1
+author Conrad Irwin
+author-mail <conrad@zed.dev>
+author-time 1692644159
+author-tz -0600
+committer Conrad Irwin
+committer-mail <conrad@zed.dev>
+committer-time 1692732477
+committer-tz -0600
+summary Rewrite paste
+previous 31db5e4f62e8fca75aa0870f903ae044524c3580 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+33d7fe02ee560f6ed57d1425c43e60aef3b66e64 14 47 6
+previous 31db5e4f62e8fca75aa0870f903ae044524c3580 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+868c46062008bb0bcab2d41a38b4295996b9b958 20 81 1
+author Max Brunsfeld
+author-mail <maxbrunsfeld@gmail.com>
+author-time 1659072896
+author-tz -0700
+committer Max Brunsfeld
+committer-mail <maxbrunsfeld@gmail.com>
+committer-time 1659073230
+committer-tz -0700
+summary :art: Rename and simplify some autoindent stuff
+previous 7a26fa18c7fee3fe031b991e18b55fd8f9c4eb1b crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+e93c49f4f02b3edaddae6a6a4cc0ac433f242357 5 37 1
+author Kaylee Simmons
+author-mail <kay@the-simmons.net>
+author-time 1653424557
+author-tz -0700
+committer Kaylee Simmons
+committer-mail <kay@the-simmons.net>
+committer-time 1653609725
+committer-tz -0700
+summary Unify visual line_mode and non line_mode operators
+previous 11569a869a72f786a9798c53266e28c05c79f824 crates/vim/src/utils.rs
+filename crates/vim/src/utils.rs
+082036161fd3815c831ceedfd28ba15b0ed6eb9f 1 3 1
+author Kaylee Simmons
+author-mail <kay@the-simmons.net>
+author-time 1653007350
+author-tz -0700
+committer Kaylee Simmons
+committer-mail <kay@the-simmons.net>
+committer-time 1653609725
+committer-tz -0700
+summary Enable copy and paste in vim mode
+filename crates/vim/src/utils.rs
+082036161fd3815c831ceedfd28ba15b0ed6eb9f 4 9 1
+filename crates/vim/src/utils.rs
+082036161fd3815c831ceedfd28ba15b0ed6eb9f 7 38 3
+filename crates/vim/src/utils.rs
+082036161fd3815c831ceedfd28ba15b0ed6eb9f 10 42 1
+filename crates/vim/src/utils.rs
+082036161fd3815c831ceedfd28ba15b0ed6eb9f 11 44 1
+filename crates/vim/src/utils.rs
+082036161fd3815c831ceedfd28ba15b0ed6eb9f 14 46 1
+filename crates/vim/src/utils.rs
+082036161fd3815c831ceedfd28ba15b0ed6eb9f 15 72 3
+filename crates/vim/src/utils.rs
+082036161fd3815c831ceedfd28ba15b0ed6eb9f 18 78 3
+filename crates/vim/src/utils.rs
+082036161fd3815c831ceedfd28ba15b0ed6eb9f 21 82 4
+filename crates/vim/src/utils.rs
+082036161fd3815c831ceedfd28ba15b0ed6eb9f 26 120 1
+filename crates/vim/src/utils.rs
@@ -0,0 +1,64 @@
+0000000000000000000000000000000000000000 4 4 3
+author External file (--contents)
+author-mail <external.file>
+author-time 1710764166
+author-tz +0100
+committer External file (--contents)
+committer-mail <external.file>
+committer-time 1710764166
+committer-tz +0100
+summary Version of file_b.txt from file_b_memory.txt
+previous 4aaba34cb86b12f3a749dd6ddbf185de88de6527 file_b.txt
+filename file_b.txt
+0000000000000000000000000000000000000000 10 10 1
+previous 4aaba34cb86b12f3a749dd6ddbf185de88de6527 file_b.txt
+filename file_b.txt
+0000000000000000000000000000000000000000 15 15 2
+previous 4aaba34cb86b12f3a749dd6ddbf185de88de6527 file_b.txt
+filename file_b.txt
+4aaba34cb86b12f3a749dd6ddbf185de88de6527 4 7 2
+author Thorsten Ball
+author-mail <mrnugget@gmail.com>
+author-time 1710764113
+author-tz +0100
+committer Thorsten Ball
+committer-mail <mrnugget@gmail.com>
+committer-time 1710764113
+committer-tz +0100
+summary Another commit
+previous b2f6b0d982a9e7654bbb6d979c9ccb00b1d2e913 file_b.txt
+filename file_b.txt
+4aaba34cb86b12f3a749dd6ddbf185de88de6527 11 17 2
+previous b2f6b0d982a9e7654bbb6d979c9ccb00b1d2e913 file_b.txt
+filename file_b.txt
+b2f6b0d982a9e7654bbb6d979c9ccb00b1d2e913 6 9 1
+author Thorsten Ball
+author-mail <mrnugget@gmail.com>
+author-time 1710764087
+author-tz +0100
+committer Thorsten Ball
+committer-mail <mrnugget@gmail.com>
+committer-time 1710764087
+committer-tz +0100
+summary Another commit
+previous 7aef8a9a211ebc86824769c89f48e4f64aa6183f file_b.txt
+filename file_b.txt
+b2f6b0d982a9e7654bbb6d979c9ccb00b1d2e913 7 11 2
+previous 7aef8a9a211ebc86824769c89f48e4f64aa6183f file_b.txt
+filename file_b.txt
+e6d34e8fb494fe2b576d16037c61ba9d10722ba3 1 1 3
+author Thorsten Ball
+author-mail <mrnugget@gmail.com>
+author-time 1709299737
+author-tz +0100
+committer Thorsten Ball
+committer-mail <mrnugget@gmail.com>
+committer-time 1709299737
+committer-tz +0100
+summary Initial
+boundary
+filename file_b.txt
+e6d34e8fb494fe2b576d16037c61ba9d10722ba3 9 13 2
+filename file_b.txt
+e6d34e8fb494fe2b576d16037c61ba9d10722ba3 13 19 1
+filename file_b.txt
@@ -0,0 +1,70 @@
+0000000000000000000000000000000000000000 3 3 1
+author Not Committed Yet
+author-mail <not.committed.yet>
+author-time 1709895274
+author-tz +0100
+committer Not Committed Yet
+committer-mail <not.committed.yet>
+committer-time 1709895274
+committer-tz +0100
+summary Version of index.js from index.js
+previous a7037b4567dd171bfe563c761354ec9236c803b3 index.js
+filename index.js
+0000000000000000000000000000000000000000 7 7 2
+previous a7037b4567dd171bfe563c761354ec9236c803b3 index.js
+filename index.js
+c8d34ae30c87e59aaa5eb65f6c64d6206f525d7c 7 6 1
+author Thorsten Ball
+author-mail <mrnugget@example.com>
+author-time 1709808710
+author-tz +0100
+committer Thorsten Ball
+committer-mail <mrnugget@example.com>
+committer-time 1709808710
+committer-tz +0100
+summary Make a commit
+previous 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 index.js
+filename index.js
+6ad46b5257ba16d12c5ca9f0d4900320959df7f4 2 2 1
+author Joe Schmoe
+author-mail <joe.schmoe@example.com>
+author-time 1709741400
+author-tz +0100
+committer Joe Schmoe
+committer-mail <joe.schmoe@example.com>
+committer-time 1709741400
+committer-tz +0100
+summary Joe's cool commit
+previous 486c2409237a2c627230589e567024a96751d475 index.js
+filename index.js
+6ad46b5257ba16d12c5ca9f0d4900320959df7f4 3 4 1
+previous 486c2409237a2c627230589e567024a96751d475 index.js
+filename index.js
+6ad46b5257ba16d12c5ca9f0d4900320959df7f4 13 9 1
+previous 486c2409237a2c627230589e567024a96751d475 index.js
+filename index.js
+486c2409237a2c627230589e567024a96751d475 3 1 1
+author Thorsten Ball
+author-mail <mrnugget@example.com>
+author-time 1709129122
+author-tz +0100
+committer Thorsten Ball
+committer-mail <mrnugget@example.com>
+committer-time 1709129122
+committer-tz +0100
+summary Get to a state where eslint would change code and imports
+previous 504065e448b467e79920040f22153e9d2ea0fd6e index.js
+filename index.js
+504065e448b467e79920040f22153e9d2ea0fd6e 3 5 1
+author Thorsten Ball
+author-mail <mrnugget@example.com>
+author-time 1709128963
+author-tz +0100
+committer Thorsten Ball
+committer-mail <mrnugget@example.com>
+committer-time 1709128963
+committer-tz +0100
+summary Add some stuff
+filename index.js
+504065e448b467e79920040f22153e9d2ea0fd6e 21 10 1
+filename index.js
@@ -0,0 +1,781 @@
+[
+ {
+ "sha": "5c4f3c0ceaa0b2270c8f4fc8ee32b85c70810206",
+ "range": {
+ "start": 5,
+ "end": 8
+ },
+ "original_line_number": 6,
+ "author": "Mahdy M. Karam",
+ "author_mail": "<64036912+mmkaram@users.noreply.github.com>",
+ "author_time": 1708621949,
+ "author_tz": "-0800",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1708621949,
+ "committer_tz": "-0700",
+ "summary": "Add option to either use system clipboard or vim clipboard (#7936)",
+ "previous": "c6826a61a0a947acf09d65ada568c9c4e4494cb2 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "5c4f3c0ceaa0b2270c8f4fc8ee32b85c70810206",
+ "range": {
+ "start": 11,
+ "end": 13
+ },
+ "original_line_number": 12,
+ "author": "Mahdy M. Karam",
+ "author_mail": "<64036912+mmkaram@users.noreply.github.com>",
+ "author_time": 1708621949,
+ "author_tz": "-0800",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1708621949,
+ "committer_tz": "-0700",
+ "summary": "Add option to either use system clipboard or vim clipboard (#7936)",
+ "previous": "c6826a61a0a947acf09d65ada568c9c4e4494cb2 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "5c4f3c0ceaa0b2270c8f4fc8ee32b85c70810206",
+ "range": {
+ "start": 17,
+ "end": 18
+ },
+ "original_line_number": 18,
+ "author": "Mahdy M. Karam",
+ "author_mail": "<64036912+mmkaram@users.noreply.github.com>",
+ "author_time": 1708621949,
+ "author_tz": "-0800",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1708621949,
+ "committer_tz": "-0700",
+ "summary": "Add option to either use system clipboard or vim clipboard (#7936)",
+ "previous": "c6826a61a0a947acf09d65ada568c9c4e4494cb2 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "5c4f3c0ceaa0b2270c8f4fc8ee32b85c70810206",
+ "range": {
+ "start": 20,
+ "end": 27
+ },
+ "original_line_number": 21,
+ "author": "Mahdy M. Karam",
+ "author_mail": "<64036912+mmkaram@users.noreply.github.com>",
+ "author_time": 1708621949,
+ "author_tz": "-0800",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1708621949,
+ "committer_tz": "-0700",
+ "summary": "Add option to either use system clipboard or vim clipboard (#7936)",
+ "previous": "c6826a61a0a947acf09d65ada568c9c4e4494cb2 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "5c4f3c0ceaa0b2270c8f4fc8ee32b85c70810206",
+ "range": {
+ "start": 30,
+ "end": 31
+ },
+ "original_line_number": 31,
+ "author": "Mahdy M. Karam",
+ "author_mail": "<64036912+mmkaram@users.noreply.github.com>",
+ "author_time": 1708621949,
+ "author_tz": "-0800",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1708621949,
+ "committer_tz": "-0700",
+ "summary": "Add option to either use system clipboard or vim clipboard (#7936)",
+ "previous": "c6826a61a0a947acf09d65ada568c9c4e4494cb2 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "5c4f3c0ceaa0b2270c8f4fc8ee32b85c70810206",
+ "range": {
+ "start": 33,
+ "end": 34
+ },
+ "original_line_number": 34,
+ "author": "Mahdy M. Karam",
+ "author_mail": "<64036912+mmkaram@users.noreply.github.com>",
+ "author_time": 1708621949,
+ "author_tz": "-0800",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1708621949,
+ "committer_tz": "-0700",
+ "summary": "Add option to either use system clipboard or vim clipboard (#7936)",
+ "previous": "c6826a61a0a947acf09d65ada568c9c4e4494cb2 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "5c4f3c0ceaa0b2270c8f4fc8ee32b85c70810206",
+ "range": {
+ "start": 85,
+ "end": 101
+ },
+ "original_line_number": 86,
+ "author": "Mahdy M. Karam",
+ "author_mail": "<64036912+mmkaram@users.noreply.github.com>",
+ "author_time": 1708621949,
+ "author_tz": "-0800",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1708621949,
+ "committer_tz": "-0700",
+ "summary": "Add option to either use system clipboard or vim clipboard (#7936)",
+ "previous": "c6826a61a0a947acf09d65ada568c9c4e4494cb2 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "3635d2dcedd83f1b6702f33ca2673317f7fa4695",
+ "range": {
+ "start": 0,
+ "end": 2
+ },
+ "original_line_number": 1,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1707520689,
+ "author_tz": "-0700",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1707520689,
+ "committer_tz": "-0700",
+ "summary": "Highlight selections on vim yank (#7638)",
+ "previous": "efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "3635d2dcedd83f1b6702f33ca2673317f7fa4695",
+ "range": {
+ "start": 3,
+ "end": 4
+ },
+ "original_line_number": 4,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1707520689,
+ "author_tz": "-0700",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1707520689,
+ "committer_tz": "-0700",
+ "summary": "Highlight selections on vim yank (#7638)",
+ "previous": "efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "3635d2dcedd83f1b6702f33ca2673317f7fa4695",
+ "range": {
+ "start": 9,
+ "end": 11
+ },
+ "original_line_number": 7,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1707520689,
+ "author_tz": "-0700",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1707520689,
+ "committer_tz": "-0700",
+ "summary": "Highlight selections on vim yank (#7638)",
+ "previous": "efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "3635d2dcedd83f1b6702f33ca2673317f7fa4695",
+ "range": {
+ "start": 13,
+ "end": 17
+ },
+ "original_line_number": 10,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1707520689,
+ "author_tz": "-0700",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1707520689,
+ "committer_tz": "-0700",
+ "summary": "Highlight selections on vim yank (#7638)",
+ "previous": "efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "3635d2dcedd83f1b6702f33ca2673317f7fa4695",
+ "range": {
+ "start": 18,
+ "end": 20
+ },
+ "original_line_number": 15,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1707520689,
+ "author_tz": "-0700",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1707520689,
+ "committer_tz": "-0700",
+ "summary": "Highlight selections on vim yank (#7638)",
+ "previous": "efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "3635d2dcedd83f1b6702f33ca2673317f7fa4695",
+ "range": {
+ "start": 27,
+ "end": 30
+ },
+ "original_line_number": 19,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1707520689,
+ "author_tz": "-0700",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1707520689,
+ "committer_tz": "-0700",
+ "summary": "Highlight selections on vim yank (#7638)",
+ "previous": "efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "3635d2dcedd83f1b6702f33ca2673317f7fa4695",
+ "range": {
+ "start": 31,
+ "end": 33
+ },
+ "original_line_number": 22,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1707520689,
+ "author_tz": "-0700",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1707520689,
+ "committer_tz": "-0700",
+ "summary": "Highlight selections on vim yank (#7638)",
+ "previous": "efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "3635d2dcedd83f1b6702f33ca2673317f7fa4695",
+ "range": {
+ "start": 34,
+ "end": 36
+ },
+ "original_line_number": 25,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1707520689,
+ "author_tz": "-0700",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1707520689,
+ "committer_tz": "-0700",
+ "summary": "Highlight selections on vim yank (#7638)",
+ "previous": "efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "3635d2dcedd83f1b6702f33ca2673317f7fa4695",
+ "range": {
+ "start": 40,
+ "end": 41
+ },
+ "original_line_number": 31,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1707520689,
+ "author_tz": "-0700",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1707520689,
+ "committer_tz": "-0700",
+ "summary": "Highlight selections on vim yank (#7638)",
+ "previous": "efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "3635d2dcedd83f1b6702f33ca2673317f7fa4695",
+ "range": {
+ "start": 66,
+ "end": 71
+ },
+ "original_line_number": 57,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1707520689,
+ "author_tz": "-0700",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1707520689,
+ "committer_tz": "-0700",
+ "summary": "Highlight selections on vim yank (#7638)",
+ "previous": "efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "3635d2dcedd83f1b6702f33ca2673317f7fa4695",
+ "range": {
+ "start": 101,
+ "end": 119
+ },
+ "original_line_number": 78,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1707520689,
+ "author_tz": "-0700",
+ "committer": "GitHub",
+ "committer_mail": "<noreply@github.com>",
+ "committer_time": 1707520689,
+ "committer_tz": "-0700",
+ "summary": "Highlight selections on vim yank (#7638)",
+ "previous": "efe23ebfcdd653b13be79132b1e2925bcd7bde45 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "b65cf6d2d9576171edb407f5bbaa231c33af1f71",
+ "range": {
+ "start": 4,
+ "end": 5
+ },
+ "original_line_number": 3,
+ "author": "Max Brunsfeld",
+ "author_mail": "<maxbrunsfeld@gmail.com>",
+ "author_time": 1705619094,
+ "author_tz": "-0800",
+ "committer": "Max Brunsfeld",
+ "committer_mail": "<maxbrunsfeld@gmail.com>",
+ "committer_time": 1705619205,
+ "committer_tz": "-0800",
+ "summary": "Merge branch 'main' into language-api-docs",
+ "previous": "6457ccf9ece3b36a37e675783abee9748a443115 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "b65cf6d2d9576171edb407f5bbaa231c33af1f71",
+ "range": {
+ "start": 120,
+ "end": 128
+ },
+ "original_line_number": 51,
+ "author": "Max Brunsfeld",
+ "author_mail": "<maxbrunsfeld@gmail.com>",
+ "author_time": 1705619094,
+ "author_tz": "-0800",
+ "committer": "Max Brunsfeld",
+ "committer_mail": "<maxbrunsfeld@gmail.com>",
+ "committer_time": 1705619205,
+ "committer_tz": "-0800",
+ "summary": "Merge branch 'main' into language-api-docs",
+ "previous": "6457ccf9ece3b36a37e675783abee9748a443115 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "b02bd9bce1db3a68dcd606718fa02709020860af",
+ "range": {
+ "start": 60,
+ "end": 61
+ },
+ "original_line_number": 29,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1694798044,
+ "author_tz": "-0600",
+ "committer": "Conrad Irwin",
+ "committer_mail": "<conrad@zed.dev>",
+ "committer_time": 1694798044,
+ "committer_tz": "-0600",
+ "summary": "Fix Y on last line with no trailing new line",
+ "previous": "7c77baa7c17eea106330622e70513ea9389d50a1 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "b02bd9bce1db3a68dcd606718fa02709020860af",
+ "range": {
+ "start": 64,
+ "end": 65
+ },
+ "original_line_number": 33,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1694798044,
+ "author_tz": "-0600",
+ "committer": "Conrad Irwin",
+ "committer_mail": "<conrad@zed.dev>",
+ "committer_time": 1694798044,
+ "committer_tz": "-0600",
+ "summary": "Fix Y on last line with no trailing new line",
+ "previous": "7c77baa7c17eea106330622e70513ea9389d50a1 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "e4794e3134b6449e36ed2771a8849046489cc252",
+ "range": {
+ "start": 44,
+ "end": 45
+ },
+ "original_line_number": 13,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1692855942,
+ "author_tz": "-0600",
+ "committer": "Conrad Irwin",
+ "committer_mail": "<conrad@zed.dev>",
+ "committer_time": 1692856812,
+ "committer_tz": "-0600",
+ "summary": "vim: Fix linewise copy of last line with no trailing newline",
+ "previous": "26c3312049a9c73bc3350528c1defd3820a7a8c7 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "e4794e3134b6449e36ed2771a8849046489cc252",
+ "range": {
+ "start": 52,
+ "end": 60
+ },
+ "original_line_number": 21,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1692855942,
+ "author_tz": "-0600",
+ "committer": "Conrad Irwin",
+ "committer_mail": "<conrad@zed.dev>",
+ "committer_time": 1692856812,
+ "committer_tz": "-0600",
+ "summary": "vim: Fix linewise copy of last line with no trailing newline",
+ "previous": "26c3312049a9c73bc3350528c1defd3820a7a8c7 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "e4794e3134b6449e36ed2771a8849046489cc252",
+ "range": {
+ "start": 61,
+ "end": 64
+ },
+ "original_line_number": 29,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1692855942,
+ "author_tz": "-0600",
+ "committer": "Conrad Irwin",
+ "committer_mail": "<conrad@zed.dev>",
+ "committer_time": 1692856812,
+ "committer_tz": "-0600",
+ "summary": "vim: Fix linewise copy of last line with no trailing newline",
+ "previous": "26c3312049a9c73bc3350528c1defd3820a7a8c7 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "e4794e3134b6449e36ed2771a8849046489cc252",
+ "range": {
+ "start": 65,
+ "end": 66
+ },
+ "original_line_number": 33,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1692855942,
+ "author_tz": "-0600",
+ "committer": "Conrad Irwin",
+ "committer_mail": "<conrad@zed.dev>",
+ "committer_time": 1692856812,
+ "committer_tz": "-0600",
+ "summary": "vim: Fix linewise copy of last line with no trailing newline",
+ "previous": "26c3312049a9c73bc3350528c1defd3820a7a8c7 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "e4794e3134b6449e36ed2771a8849046489cc252",
+ "range": {
+ "start": 74,
+ "end": 77
+ },
+ "original_line_number": 37,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1692855942,
+ "author_tz": "-0600",
+ "committer": "Conrad Irwin",
+ "committer_mail": "<conrad@zed.dev>",
+ "committer_time": 1692856812,
+ "committer_tz": "-0600",
+ "summary": "vim: Fix linewise copy of last line with no trailing newline",
+ "previous": "26c3312049a9c73bc3350528c1defd3820a7a8c7 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "33d7fe02ee560f6ed57d1425c43e60aef3b66e64",
+ "range": {
+ "start": 42,
+ "end": 43
+ },
+ "original_line_number": 10,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1692644159,
+ "author_tz": "-0600",
+ "committer": "Conrad Irwin",
+ "committer_mail": "<conrad@zed.dev>",
+ "committer_time": 1692732477,
+ "committer_tz": "-0600",
+ "summary": "Rewrite paste",
+ "previous": "31db5e4f62e8fca75aa0870f903ae044524c3580 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "33d7fe02ee560f6ed57d1425c43e60aef3b66e64",
+ "range": {
+ "start": 46,
+ "end": 52
+ },
+ "original_line_number": 14,
+ "author": "Conrad Irwin",
+ "author_mail": "<conrad@zed.dev>",
+ "author_time": 1692644159,
+ "author_tz": "-0600",
+ "committer": "Conrad Irwin",
+ "committer_mail": "<conrad@zed.dev>",
+ "committer_time": 1692732477,
+ "committer_tz": "-0600",
+ "summary": "Rewrite paste",
+ "previous": "31db5e4f62e8fca75aa0870f903ae044524c3580 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "868c46062008bb0bcab2d41a38b4295996b9b958",
+ "range": {
+ "start": 80,
+ "end": 81
+ },
+ "original_line_number": 20,
+ "author": "Max Brunsfeld",
+ "author_mail": "<maxbrunsfeld@gmail.com>",
+ "author_time": 1659072896,
+ "author_tz": "-0700",
+ "committer": "Max Brunsfeld",
+ "committer_mail": "<maxbrunsfeld@gmail.com>",
+ "committer_time": 1659073230,
+ "committer_tz": "-0700",
+ "summary": ":art: Rename and simplify some autoindent stuff",
+ "previous": "7a26fa18c7fee3fe031b991e18b55fd8f9c4eb1b crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "e93c49f4f02b3edaddae6a6a4cc0ac433f242357",
+ "range": {
+ "start": 36,
+ "end": 37
+ },
+ "original_line_number": 5,
+ "author": "Kaylee Simmons",
+ "author_mail": "<kay@the-simmons.net>",
+ "author_time": 1653424557,
+ "author_tz": "-0700",
+ "committer": "Kaylee Simmons",
+ "committer_mail": "<kay@the-simmons.net>",
+ "committer_time": 1653609725,
+ "committer_tz": "-0700",
+ "summary": "Unify visual line_mode and non line_mode operators",
+ "previous": "11569a869a72f786a9798c53266e28c05c79f824 crates/vim/src/utils.rs",
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "082036161fd3815c831ceedfd28ba15b0ed6eb9f",
+ "range": {
+ "start": 2,
+ "end": 3
+ },
+ "original_line_number": 1,
+ "author": "Kaylee Simmons",
+ "author_mail": "<kay@the-simmons.net>",
+ "author_time": 1653007350,
+ "author_tz": "-0700",
+ "committer": "Kaylee Simmons",
+ "committer_mail": "<kay@the-simmons.net>",
+ "committer_time": 1653609725,
+ "committer_tz": "-0700",
+ "summary": "Enable copy and paste in vim mode",
+ "previous": null,
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "082036161fd3815c831ceedfd28ba15b0ed6eb9f",
+ "range": {
+ "start": 8,
+ "end": 9
+ },
+ "original_line_number": 4,
+ "author": "Kaylee Simmons",
+ "author_mail": "<kay@the-simmons.net>",
+ "author_time": 1653007350,
+ "author_tz": "-0700",
+ "committer": "Kaylee Simmons",
+ "committer_mail": "<kay@the-simmons.net>",
+ "committer_time": 1653609725,
+ "committer_tz": "-0700",
+ "summary": "Enable copy and paste in vim mode",
+ "previous": null,
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "082036161fd3815c831ceedfd28ba15b0ed6eb9f",
+ "range": {
+ "start": 37,
+ "end": 40
+ },
+ "original_line_number": 7,
+ "author": "Kaylee Simmons",
+ "author_mail": "<kay@the-simmons.net>",
+ "author_time": 1653007350,
+ "author_tz": "-0700",
+ "committer": "Kaylee Simmons",
+ "committer_mail": "<kay@the-simmons.net>",
+ "committer_time": 1653609725,
+ "committer_tz": "-0700",
+ "summary": "Enable copy and paste in vim mode",
+ "previous": null,
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "082036161fd3815c831ceedfd28ba15b0ed6eb9f",
+ "range": {
+ "start": 41,
+ "end": 42
+ },
+ "original_line_number": 10,
+ "author": "Kaylee Simmons",
+ "author_mail": "<kay@the-simmons.net>",
+ "author_time": 1653007350,
+ "author_tz": "-0700",
+ "committer": "Kaylee Simmons",
+ "committer_mail": "<kay@the-simmons.net>",
+ "committer_time": 1653609725,
+ "committer_tz": "-0700",
+ "summary": "Enable copy and paste in vim mode",
+ "previous": null,
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "082036161fd3815c831ceedfd28ba15b0ed6eb9f",
+ "range": {
+ "start": 43,
+ "end": 44
+ },
+ "original_line_number": 11,
+ "author": "Kaylee Simmons",
+ "author_mail": "<kay@the-simmons.net>",
+ "author_time": 1653007350,
+ "author_tz": "-0700",
+ "committer": "Kaylee Simmons",
+ "committer_mail": "<kay@the-simmons.net>",
+ "committer_time": 1653609725,
+ "committer_tz": "-0700",
+ "summary": "Enable copy and paste in vim mode",
+ "previous": null,
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "082036161fd3815c831ceedfd28ba15b0ed6eb9f",
+ "range": {
+ "start": 45,
+ "end": 46
+ },
+ "original_line_number": 14,
+ "author": "Kaylee Simmons",
+ "author_mail": "<kay@the-simmons.net>",
+ "author_time": 1653007350,
+ "author_tz": "-0700",
+ "committer": "Kaylee Simmons",
+ "committer_mail": "<kay@the-simmons.net>",
+ "committer_time": 1653609725,
+ "committer_tz": "-0700",
+ "summary": "Enable copy and paste in vim mode",
+ "previous": null,
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "082036161fd3815c831ceedfd28ba15b0ed6eb9f",
+ "range": {
+ "start": 71,
+ "end": 74
+ },
+ "original_line_number": 15,
+ "author": "Kaylee Simmons",
+ "author_mail": "<kay@the-simmons.net>",
+ "author_time": 1653007350,
+ "author_tz": "-0700",
+ "committer": "Kaylee Simmons",
+ "committer_mail": "<kay@the-simmons.net>",
+ "committer_time": 1653609725,
+ "committer_tz": "-0700",
+ "summary": "Enable copy and paste in vim mode",
+ "previous": null,
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "082036161fd3815c831ceedfd28ba15b0ed6eb9f",
+ "range": {
+ "start": 77,
+ "end": 80
+ },
+ "original_line_number": 18,
+ "author": "Kaylee Simmons",
+ "author_mail": "<kay@the-simmons.net>",
+ "author_time": 1653007350,
+ "author_tz": "-0700",
+ "committer": "Kaylee Simmons",
+ "committer_mail": "<kay@the-simmons.net>",
+ "committer_time": 1653609725,
+ "committer_tz": "-0700",
+ "summary": "Enable copy and paste in vim mode",
+ "previous": null,
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "082036161fd3815c831ceedfd28ba15b0ed6eb9f",
+ "range": {
+ "start": 81,
+ "end": 85
+ },
+ "original_line_number": 21,
+ "author": "Kaylee Simmons",
+ "author_mail": "<kay@the-simmons.net>",
+ "author_time": 1653007350,
+ "author_tz": "-0700",
+ "committer": "Kaylee Simmons",
+ "committer_mail": "<kay@the-simmons.net>",
+ "committer_time": 1653609725,
+ "committer_tz": "-0700",
+ "summary": "Enable copy and paste in vim mode",
+ "previous": null,
+ "filename": "crates/vim/src/utils.rs"
+ },
+ {
+ "sha": "082036161fd3815c831ceedfd28ba15b0ed6eb9f",
+ "range": {
+ "start": 119,
+ "end": 120
+ },
+ "original_line_number": 26,
+ "author": "Kaylee Simmons",
+ "author_mail": "<kay@the-simmons.net>",
+ "author_time": 1653007350,
+ "author_tz": "-0700",
+ "committer": "Kaylee Simmons",
+ "committer_mail": "<kay@the-simmons.net>",
+ "committer_time": 1653609725,
+ "committer_tz": "-0700",
+ "summary": "Enable copy and paste in vim mode",
+ "previous": null,
+ "filename": "crates/vim/src/utils.rs"
+ }
+]
@@ -0,0 +1,135 @@
+[
+ {
+ "sha": "4aaba34cb86b12f3a749dd6ddbf185de88de6527",
+ "range": {
+ "start": 6,
+ "end": 8
+ },
+ "original_line_number": 4,
+ "author": "Thorsten Ball",
+ "author_mail": "<mrnugget@gmail.com>",
+ "author_time": 1710764113,
+ "author_tz": "+0100",
+ "committer": "Thorsten Ball",
+ "committer_mail": "<mrnugget@gmail.com>",
+ "committer_time": 1710764113,
+ "committer_tz": "+0100",
+ "summary": "Another commit",
+ "previous": "b2f6b0d982a9e7654bbb6d979c9ccb00b1d2e913 file_b.txt",
+ "filename": "file_b.txt"
+ },
+ {
+ "sha": "4aaba34cb86b12f3a749dd6ddbf185de88de6527",
+ "range": {
+ "start": 16,
+ "end": 18
+ },
+ "original_line_number": 11,
+ "author": "Thorsten Ball",
+ "author_mail": "<mrnugget@gmail.com>",
+ "author_time": 1710764113,
+ "author_tz": "+0100",
+ "committer": "Thorsten Ball",
+ "committer_mail": "<mrnugget@gmail.com>",
+ "committer_time": 1710764113,
+ "committer_tz": "+0100",
+ "summary": "Another commit",
+ "previous": "b2f6b0d982a9e7654bbb6d979c9ccb00b1d2e913 file_b.txt",
+ "filename": "file_b.txt"
+ },
+ {
+ "sha": "b2f6b0d982a9e7654bbb6d979c9ccb00b1d2e913",
+ "range": {
+ "start": 8,
+ "end": 9
+ },
+ "original_line_number": 6,
+ "author": "Thorsten Ball",
+ "author_mail": "<mrnugget@gmail.com>",
+ "author_time": 1710764087,
+ "author_tz": "+0100",
+ "committer": "Thorsten Ball",
+ "committer_mail": "<mrnugget@gmail.com>",
+ "committer_time": 1710764087,
+ "committer_tz": "+0100",
+ "summary": "Another commit",
+ "previous": "7aef8a9a211ebc86824769c89f48e4f64aa6183f file_b.txt",
+ "filename": "file_b.txt"
+ },
+ {
+ "sha": "b2f6b0d982a9e7654bbb6d979c9ccb00b1d2e913",
+ "range": {
+ "start": 10,
+ "end": 12
+ },
+ "original_line_number": 7,
+ "author": "Thorsten Ball",
+ "author_mail": "<mrnugget@gmail.com>",
+ "author_time": 1710764087,
+ "author_tz": "+0100",
+ "committer": "Thorsten Ball",
+ "committer_mail": "<mrnugget@gmail.com>",
+ "committer_time": 1710764087,
+ "committer_tz": "+0100",
+ "summary": "Another commit",
+ "previous": "7aef8a9a211ebc86824769c89f48e4f64aa6183f file_b.txt",
+ "filename": "file_b.txt"
+ },
+ {
+ "sha": "e6d34e8fb494fe2b576d16037c61ba9d10722ba3",
+ "range": {
+ "start": 0,
+ "end": 3
+ },
+ "original_line_number": 1,
+ "author": "Thorsten Ball",
+ "author_mail": "<mrnugget@gmail.com>",
+ "author_time": 1709299737,
+ "author_tz": "+0100",
+ "committer": "Thorsten Ball",
+ "committer_mail": "<mrnugget@gmail.com>",
+ "committer_time": 1709299737,
+ "committer_tz": "+0100",
+ "summary": "Initial",
+ "previous": null,
+ "filename": "file_b.txt"
+ },
+ {
+ "sha": "e6d34e8fb494fe2b576d16037c61ba9d10722ba3",
+ "range": {
+ "start": 12,
+ "end": 14
+ },
+ "original_line_number": 9,
+ "author": "Thorsten Ball",
+ "author_mail": "<mrnugget@gmail.com>",
+ "author_time": 1709299737,
+ "author_tz": "+0100",
+ "committer": "Thorsten Ball",
+ "committer_mail": "<mrnugget@gmail.com>",
+ "committer_time": 1709299737,
+ "committer_tz": "+0100",
+ "summary": "Initial",
+ "previous": null,
+ "filename": "file_b.txt"
+ },
+ {
+ "sha": "e6d34e8fb494fe2b576d16037c61ba9d10722ba3",
+ "range": {
+ "start": 18,
+ "end": 19
+ },
+ "original_line_number": 13,
+ "author": "Thorsten Ball",
+ "author_mail": "<mrnugget@gmail.com>",
+ "author_time": 1709299737,
+ "author_tz": "+0100",
+ "committer": "Thorsten Ball",
+ "committer_mail": "<mrnugget@gmail.com>",
+ "committer_time": 1709299737,
+ "committer_tz": "+0100",
+ "summary": "Initial",
+ "previous": null,
+ "filename": "file_b.txt"
+ }
+]
@@ -0,0 +1,135 @@
+[
+ {
+ "sha": "c8d34ae30c87e59aaa5eb65f6c64d6206f525d7c",
+ "range": {
+ "start": 5,
+ "end": 6
+ },
+ "original_line_number": 7,
+ "author": "Thorsten Ball",
+ "author_mail": "<mrnugget@example.com>",
+ "author_time": 1709808710,
+ "author_tz": "+0100",
+ "committer": "Thorsten Ball",
+ "committer_mail": "<mrnugget@example.com>",
+ "committer_time": 1709808710,
+ "committer_tz": "+0100",
+ "summary": "Make a commit",
+ "previous": "6ad46b5257ba16d12c5ca9f0d4900320959df7f4 index.js",
+ "filename": "index.js"
+ },
+ {
+ "sha": "6ad46b5257ba16d12c5ca9f0d4900320959df7f4",
+ "range": {
+ "start": 1,
+ "end": 2
+ },
+ "original_line_number": 2,
+ "author": "Joe Schmoe",
+ "author_mail": "<joe.schmoe@example.com>",
+ "author_time": 1709741400,
+ "author_tz": "+0100",
+ "committer": "Joe Schmoe",
+ "committer_mail": "<joe.schmoe@example.com>",
+ "committer_time": 1709741400,
+ "committer_tz": "+0100",
+ "summary": "Joe's cool commit",
+ "previous": "486c2409237a2c627230589e567024a96751d475 index.js",
+ "filename": "index.js"
+ },
+ {
+ "sha": "6ad46b5257ba16d12c5ca9f0d4900320959df7f4",
+ "range": {
+ "start": 3,
+ "end": 4
+ },
+ "original_line_number": 3,
+ "author": "Joe Schmoe",
+ "author_mail": "<joe.schmoe@example.com>",
+ "author_time": 1709741400,
+ "author_tz": "+0100",
+ "committer": "Joe Schmoe",
+ "committer_mail": "<joe.schmoe@example.com>",
+ "committer_time": 1709741400,
+ "committer_tz": "+0100",
+ "summary": "Joe's cool commit",
+ "previous": "486c2409237a2c627230589e567024a96751d475 index.js",
+ "filename": "index.js"
+ },
+ {
+ "sha": "6ad46b5257ba16d12c5ca9f0d4900320959df7f4",
+ "range": {
+ "start": 8,
+ "end": 9
+ },
+ "original_line_number": 13,
+ "author": "Joe Schmoe",
+ "author_mail": "<joe.schmoe@example.com>",
+ "author_time": 1709741400,
+ "author_tz": "+0100",
+ "committer": "Joe Schmoe",
+ "committer_mail": "<joe.schmoe@example.com>",
+ "committer_time": 1709741400,
+ "committer_tz": "+0100",
+ "summary": "Joe's cool commit",
+ "previous": "486c2409237a2c627230589e567024a96751d475 index.js",
+ "filename": "index.js"
+ },
+ {
+ "sha": "486c2409237a2c627230589e567024a96751d475",
+ "range": {
+ "start": 0,
+ "end": 1
+ },
+ "original_line_number": 3,
+ "author": "Thorsten Ball",
+ "author_mail": "<mrnugget@example.com>",
+ "author_time": 1709129122,
+ "author_tz": "+0100",
+ "committer": "Thorsten Ball",
+ "committer_mail": "<mrnugget@example.com>",
+ "committer_time": 1709129122,
+ "committer_tz": "+0100",
+ "summary": "Get to a state where eslint would change code and imports",
+ "previous": "504065e448b467e79920040f22153e9d2ea0fd6e index.js",
+ "filename": "index.js"
+ },
+ {
+ "sha": "504065e448b467e79920040f22153e9d2ea0fd6e",
+ "range": {
+ "start": 4,
+ "end": 5
+ },
+ "original_line_number": 3,
+ "author": "Thorsten Ball",
+ "author_mail": "<mrnugget@example.com>",
+ "author_time": 1709128963,
+ "author_tz": "+0100",
+ "committer": "Thorsten Ball",
+ "committer_mail": "<mrnugget@example.com>",
+ "committer_time": 1709128963,
+ "committer_tz": "+0100",
+ "summary": "Add some stuff",
+ "previous": null,
+ "filename": "index.js"
+ },
+ {
+ "sha": "504065e448b467e79920040f22153e9d2ea0fd6e",
+ "range": {
+ "start": 9,
+ "end": 10
+ },
+ "original_line_number": 21,
+ "author": "Thorsten Ball",
+ "author_mail": "<mrnugget@example.com>",
+ "author_time": 1709128963,
+ "author_tz": "+0100",
+ "committer": "Thorsten Ball",
+ "committer_mail": "<mrnugget@example.com>",
+ "committer_time": 1709128963,
+ "committer_tz": "+0100",
+ "summary": "Add some stuff",
+ "previous": null,
+ "filename": "index.js"
+ }
+]
@@ -189,6 +189,11 @@ impl App {
pub fn text_system(&self) -> Arc<TextSystem> {
self.0.borrow().text_system.clone()
}
+
+ /// Returns the file URL of the executable with the specified name in the application bundle
+ pub fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
+ self.0.borrow().path_for_auxiliary_executable(name)
+ }
}
type Handler = Box<dyn FnMut(&mut AppContext) -> bool + 'static>;
@@ -34,6 +34,7 @@ copilot.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
+git.workspace = true
globset.workspace = true
gpui.workspace = true
itertools.workspace = true
@@ -30,6 +30,7 @@ use futures::{
stream::FuturesUnordered,
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
};
+use git::blame::Blame;
use globset::{Glob, GlobSet, GlobSetBuilder};
use gpui::{
AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, BorrowAppContext, Context, Entity,
@@ -83,7 +84,7 @@ use std::{
ops::Range,
path::{self, Component, Path, PathBuf},
process::Stdio,
- str,
+ str::{self, FromStr},
sync::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
@@ -95,7 +96,7 @@ use terminals::Terminals;
use text::{Anchor, BufferId, RopeFingerprint};
use util::{
debug_panic, defer,
- http::HttpClient,
+ http::{HttpClient, Url},
maybe, merge_json_value_into,
paths::{
LOCAL_SETTINGS_RELATIVE_PATH, LOCAL_TASKS_RELATIVE_PATH, LOCAL_VSCODE_TASKS_RELATIVE_PATH,
@@ -304,6 +305,7 @@ pub enum Event {
WorktreeAdded,
WorktreeRemoved(WorktreeId),
WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
+ WorktreeUpdatedGitRepositories,
DiskBasedDiagnosticsStarted {
language_server_id: LanguageServerId,
},
@@ -593,6 +595,7 @@ impl Project {
client.add_model_request_handler(Self::handle_save_buffer);
client.add_model_message_handler(Self::handle_update_diff_base);
client.add_model_request_handler(Self::handle_lsp_command::<lsp_ext_command::ExpandMacro>);
+ client.add_model_request_handler(Self::handle_blame_buffer);
}
pub fn local(
@@ -6746,8 +6749,13 @@ impl Project {
}
worktree::Event::UpdatedGitRepositories(updated_repos) => {
if is_local {
- this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
+ this.update_local_worktree_buffers_git_repos(
+ worktree.clone(),
+ updated_repos,
+ cx,
+ )
}
+ cx.emit(Event::WorktreeUpdatedGitRepositories);
}
}
})
@@ -7036,9 +7044,10 @@ impl Project {
.filter_map(|(buffer, path)| {
let (work_directory, repo) =
snapshot.repository_and_work_directory_for_path(&path)?;
- let repo = snapshot.get_local_repo(&repo)?;
+ let repo_entry = snapshot.get_local_repo(&repo)?;
let relative_path = path.strip_prefix(&work_directory).ok()?;
- let base_text = repo.load_index_text(relative_path);
+ let base_text = repo_entry.repo().lock().load_index_text(relative_path);
+
Some((buffer, base_text))
})
.collect::<Vec<_>>()
@@ -7315,6 +7324,19 @@ impl Project {
})
}
+ pub fn get_workspace_root(
+ &self,
+ project_path: &ProjectPath,
+ cx: &AppContext,
+ ) -> Option<PathBuf> {
+ Some(
+ self.worktree_for_id(project_path.worktree_id, cx)?
+ .read(cx)
+ .abs_path()
+ .to_path_buf(),
+ )
+ }
+
pub fn get_repo(
&self,
project_path: &ProjectPath,
@@ -7327,8 +7349,107 @@ impl Project {
.local_git_repo(&project_path.path)
}
+ pub fn blame_buffer(
+ &self,
+ buffer: &Model<Buffer>,
+ version: Option<clock::Global>,
+ cx: &AppContext,
+ ) -> Task<Result<Blame>> {
+ if self.is_local() {
+ let blame_params = maybe!({
+ let buffer = buffer.read(cx);
+ let buffer_project_path = buffer
+ .project_path(cx)
+ .context("failed to get buffer project path")?;
+
+ let worktree = self
+ .worktree_for_id(buffer_project_path.worktree_id, cx)
+ .context("failed to get worktree")?
+ .read(cx)
+ .as_local()
+ .context("worktree was not local")?
+ .snapshot();
+ let (work_directory, repo) = worktree
+ .repository_and_work_directory_for_path(&buffer_project_path.path)
+ .context("failed to get repo for blamed buffer")?;
+
+ let repo_entry = worktree
+ .get_local_repo(&repo)
+ .context("failed to get repo for blamed buffer")?;
+
+ let relative_path = buffer_project_path
+ .path
+ .strip_prefix(&work_directory)?
+ .to_path_buf();
+
+ let content = match version {
+ Some(version) => buffer.rope_for_version(&version).clone(),
+ None => buffer.as_rope().clone(),
+ };
+ let repo = repo_entry.repo().clone();
+
+ anyhow::Ok((repo, relative_path, content))
+ });
+
+ cx.background_executor().spawn(async move {
+ let (repo, relative_path, content) = blame_params?;
+ let lock = repo.lock();
+ lock.blame(&relative_path, content)
+ })
+ } else {
+ let project_id = self.remote_id();
+ let buffer_id = buffer.read(cx).remote_id();
+ let client = self.client.clone();
+ let version = buffer.read(cx).version();
+
+ cx.spawn(|_| async move {
+ let project_id = project_id.context("unable to get project id for buffer")?;
+ let response = client
+ .request(proto::BlameBuffer {
+ project_id,
+ buffer_id: buffer_id.into(),
+ version: serialize_version(&version),
+ })
+ .await?;
+
+ Ok(deserialize_blame_buffer_response(response))
+ })
+ }
+ }
+
// RPC message handlers
+ async fn handle_blame_buffer(
+ this: Model<Self>,
+ envelope: TypedEnvelope<proto::BlameBuffer>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<proto::BlameBufferResponse> {
+ let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
+ let version = deserialize_version(&envelope.payload.version);
+
+ let buffer = this.update(&mut cx, |this, _cx| {
+ this.opened_buffers
+ .get(&buffer_id)
+ .and_then(|buffer| buffer.upgrade())
+ .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))
+ })??;
+
+ buffer
+ .update(&mut cx, |buffer, _| {
+ buffer.wait_for_version(version.clone())
+ })?
+ .await?;
+
+ let blame = this
+ .update(&mut cx, |this, cx| {
+ this.blame_buffer(&buffer, Some(version), cx)
+ })?
+ .await?;
+
+ Ok(serialize_blame_buffer_response(blame))
+ }
+
async fn handle_unshare_project(
this: Model<Self>,
_: TypedEnvelope<proto::UnshareProject>,
@@ -9768,3 +9889,99 @@ async fn load_shell_environment(dir: &Path) -> Result<HashMap<String, String>> {
}
Ok(parsed_env)
}
+
+fn serialize_blame_buffer_response(blame: git::blame::Blame) -> proto::BlameBufferResponse {
+ let entries = blame
+ .entries
+ .into_iter()
+ .map(|entry| proto::BlameEntry {
+ sha: entry.sha.as_bytes().into(),
+ start_line: entry.range.start,
+ end_line: entry.range.end,
+ original_line_number: entry.original_line_number,
+ author: entry.author.clone(),
+ author_mail: entry.author_mail.clone(),
+ author_time: entry.author_time,
+ author_tz: entry.author_tz.clone(),
+ committer: entry.committer.clone(),
+ committer_mail: entry.committer_mail.clone(),
+ committer_time: entry.committer_time,
+ committer_tz: entry.committer_tz.clone(),
+ summary: entry.summary.clone(),
+ previous: entry.previous.clone(),
+ filename: entry.filename.clone(),
+ })
+ .collect::<Vec<_>>();
+
+ let messages = blame
+ .messages
+ .into_iter()
+ .map(|(oid, message)| proto::CommitMessage {
+ oid: oid.as_bytes().into(),
+ message,
+ })
+ .collect::<Vec<_>>();
+
+ let permalinks = blame
+ .permalinks
+ .into_iter()
+ .map(|(oid, url)| proto::CommitPermalink {
+ oid: oid.as_bytes().into(),
+ permalink: url.to_string(),
+ })
+ .collect::<Vec<_>>();
+
+ proto::BlameBufferResponse {
+ entries,
+ messages,
+ permalinks,
+ }
+}
+
+fn deserialize_blame_buffer_response(response: proto::BlameBufferResponse) -> git::blame::Blame {
+ let entries = response
+ .entries
+ .into_iter()
+ .filter_map(|entry| {
+ Some(git::blame::BlameEntry {
+ sha: git::Oid::from_bytes(&entry.sha).ok()?,
+ range: entry.start_line..entry.end_line,
+ original_line_number: entry.original_line_number,
+ committer: entry.committer,
+ committer_time: entry.committer_time,
+ committer_tz: entry.committer_tz,
+ committer_mail: entry.committer_mail,
+ author: entry.author,
+ author_mail: entry.author_mail,
+ author_time: entry.author_time,
+ author_tz: entry.author_tz,
+ summary: entry.summary,
+ previous: entry.previous,
+ filename: entry.filename,
+ })
+ })
+ .collect::<Vec<_>>();
+
+ let messages = response
+ .messages
+ .into_iter()
+ .filter_map(|message| Some((git::Oid::from_bytes(&message.oid).ok()?, message.message)))
+ .collect::<HashMap<_, _>>();
+
+ let permalinks = response
+ .permalinks
+ .into_iter()
+ .filter_map(|permalink| {
+ Some((
+ git::Oid::from_bytes(&permalink.oid).ok()?,
+ Url::from_str(&permalink.permalink).ok()?,
+ ))
+ })
+ .collect::<HashMap<_, _>>();
+
+ Blame {
+ entries,
+ permalinks,
+ messages,
+ }
+}
@@ -77,7 +77,7 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
)
.unwrap();
- let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await;
+ let project = Project::test(Arc::new(RealFs::default()), [root_link_path.as_ref()], cx).await;
project.update(cx, |project, cx| {
let tree = project.worktrees().next().unwrap().read(cx);
@@ -2844,7 +2844,7 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
}
}));
- let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await;
+ let project = Project::test(Arc::new(RealFs::default()), [dir.path()], cx).await;
let rpc = project.update(cx, |p, _| p.client.clone());
let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
@@ -205,7 +205,10 @@ message Envelope {
CountTokensWithLanguageModel count_tokens_with_language_model = 168;
CountTokensResponse count_tokens_response = 169;
UpdateChannelMessage update_channel_message = 170;
- ChannelMessageUpdate channel_message_update = 171; // current max
+ ChannelMessageUpdate channel_message_update = 171;
+
+ BlameBuffer blame_buffer = 172;
+ BlameBufferResponse blame_buffer_response = 173; // Current max
}
reserved 158 to 161;
@@ -1784,3 +1787,48 @@ message CountTokensWithLanguageModel {
message CountTokensResponse {
uint32 token_count = 1;
}
+
+message BlameBuffer {
+ uint64 project_id = 1;
+ uint64 buffer_id = 2;
+ repeated VectorClockEntry version = 3;
+}
+
+message BlameEntry {
+ bytes sha = 1;
+
+ uint32 start_line = 2;
+ uint32 end_line = 3;
+ uint32 original_line_number = 4;
+
+ optional string author = 5;
+ optional string author_mail = 6;
+ optional int64 author_time = 7;
+ optional string author_tz = 8;
+
+ optional string committer = 9;
+ optional string committer_mail = 10;
+ optional int64 committer_time = 11;
+ optional string committer_tz = 12;
+
+ optional string summary = 13;
+ optional string previous = 14;
+
+ string filename = 15;
+}
+
+message CommitMessage {
+ bytes oid = 1;
+ string message = 2;
+}
+
+message CommitPermalink {
+ bytes oid = 1;
+ string permalink = 2;
+}
+
+message BlameBufferResponse {
+ repeated BlameEntry entries = 1;
+ repeated CommitMessage messages = 2;
+ repeated CommitPermalink permalinks = 3;
+}
@@ -296,6 +296,8 @@ messages!(
(LspExtExpandMacro, Background),
(LspExtExpandMacroResponse, Background),
(SetRoomParticipantRole, Foreground),
+ (BlameBuffer, Foreground),
+ (BlameBufferResponse, Foreground),
);
request_messages!(
@@ -386,6 +388,7 @@ request_messages!(
(UpdateWorktree, Ack),
(LspExtExpandMacro, LspExtExpandMacroResponse),
(SetRoomParticipantRole, Ack),
+ (BlameBuffer, BlameBufferResponse),
);
entity_messages!(
@@ -393,6 +396,7 @@ entity_messages!(
AddProjectCollaborator,
ApplyCodeAction,
ApplyCompletionAdditionalEdits,
+ BlameBuffer,
BufferReloaded,
BufferSaved,
CopyProjectEntry,
@@ -108,6 +108,11 @@ fn test_random_edits(mut rng: StdRng) {
}
assert_eq!(text.to_string(), buffer.text());
+ assert_eq!(
+ buffer.rope_for_version(old_buffer.version()).to_string(),
+ old_buffer.text()
+ );
+
for _ in 0..5 {
let end_ix = old_buffer.clip_offset(rng.gen_range(0..=old_buffer.len()), Bias::Right);
let start_ix = old_buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left);
@@ -1622,6 +1622,49 @@ impl BufferSnapshot {
&self.visible_text
}
+ pub fn rope_for_version(&self, version: &clock::Global) -> Rope {
+ let mut rope = Rope::new();
+
+ let mut cursor = self
+ .fragments
+ .filter::<_, FragmentTextSummary>(move |summary| {
+ !version.observed_all(&summary.max_version)
+ });
+ cursor.next(&None);
+
+ let mut visible_cursor = self.visible_text.cursor(0);
+ let mut deleted_cursor = self.deleted_text.cursor(0);
+
+ while let Some(fragment) = cursor.item() {
+ if cursor.start().visible > visible_cursor.offset() {
+ let text = visible_cursor.slice(cursor.start().visible);
+ rope.append(text);
+ }
+
+ if fragment.was_visible(version, &self.undo_map) {
+ if fragment.visible {
+ let text = visible_cursor.slice(cursor.end(&None).visible);
+ rope.append(text);
+ } else {
+ deleted_cursor.seek_forward(cursor.start().deleted);
+ let text = deleted_cursor.slice(cursor.end(&None).deleted);
+ rope.append(text);
+ }
+ } else if fragment.visible {
+ visible_cursor.seek_forward(cursor.end(&None).visible);
+ }
+
+ cursor.next(&None);
+ }
+
+ if cursor.start().visible > visible_cursor.offset() {
+ let text = visible_cursor.slice(cursor.start().visible);
+ rope.append(text);
+ }
+
+ rope
+ }
+
pub fn remote_id(&self) -> BufferId {
self.remote_id
}
@@ -277,8 +277,8 @@ pub struct LocalRepositoryEntry {
}
impl LocalRepositoryEntry {
- pub fn load_index_text(&self, relative_file_path: &Path) -> Option<String> {
- self.repo_ptr.lock().load_index_text(relative_file_path)
+ pub fn repo(&self) -> &Arc<Mutex<dyn GitRepository>> {
+ &self.repo_ptr
}
}
@@ -459,7 +459,7 @@ async fn test_renaming_case_only(cx: &mut TestAppContext) {
const OLD_NAME: &str = "aaa.rs";
const NEW_NAME: &str = "AAA.rs";
- let fs = Arc::new(RealFs);
+ let fs = Arc::new(RealFs::default());
let temp_root = temp_tree(json!({
OLD_NAME: "",
}));
@@ -969,7 +969,7 @@ async fn test_write_file(cx: &mut TestAppContext) {
build_client(cx),
dir.path(),
true,
- Arc::new(RealFs),
+ Arc::new(RealFs::default()),
Default::default(),
&mut cx.to_async(),
)
@@ -1049,7 +1049,7 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
build_client(cx),
dir.path(),
true,
- Arc::new(RealFs),
+ Arc::new(RealFs::default()),
Default::default(),
&mut cx.to_async(),
)
@@ -1153,7 +1153,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
build_client(cx),
dir.path(),
true,
- Arc::new(RealFs),
+ Arc::new(RealFs::default()),
Default::default(),
&mut cx.to_async(),
)
@@ -1263,7 +1263,7 @@ async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
build_client(cx),
dot_git_worktree_dir.clone(),
true,
- Arc::new(RealFs),
+ Arc::new(RealFs::default()),
Default::default(),
&mut cx.to_async(),
)
@@ -1404,7 +1404,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
)
});
- let fs_real = Arc::new(RealFs);
+ let fs_real = Arc::new(RealFs::default());
let temp_root = temp_tree(json!({
"a": {}
}));
@@ -2008,7 +2008,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
build_client(cx),
root_path,
true,
- Arc::new(RealFs),
+ Arc::new(RealFs::default()),
Default::default(),
&mut cx.to_async(),
)
@@ -2087,7 +2087,7 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
build_client(cx),
root.path(),
true,
- Arc::new(RealFs),
+ Arc::new(RealFs::default()),
Default::default(),
&mut cx.to_async(),
)
@@ -2228,7 +2228,7 @@ async fn test_git_status(cx: &mut TestAppContext) {
build_client(cx),
root.path(),
true,
- Arc::new(RealFs),
+ Arc::new(RealFs::default()),
Default::default(),
&mut cx.to_async(),
)
@@ -92,7 +92,16 @@ fn main() {
let session_id = Uuid::new_v4().to_string();
init_panic_hook(&app, installation_id.clone(), session_id.clone());
- let fs = Arc::new(RealFs);
+ let git_binary_path = if option_env!("ZED_BUNDLE").as_deref() == Some("true") {
+ app.path_for_auxiliary_executable("git")
+ .context("could not find git binary path")
+ .log_err()
+ } else {
+ None
+ };
+ log::info!("Using git binary path: {:?}", git_binary_path);
+
+ let fs = Arc::new(RealFs::new(git_binary_path));
let user_settings_file_rx = watch_config_file(
&app.background_executor(),
fs.clone(),
@@ -112,11 +112,64 @@ mv Cargo.toml.backup Cargo.toml
popd
echo "Bundled ${app_path}"
+GIT_VERSION="v2.43.3"
+GIT_VERSION_SHA="fa29823"
+
+function download_and_unpack() {
+ local url=$1
+ local path_to_unpack=$2
+ local target_path=$3
+
+ temp_dir=$(mktemp -d)
+
+ if ! command -v curl &> /dev/null; then
+ echo "curl is not installed. Please install curl to continue."
+ exit 1
+ fi
+
+ curl --silent --fail --location "$url" | tar -xvz -C "$temp_dir" -f - $path_to_unpack
+
+ mv "$temp_dir/$path_to_unpack" "$target_path"
+
+ rm -rf "$temp_dir"
+}
+
+function download_git() {
+ local architecture=$1
+ local target_binary=$2
+
+ tmp_dir=$(mktemp -d)
+ pushd "$tmp_dir"
+
+ case "$architecture" in
+ aarch64-apple-darwin)
+ download_and_unpack "https://github.com/desktop/dugite-native/releases/download/${GIT_VERSION}/dugite-native-${GIT_VERSION}-${GIT_VERSION_SHA}-macOS-arm64.tar.gz" bin/git ./git
+ ;;
+ x86_64-apple-darwin)
+ download_and_unpack "https://github.com/desktop/dugite-native/releases/download/${GIT_VERSION}/dugite-native-${GIT_VERSION}-${GIT_VERSION_SHA}-macOS-x64.tar.gz" bin/git ./git
+ ;;
+ universal)
+ download_and_unpack "https://github.com/desktop/dugite-native/releases/download/${GIT_VERSION}/dugite-native-${GIT_VERSION}-${GIT_VERSION_SHA}-macOS-arm64.tar.gz" bin/git ./git_arm64
+ download_and_unpack "https://github.com/desktop/dugite-native/releases/download/${GIT_VERSION}/dugite-native-${GIT_VERSION}-${GIT_VERSION_SHA}-macOS-x64.tar.gz" bin/git ./git_x64
+ lipo -create ./git_arm64 ./git_x64 -output ./git
+ ;;
+ *)
+ echo "Unsupported architecture: $architecture"
+ exit 1
+ ;;
+ esac
+
+ popd
+
+ mv "${tmp_dir}/git" "${target_binary}"
+ rm -rf "$tmp_dir"
+}
+
function prepare_binaries() {
local architecture=$1
local app_path=$2
- echo "Uploading dSYMs for $architecture"
+ echo "Unpacking dSYMs for $architecture"
dsymutil --flat target/${architecture}/${target_dir}/Zed
version="$(cargo metadata --no-deps --manifest-path crates/zed/Cargo.toml --offline --format-version=1 | jq -r '.packages | map(select(.name == "zed"))[0].version')"
if [ "$channel" == "nightly" ]; then
@@ -139,14 +192,10 @@ function prepare_binaries() {
cp target/${architecture}/${target_dir}/cli "${app_path}/Contents/MacOS/cli"
}
-if [ "$local_arch" = false ]; then
- prepare_binaries "aarch64-apple-darwin" "$app_path_aarch64"
- prepare_binaries "x86_64-apple-darwin" "$app_path_x64"
-fi
-
function sign_binaries() {
local app_path=$1
- local architecture_dir=$2
+ local architecture=$2
+ local architecture_dir=$3
echo "Copying WebRTC.framework into the frameworks folder"
mkdir "${app_path}/Contents/Frameworks"
if [ "$local_arch" = false ]; then
@@ -156,6 +205,9 @@ function sign_binaries() {
cp -R target/${target_dir}/cli "${app_path}/Contents/MacOS/"
fi
+ echo "Downloading git binary"
+ download_git "${architecture}" "${app_path}/Contents/MacOS/git"
+
# Note: The app identifier for our development builds is the same as the app identifier for nightly.
cp crates/${zed_crate}/contents/$channel/embedded.provisionprofile "${app_path}/Contents/"
@@ -172,6 +224,7 @@ function sign_binaries() {
# sequence of codesign commands modeled after this example: https://developer.apple.com/forums/thread/701514
/usr/bin/codesign --deep --force --timestamp --sign "Zed Industries, Inc." "${app_path}/Contents/Frameworks/WebRTC.framework" -v
/usr/bin/codesign --deep --force --timestamp --options runtime --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/cli" -v
+ /usr/bin/codesign --deep --force --timestamp --options runtime --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/git" -v
/usr/bin/codesign --deep --force --timestamp --options runtime --entitlements crates/${zed_crate}/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/${zed_crate}" -v
/usr/bin/codesign --force --timestamp --options runtime --entitlements crates/${zed_crate}/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}" -v
@@ -303,9 +356,12 @@ function sign_binaries() {
}
if [ "$local_arch" = true ]; then
- sign_binaries "$app_path" "$local_target_triple"
+ sign_binaries "$app_path" "$local_target_triple" "$local_target_triple"
else
# Create universal binary
+ prepare_binaries "aarch64-apple-darwin" "$app_path_aarch64"
+ prepare_binaries "x86_64-apple-darwin" "$app_path_x64"
+
cp -R "$app_path_x64" target/release/
app_path=target/release/$(basename "$app_path_x64")
lipo \
@@ -318,8 +374,8 @@ else
target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/cli \
-output \
"${app_path}/Contents/MacOS/cli"
- sign_binaries "$app_path" "."
+ sign_binaries "$app_path" "universal" "."
- sign_binaries "$app_path_x64" "x86_64-apple-darwin"
- sign_binaries "$app_path_aarch64" "aarch64-apple-darwin"
+ sign_binaries "$app_path_x64" "x86_64-apple-darwin" "x86_64-apple-darwin"
+ sign_binaries "$app_path_aarch64" "aarch64-apple-darwin" "aarch64-apple-darwin"
fi