diff --git a/Default.sublime-keymap b/Default.sublime-keymap index dd1c90f2b..11497a3e5 100644 --- a/Default.sublime-keymap +++ b/Default.sublime-keymap @@ -471,6 +471,14 @@ // STATUS VIEW - NAVIGATION // ////////////////////////////// + { + "keys": ["o"], + "command": "gs_interface_show_commit", + "context": [ + { "key": "setting.command_mode", "operator": "equal", "operand": false }, + { "key": "selector", "operand": "meta.git-savvy.summary-header constant.other.git-savvy.sha1" } + ] + }, { "keys": ["?"], "command": "gs_interface_toggle_help", diff --git a/common/ui.py b/common/ui.py index e8a680a4d..5cea74f46 100644 --- a/common/ui.py +++ b/common/ui.py @@ -28,6 +28,7 @@ "gs_interface_close", "gs_interface_refresh", "gs_interface_toggle_help", + "gs_interface_show_commit", "gs_edit_view_complete", "gs_edit_view_close", ) @@ -308,6 +309,12 @@ def wrapper(self, *args, **kwargs): return wrapper +@contextmanager +def noop_context(): + # type: () -> Iterator[None] + yield + + class ReactiveInterface(Interface, GitCommand, Generic[T_state]): state: T_state subscribe_to: Set[str] @@ -347,13 +354,14 @@ def render(self): # We check twice if a re-render is actually necessary because the state has grown # and invalidates when formatted relative dates change, t.i., too often. @distinct_until_state_changed # <== 1st check data/state - def just_render(self): - # type: () -> None + def just_render(self, keep_cursor_on_something=True): + # type: (bool) -> None content, regions = self._render_template() if content == self.view.substr(sublime.Region(0, self.view.size())): # <== 2nd check actual view content return - with self.keep_cursor_on_something(): + ctx = self.keep_cursor_on_something() if keep_cursor_on_something else noop_context() + with ctx: self.draw(self.title(), content, regions) def initial_state(self): @@ -628,6 +636,19 @@ def run(self, edit): self.view.run_command("gs_interface_refresh") +class gs_interface_show_commit(TextCommand): + def run(self, edit: sublime.Edit) -> None: + view = self.view + frozen_sel = list(view.sel()) + window = view.window() + assert window + + for r in view.find_by_selector("constant.other.git-savvy.sha1"): + for s in frozen_sel: + if r.a <= s.a <= r.b: + window.run_command("gs_show_commit", {"commit_hash": view.substr(r)}) + + class EditView(): def __init__(self, content, on_done, repo_path, help_text=None, window=None): diff --git a/core/interfaces/branch.py b/core/interfaces/branch.py index 13074c56e..15ed2ce47 100644 --- a/core/interfaces/branch.py +++ b/core/interfaces/branch.py @@ -163,7 +163,11 @@ def cursor_is_on_active_branch(): "meta.git-savvy.branches.branch.active-branch" ) ) - on_special_symbol = partial(self.cursor_is_on_something, "meta.git-savvy.branches.branch") + on_special_symbol = partial( + self.cursor_is_on_something, + "meta.git-savvy.branches.branch" + ", constant.other.git-savvy.sha1" + ) cursor_was_on_active_branch = cursor_is_on_active_branch() yield @@ -678,7 +682,10 @@ class gs_branches_navigate_branch(GsNavigate): offset = 0 def get_available_regions(self): - return self.view.find_by_selector("constant.other.git-savvy.branches.branch.sha1") + return self.view.find_by_selector( + "constant.other.git-savvy.branches.branch.sha1" + ", meta.git-savvy.summary-header constant.other.git-savvy.sha1" + ) class gs_branches_navigate_to_active_branch(GsNavigate): diff --git a/core/interfaces/status.py b/core/interfaces/status.py index 11fe70cca..8f7b1b04d 100644 --- a/core/interfaces/status.py +++ b/core/interfaces/status.py @@ -3,7 +3,7 @@ import os import sublime -from sublime_plugin import WindowCommand +from sublime_plugin import EventListener, WindowCommand from ..git_mixins.status import FileStatus from ..git_mixins.active_branch import format_and_limit @@ -18,6 +18,7 @@ __all__ = ( "gs_show_status", + "StatusViewContextSensitiveHelpEventListener", "gs_status_open_file", "gs_status_open_file_on_remote", "gs_status_diff_inline", @@ -52,6 +53,7 @@ class StatusViewState(TypedDict, total=False): long_status: str git_root: str show_help: bool + help_context: Optional[str] head: HeadState branches: List[Branch] recent_commits: List[Commit] @@ -95,6 +97,26 @@ def run(self): ui.show_interface(self.window, self.repo_path, "status") +class StatusViewContextSensitiveHelpEventListener(EventListener): + def on_selection_modified_async(self, view): + interface = ui.interfaces.get(view.id()) + if not isinstance(interface, StatusInterface): + return + + frozen_sel = list(view.sel()) + if all(view.match_selector(s.a, "constant.other.git-savvy.sha1") for s in frozen_sel): + next_state = "on_commit" + elif all(view.match_selector(s.a, "meta.git-savvy.status.section.added") for s in frozen_sel): + next_state = "on_added" + else: + next_state = None + + current_state = interface.state.get("help_context") + if next_state != current_state: + interface.state["help_context"] = next_state + interface.just_render(keep_cursor_on_something=False) + + class StatusInterface(ui.ReactiveInterface, GitCommand): """ @@ -122,21 +144,7 @@ class StatusInterface(ui.ReactiveInterface, GitCommand): {< help} """ - template_help = """ - ################### ############### - ## SELECTED FILE ## ## ALL FILES ## - ################### ############### - - [o] open file [a] stage all unstaged files - [s] stage file [A] stage all unstaged and untracked files - [u] unstage file [U] unstage all staged files - [d] discard changes to file [D] discard all unstaged changes - [i] skip/unskip file - [h] open file in browser - - [l] diff file inline [f] diff all files - [e] diff file [F] diff all cached files - + _template_help = """ ############# ############# ## ACTIONS ## ## STASHES ## ############# ############# @@ -166,6 +174,43 @@ class StatusInterface(ui.ReactiveInterface, GitCommand): - """ + template_help = """ + ################### ############### + ## SELECTED FILE ## ## ALL FILES ## + ################### ############### + + [o] open file [a] stage all unstaged files + [s] stage file [A] stage all unstaged and untracked files + [u] unstage file [U] unstage all staged files + [d] discard changes to file [D] discard all unstaged changes + [i] skip/unskip file + [h] open file in browser + + [l] diff file inline [f] diff all files + [e] diff file [F] diff all cached files + """ + _template_help + + template_help_on_added = template_help.replace( + "[u] unstage file ", + "[u] unadd file " + ) + + template_help_on_commit = """ + ##################### ############### + ## SELECTED COMMIT ## ## ALL FILES ## + ##################### ############### + + [o] show commit [a] stage all unstaged files + [A] stage all unstaged and untracked files + [U] unstage all staged files + [D] discard all unstaged changes + + + + [f] diff all files + [F] diff all cached files + """ + _template_help + conflicts_keybindings = ui.indent_by_2(""" ############### ## CONFLICTS ## @@ -215,6 +260,11 @@ class StatusInterface(ui.ReactiveInterface, GitCommand): } state: StatusViewState + def initial_state(self): + return { + 'help_context': None, + } + def title(self): # type: () -> str return "STATUS: {}".format(os.path.basename(self.repo_path)) @@ -243,7 +293,11 @@ def refresh_view_state(self): def keep_cursor_on_something(self): # type: () -> Iterator[None] on_a_file = partial(self.cursor_is_on_something, 'meta.git-savvy.entity.filename') - on_special_symbol = partial(self.cursor_is_on_something, 'meta.git-savvy.section.body.row') + on_special_symbol = partial( + self.cursor_is_on_something, + 'meta.git-savvy.section.body.row' + ', constant.other.git-savvy.sha1' + ) was_on_a_file = on_a_file() yield @@ -384,11 +438,15 @@ def render_stashes(self, stashes): " ({}) {}".format(stash.id, stash.description) for stash in stashes)) @ui.section("help") - def render_help(self, show_help): - # type: (bool) -> str + def render_help(self, show_help, help_context): + # type: (bool, Optional[str]) -> str if not show_help: return "" + if help_context == "on_commit": + return self.template_help_on_commit.format(conflicts_bindings=self.render_conflicts_bindings()) + if help_context == "on_added": + return self.template_help_on_added.format(conflicts_bindings=self.render_conflicts_bindings()) return self.template_help.format(conflicts_bindings=self.render_conflicts_bindings()) @@ -876,6 +934,7 @@ class gs_status_navigate_file(GsNavigate): def get_available_regions(self): return self.view.find_by_selector( "meta.git-savvy.entity - meta.git-savvy.entity.filename.renamed.to" + ", meta.git-savvy.summary-header constant.other.git-savvy.sha1" ) diff --git a/core/interfaces/tags.py b/core/interfaces/tags.py index c147f5c01..4d577ccc4 100644 --- a/core/interfaces/tags.py +++ b/core/interfaces/tags.py @@ -27,6 +27,7 @@ "gs_tags_show_commit", "gs_tags_show_graph", "gs_tags_navigate_tag", + "gs_tags_navigate_to_next_tag", ) @@ -204,11 +205,15 @@ def sink(): @contextmanager def keep_cursor_on_something(self): # type: () -> Iterator[None] - on_special_symbol = partial(self.cursor_is_on_something, "meta.git-savvy.tags.tag") + on_special_symbol = partial( + self.cursor_is_on_something, + "meta.git-savvy.tags.tag" + ", constant.other.git-savvy.sha1" + ) yield if not on_special_symbol(): - self.view.run_command("gs_tags_navigate_tag") + self.view.run_command("gs_tags_navigate_to_next_tag") @ui.section("branch_status") def render_branch_status(self, long_status): @@ -531,7 +536,21 @@ def run(self, edit) -> None: class gs_tags_navigate_tag(GsNavigate): """ - Move cursor to the next (or previous) selectable file in the dashboard. + Move cursor to the next (or previous) selectable tag in the dashboard. + """ + offset = 0 + + def get_available_regions(self): + return self.view.find_by_selector( + "constant.other.git-savvy.tags.sha1" + ", meta.git-savvy.summary-header constant.other.git-savvy.sha1" + ) + + +class gs_tags_navigate_to_next_tag(GsNavigate): + + """ + Move cursor to the next (or previous) selectable tag in the dashboard. """ offset = 0