mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 00:15:06 +00:00
feat(editor): git change markers in the gutter
Diff each buffer against its file's index version (what VS Code's quick diff and the SCM panel's Changes compare with) and hand the hunks to the editor as gutter markers: added, modified, and a wedge where lines were deleted. The base is read with `git show :./name` through the buffer's own host, so remote workspaces work too; SFTP buffers and untracked files get no markers. It is re-read when the repository's SCM epoch moves, on save and when the path changes; the diff itself is a line-level Myers diff run off the UI thread shortly after each edit. Next/previous change and Revert Change (one undo step, also in the right-click menu) are commands; clicking a marker opens a peek of the staged lines with a Revert button. The editor watch now also follows the repository the gutter found, so a stage or checkout refreshes the markers. The `editor_git_gutter` config (default on) is flipped by the ToggleEditorGitGutter command.
This commit is contained in:
@@ -263,6 +263,12 @@ pub struct Config {
|
||||
/// [`Self::editor_soft_wrap`]. Files that are not Markdown ignore it.
|
||||
#[serde(default)]
|
||||
pub editor_markdown_preview: bool,
|
||||
/// Whether the code editor marks lines that differ from the file's
|
||||
/// staged (index) version in its gutter. On by default; flipped by the
|
||||
/// `ToggleEditorGitGutter` command and remembered like
|
||||
/// [`Self::editor_soft_wrap`].
|
||||
#[serde(default = "default_true")]
|
||||
pub editor_git_gutter: bool,
|
||||
/// Whether the sidebar files tabs nobody pinned into groups of its own —
|
||||
/// by repository, and by host for an SSH pane. Off, those tabs sit in one
|
||||
/// flat list below the pinned groups, which are the user's and show
|
||||
@@ -759,6 +765,7 @@ impl Default for Config {
|
||||
scm_changes_tree: false,
|
||||
editor_soft_wrap: false,
|
||||
editor_markdown_preview: false,
|
||||
editor_git_gutter: true,
|
||||
sidebar_auto_grouping: true,
|
||||
notify_on_command_finish: NotifyMode::Unfocused,
|
||||
check_for_updates: true,
|
||||
|
||||
@@ -128,6 +128,10 @@ actions!(
|
||||
EditorSaveAs,
|
||||
EditorGoToLine,
|
||||
EditorNewFile,
|
||||
EditorNextChange,
|
||||
EditorPrevChange,
|
||||
EditorRevertChange,
|
||||
ToggleEditorGitGutter,
|
||||
OpenSshProfiles,
|
||||
RestartSshSession,
|
||||
SendTab,
|
||||
|
||||
@@ -890,8 +890,12 @@ impl Tty7App {
|
||||
// it against a local repository that merely shares the path.
|
||||
let (host, path) = self.editor_active_location()?;
|
||||
let root = cx
|
||||
.try_global::<crate::terminal::git_status::GitStatusCache>()?
|
||||
.repo_root_for(host, path.parent()?)?;
|
||||
.try_global::<crate::terminal::git_status::GitStatusCache>()
|
||||
.and_then(|cache| cache.repo_root_for(host, path.parent()?))
|
||||
// The gutter's own base read finds the repository of a file no
|
||||
// terminal has been in, and its markers need this watch to
|
||||
// notice a stage or a checkout.
|
||||
.or_else(|| self.editor_gutter_root())?;
|
||||
Some((host, root.to_path_buf()))
|
||||
}
|
||||
|
||||
|
||||
@@ -9362,6 +9362,26 @@ impl Render for Tty7App {
|
||||
.on_action(cx.listener(|this, _: &EditorNewFile, window, cx| {
|
||||
this.editor_new_file(window, cx)
|
||||
}))
|
||||
// The gutter's change markers. Each one lets the chord through
|
||||
// when the editor is not focused or has nothing to do.
|
||||
.on_action(cx.listener(|this, _: &EditorNextChange, window, cx| {
|
||||
if !this.editor_gutter_step(true, window, cx) {
|
||||
cx.propagate();
|
||||
}
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &EditorPrevChange, window, cx| {
|
||||
if !this.editor_gutter_step(false, window, cx) {
|
||||
cx.propagate();
|
||||
}
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &EditorRevertChange, window, cx| {
|
||||
if !this.editor_gutter_revert_at_cursor(window, cx) {
|
||||
cx.propagate();
|
||||
}
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &ToggleEditorGitGutter, _, cx| {
|
||||
this.toggle_editor_git_gutter(cx)
|
||||
}))
|
||||
.on_action(
|
||||
cx.listener(|this, _: &Quit, window, cx| this.quit_stop_sessions(window, cx)),
|
||||
)
|
||||
|
||||
@@ -22,6 +22,8 @@ use crate::ui::editor_text::{self, EditorConfig, Indent, LineEnding, TextFormat}
|
||||
use crate::ui::host_ops::{HostId, HostOps, MTime, SharedHost, WatchSub};
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt};
|
||||
|
||||
mod gutter;
|
||||
|
||||
const MAX_FILE_BYTES: u64 = 4 * 1024 * 1024;
|
||||
|
||||
const RELOAD_DEBOUNCE: std::time::Duration = std::time::Duration::from_millis(200);
|
||||
@@ -117,6 +119,8 @@ pub(crate) struct OpenFile {
|
||||
/// scrollbar every other scrolling surface in tty7 has. The editor itself
|
||||
/// gets one from `Input`.
|
||||
pub(crate) preview_scroll: gpui::ScrollHandle,
|
||||
/// Git change markers — see `gutter`.
|
||||
gutter: gutter::BufferGutter,
|
||||
_sub: Subscription,
|
||||
_observe: Subscription,
|
||||
}
|
||||
@@ -1247,6 +1251,7 @@ impl Tty7App {
|
||||
.default_value(text)
|
||||
});
|
||||
let id = input.entity_id();
|
||||
let git_gutter = gutter::BufferGutter::attach(&input, id, cx);
|
||||
let sub = cx.subscribe_in(
|
||||
&input,
|
||||
window,
|
||||
@@ -1277,6 +1282,7 @@ impl Tty7App {
|
||||
preview,
|
||||
wrap,
|
||||
preview_scroll: gpui::ScrollHandle::new(),
|
||||
gutter: git_gutter,
|
||||
_sub: sub,
|
||||
_observe: observe,
|
||||
});
|
||||
@@ -1297,6 +1303,7 @@ impl Tty7App {
|
||||
if let Some(f) = self.buffer_mut(id) {
|
||||
f.dirty = dirty;
|
||||
}
|
||||
self.editor_gutter_note_edit(id, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1587,6 +1594,7 @@ impl Tty7App {
|
||||
f.saved_format = format;
|
||||
f.conflict = None;
|
||||
app.editor_note_edit(id, cx);
|
||||
app.editor_gutter_refetch(id);
|
||||
// A save is a working-tree edit the `.git` watch cannot
|
||||
// see, and the file tree only sees it while it happens
|
||||
// to be showing that directory.
|
||||
@@ -2104,6 +2112,11 @@ impl Tty7App {
|
||||
.menu(
|
||||
t(L10nKey::EditorGoToLineAction),
|
||||
Box::new(crate::core::actions::EditorGoToLine),
|
||||
)
|
||||
.menu_with_disabled(
|
||||
t(L10nKey::EditorGitRevertChange),
|
||||
Box::new(crate::core::actions::EditorRevertChange),
|
||||
!this.editor_gutter_can_revert(id, cx),
|
||||
);
|
||||
this.editor_file_menu_items(menu, id, app, cx)
|
||||
}
|
||||
@@ -2823,6 +2836,7 @@ impl Tty7App {
|
||||
if !self.code_panel_visible() {
|
||||
return None;
|
||||
}
|
||||
self.editor_gutter_sync(cx);
|
||||
let body = match self.active_buffer() {
|
||||
None => self.render_editor_empty(cx).into_any_element(),
|
||||
Some(f) if f.preview => {
|
||||
@@ -2869,6 +2883,7 @@ impl Tty7App {
|
||||
.text_size(cx.theme().mono_font_size)
|
||||
.size_full(),
|
||||
)
|
||||
.children(self.render_editor_gutter_peek(id, cx))
|
||||
.context_menu(move |menu, _window, cx| {
|
||||
Self::editor_body_menu(menu, &app, id, cx)
|
||||
})
|
||||
@@ -2921,6 +2936,10 @@ impl Tty7App {
|
||||
if ev.keystroke.key != "escape" {
|
||||
return;
|
||||
}
|
||||
if this.editor_gutter_close_peek(cx) {
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
}
|
||||
// Escape in the go-to-line or Save As box dismisses the
|
||||
// box, not the whole editor.
|
||||
if this.editor.bar.is_some() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1202,6 +1202,15 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::EditorGoToLine => "Go to line",
|
||||
L10nKey::EditorGoToLineAction => "Go to Line…",
|
||||
L10nKey::EditorCopyRelativePath => "Copy Relative Path",
|
||||
L10nKey::EditorGitNextChange => "Go to Next Change",
|
||||
L10nKey::EditorGitPrevChange => "Go to Previous Change",
|
||||
L10nKey::EditorGitRevertChange => "Revert Change",
|
||||
L10nKey::EditorGitToggleGutter => "Toggle Git Change Markers",
|
||||
L10nKey::EditorGitPeekRevert => "Revert",
|
||||
L10nKey::EditorGitPeekSummary => "−{removed} +{added} lines against the staged version",
|
||||
L10nKey::EditorGitPeekAddedOnly => {
|
||||
"These lines are new: the staged version has nothing here."
|
||||
}
|
||||
L10nKey::EditorGoToLinePlaceholder => "Line, or line:column (1–{total})",
|
||||
L10nKey::EditorSaveAs => "Save as",
|
||||
L10nKey::EditorSaveAsAction => "Save As…",
|
||||
|
||||
@@ -1250,6 +1250,15 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::EditorGoToLine => "行へ移動",
|
||||
L10nKey::EditorGoToLineAction => "行へ移動…",
|
||||
L10nKey::EditorCopyRelativePath => "相対パスをコピー",
|
||||
L10nKey::EditorGitNextChange => "次の変更へ移動",
|
||||
L10nKey::EditorGitPrevChange => "前の変更へ移動",
|
||||
L10nKey::EditorGitRevertChange => "変更を元に戻す",
|
||||
L10nKey::EditorGitToggleGutter => "Git 変更マーカーの切り替え",
|
||||
L10nKey::EditorGitPeekRevert => "元に戻す",
|
||||
L10nKey::EditorGitPeekSummary => "ステージ済みの版と比べて −{removed} +{added} 行",
|
||||
L10nKey::EditorGitPeekAddedOnly => {
|
||||
"新しく追加された行です。ステージ済みの版にはここに何もありません。"
|
||||
}
|
||||
L10nKey::EditorGoToLinePlaceholder => "行番号、または 行:列(1–{total})",
|
||||
L10nKey::EditorSaveAs => "名前を付けて保存",
|
||||
L10nKey::EditorSaveAsAction => "名前を付けて保存…",
|
||||
|
||||
@@ -936,6 +936,13 @@ l10n_keys! {
|
||||
EditorGoToLine,
|
||||
EditorGoToLineAction,
|
||||
EditorCopyRelativePath,
|
||||
EditorGitNextChange,
|
||||
EditorGitPrevChange,
|
||||
EditorGitRevertChange,
|
||||
EditorGitToggleGutter,
|
||||
EditorGitPeekRevert,
|
||||
EditorGitPeekSummary,
|
||||
EditorGitPeekAddedOnly,
|
||||
EditorGoToLinePlaceholder,
|
||||
EditorSaveAs,
|
||||
EditorSaveAsAction,
|
||||
|
||||
@@ -1122,6 +1122,13 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::EditorGoToLine => "跳转到行",
|
||||
L10nKey::EditorGoToLineAction => "跳转到行…",
|
||||
L10nKey::EditorCopyRelativePath => "复制相对路径",
|
||||
L10nKey::EditorGitNextChange => "转到下一处更改",
|
||||
L10nKey::EditorGitPrevChange => "转到上一处更改",
|
||||
L10nKey::EditorGitRevertChange => "还原此处更改",
|
||||
L10nKey::EditorGitToggleGutter => "切换 Git 更改标记",
|
||||
L10nKey::EditorGitPeekRevert => "还原",
|
||||
L10nKey::EditorGitPeekSummary => "与暂存版本相比 −{removed} +{added} 行",
|
||||
L10nKey::EditorGitPeekAddedOnly => "这些是新增的行:暂存版本中此处没有内容。",
|
||||
L10nKey::EditorGoToLinePlaceholder => "行号,或 行:列(1–{total})",
|
||||
L10nKey::EditorSaveAs => "另存为",
|
||||
L10nKey::EditorSaveAsAction => "另存为…",
|
||||
|
||||
@@ -592,6 +592,12 @@ fn shipped_bindings() -> Vec<(&'static str, &'static str)> {
|
||||
("EditorGoToLine", "ctrl-g"),
|
||||
// Unbound: ⌘N is New Window, and the editor's header has a + for it.
|
||||
("EditorNewFile", ""),
|
||||
// Alt+F5 / Shift+Alt+F5 (VS Code's) are fixed `Input`-context
|
||||
// bindings: a default on a function key would hide it from the shell.
|
||||
("EditorNextChange", ""),
|
||||
("EditorPrevChange", ""),
|
||||
("EditorRevertChange", ""),
|
||||
("ToggleEditorGitGutter", ""),
|
||||
("OpenSshProfiles", ""),
|
||||
("RestartSshSession", "secondary-shift-r"),
|
||||
("Quit", per_platform("secondary-q", "secondary-shift-q")),
|
||||
@@ -882,6 +888,22 @@ fn authored_entry(action: &str) -> Option<(CommandGroup, String)> {
|
||||
CommandGroup::Terminal,
|
||||
t(L10nKey::EditorNewFile).to_string(),
|
||||
),
|
||||
"EditorNextChange" => (
|
||||
CommandGroup::Terminal,
|
||||
t(L10nKey::EditorGitNextChange).to_string(),
|
||||
),
|
||||
"EditorPrevChange" => (
|
||||
CommandGroup::Terminal,
|
||||
t(L10nKey::EditorGitPrevChange).to_string(),
|
||||
),
|
||||
"EditorRevertChange" => (
|
||||
CommandGroup::Terminal,
|
||||
t(L10nKey::EditorGitRevertChange).to_string(),
|
||||
),
|
||||
"ToggleEditorGitGutter" => (
|
||||
CommandGroup::Terminal,
|
||||
t(L10nKey::EditorGitToggleGutter).to_string(),
|
||||
),
|
||||
"OpenSshProfiles" => (
|
||||
CommandGroup::Ssh,
|
||||
t(L10nKey::CmdSshManageProfiles).to_string(),
|
||||
@@ -1573,6 +1595,10 @@ fn make_binding(action: &str, keystroke: &str) -> Option<KeyBinding> {
|
||||
"EditorSaveAs" => KeyBinding::new(keystroke, EditorSaveAs, None),
|
||||
"EditorGoToLine" => KeyBinding::new(keystroke, EditorGoToLine, None),
|
||||
"EditorNewFile" => KeyBinding::new(keystroke, EditorNewFile, None),
|
||||
"EditorNextChange" => KeyBinding::new(keystroke, EditorNextChange, None),
|
||||
"EditorPrevChange" => KeyBinding::new(keystroke, EditorPrevChange, None),
|
||||
"EditorRevertChange" => KeyBinding::new(keystroke, EditorRevertChange, None),
|
||||
"ToggleEditorGitGutter" => KeyBinding::new(keystroke, ToggleEditorGitGutter, None),
|
||||
"OpenSshProfiles" => KeyBinding::new(keystroke, OpenSshProfiles, None),
|
||||
"RestartSshSession" => KeyBinding::new(keystroke, RestartSshSession, None),
|
||||
"Quit" => KeyBinding::new(keystroke, Quit, None),
|
||||
|
||||
Reference in New Issue
Block a user