Skip to content

Commit e111603

Browse files
committed
Implement reactive branch dashboard
1 parent 6569220 commit e111603

File tree

1 file changed

+129
-27
lines changed

1 file changed

+129
-27
lines changed

core/interfaces/branch.py

Lines changed: 129 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1+
from contextlib import contextmanager
2+
from functools import partial
13
import os
4+
import threading
25

6+
import sublime
37
from sublime_plugin import WindowCommand
48

9+
from .status import distinct_until_state_changed
510
from ...common import ui, util
611
from ..commands import GsNavigate
712
from ..commands.log import LogMixin
813
from ..git_command import GitCommand
914
from ..ui_mixins.quick_panel import show_remote_panel, show_branch_panel
1015
from ..ui_mixins.input_panel import show_single_line_input_panel
16+
from GitSavvy.core import store
1117
from GitSavvy.core.fns import filter_
1218
from GitSavvy.core.utils import flash
1319
from GitSavvy.core.runtime import on_worker
@@ -39,8 +45,24 @@
3945

4046
MYPY = False
4147
if MYPY:
42-
from typing import List, Optional
43-
from GitSavvy.core.git_mixins.branches import Branch
48+
from typing import Dict, List, Optional, TypedDict
49+
from ..git_mixins.active_branch import Commit
50+
from ..git_mixins.branches import Branch
51+
52+
BranchViewState = TypedDict(
53+
"BranchViewState",
54+
{
55+
"git_root": str,
56+
"long_status": str,
57+
"branches": List[Branch],
58+
"descriptions": Dict[str, str],
59+
"remotes": Dict[str, str],
60+
"recent_commits": List[Commit],
61+
"show_remotes": bool,
62+
"show_help": bool,
63+
},
64+
total=False
65+
)
4466

4567

4668
class gs_show_branch(WindowCommand, GitCommand):
@@ -62,8 +84,6 @@ class BranchInterface(ui.Interface, GitCommand):
6284
interface_type = "branch"
6385
syntax_file = "Packages/GitSavvy/syntax/branch.sublime-syntax"
6486

65-
show_remotes = None
66-
6787
template = """\
6888
6989
ROOT: {git_root}
@@ -105,18 +125,80 @@ class BranchInterface(ui.Interface, GitCommand):
105125
REMOTE ({remote_name}):
106126
{remote_branch_list}"""
107127

128+
def __init__(self, *args, **kwargs):
129+
self._lock = threading.Lock()
130+
self.state = {
131+
'git_root': '',
132+
'long_status': '',
133+
'recent_commits': [],
134+
'branches': [],
135+
'descriptions': {},
136+
'remotes': {},
137+
'show_remotes': self.savvy_settings.get("show_remotes_in_branch_dashboard"),
138+
'show_help': True,
139+
} # type: BranchViewState
140+
super().__init__(*args, **kwargs)
141+
108142
def title(self):
109143
return "BRANCHES: {}".format(os.path.basename(self.repo_path))
110144

111-
def pre_render(self):
145+
def refresh_view_state(self):
112146
sort_by_recent = self.savvy_settings.get("sort_by_recent_in_branch_dashboard")
113-
self._branches = self.get_branches(sort_by_recent=sort_by_recent)
114-
self.descriptions = self.fetch_branch_description_subjects()
115-
if self.show_remotes is None:
116-
self.show_remotes = self.savvy_settings.get("show_remotes_in_branch_dashboard")
117-
self.remotes = self.get_remotes() if self.show_remotes else {}
147+
for thunk in (
148+
lambda: {'recent_commits': self.get_latest_commits()},
149+
lambda: {
150+
'branches': self.get_branches(sort_by_recent=sort_by_recent),
151+
'descriptions': self.fetch_branch_description_subjects(),
152+
},
153+
lambda: {'remotes': self.get_remotes()},
154+
):
155+
sublime.set_timeout_async(
156+
partial(self.update_state, thunk, then=self.just_render)
157+
)
158+
159+
self.view.run_command("gs_update_status")
160+
# These are cheap to compute, so we just do it!
161+
state = store.current_state(self.repo_path)
162+
status = state.get("status")
163+
if status:
164+
self.update_state(status._asdict())
165+
self.update_state({
166+
'git_root': self.short_repo_path,
167+
'branches': state.get("branches", []),
168+
'recent_commits': state.get("recent_commits", []),
169+
'show_help': not self.view.settings().get("git_savvy.help_hidden"),
170+
})
171+
172+
def update_state(self, data, then=None):
173+
"""Update internal view state and maybe invoke a callback.
174+
175+
`data` can be a mapping or a callable ("thunk") which returns
176+
a mapping.
177+
178+
Note: We invoke the "sink" without any arguments. TBC.
179+
"""
180+
if callable(data):
181+
data = data()
182+
183+
with self._lock:
184+
self.state.update(data)
185+
186+
if callable(then):
187+
then()
118188

119189
def render(self):
190+
"""Refresh view state and render."""
191+
self.refresh_view_state()
192+
self.just_render()
193+
194+
@distinct_until_state_changed
195+
def just_render(self):
196+
content, regions = self._render_template()
197+
with self.keep_cursor_on_something():
198+
self.draw(self.title(), content, regions)
199+
200+
@contextmanager
201+
def keep_cursor_on_something(self):
120202
def cursor_is_on_active_branch():
121203
sel = self.view.sel()
122204
return (
@@ -128,29 +210,50 @@ def cursor_is_on_active_branch():
128210
)
129211

130212
cursor_was_on_active_branch = cursor_is_on_active_branch()
131-
super().render()
213+
yield
132214
if cursor_was_on_active_branch and not cursor_is_on_active_branch():
133215
self.view.run_command("gs_branches_navigate_to_active_branch")
134216

217+
def on_status_update(self, _repo_path, state):
218+
try:
219+
new_state = state["status"]._asdict()
220+
except KeyError:
221+
new_state = {}
222+
new_state["branches"] = state.get("branches")
223+
new_state["recent_commits"] = state.get("recent_commits")
224+
self.update_state(new_state, then=self.just_render)
225+
226+
def on_create(self):
227+
self._unsubscribe = store.subscribe(
228+
self.repo_path, {"status", "branches", "recent_commits"}, self.on_status_update
229+
)
230+
231+
def on_close(self):
232+
self._unsubscribe()
233+
135234
def on_new_dashboard(self):
136235
self.view.run_command("gs_branches_navigate_to_active_branch")
137236

138237
@ui.section("branch_status")
139238
def render_branch_status(self):
140-
return self.get_working_dir_status().long_status
239+
return self.state['long_status']
141240

142241
@ui.section("git_root")
143242
def render_git_root(self):
144-
return self.short_repo_path
243+
return self.state['git_root']
145244

146245
@ui.section("head")
147246
def render_head(self):
148-
return self.get_latest_commit_msg_for_head()
247+
recent_commits = self.state['recent_commits']
248+
if not recent_commits:
249+
return "No commits yet."
250+
251+
return "{0.hash} {0.message}".format(recent_commits[0])
149252

150253
@ui.section("branch_list")
151254
def render_branch_list(self):
152255
# type: () -> str
153-
branches = [branch for branch in self._branches if not branch.is_remote]
256+
branches = [branch for branch in self.state["branches"] if not branch.is_remote]
154257
return self._render_branch_list(None, branches)
155258

156259
def _render_branch_list(self, remote_name, branches):
@@ -161,7 +264,7 @@ def _render_branch_list(self, remote_name, branches):
161264
indicator="▸" if branch.active else " ",
162265
hash=branch.commit_hash,
163266
name=branch.canonical_name[remote_name_l:],
164-
description=(" " + self.descriptions.get(branch.canonical_name, "")).rstrip(),
267+
description=(" " + self.state["descriptions"].get(branch.canonical_name, "")).rstrip(),
165268
tracking=(" ({branch}{status})".format(
166269
branch=branch.upstream.canonical_name,
167270
status=", " + branch.upstream.status if branch.upstream.status else ""
@@ -172,26 +275,25 @@ def _render_branch_list(self, remote_name, branches):
172275
@ui.section("remotes")
173276
def render_remotes(self):
174277
return (self.render_remotes_on()
175-
if self.show_remotes else
278+
if self.state["show_remotes"] else
176279
self.render_remotes_off())
177280

178281
@ui.section("help")
179282
def render_help(self):
180-
help_hidden = self.view.settings().get("git_savvy.help_hidden")
181-
if help_hidden:
283+
show_help = self.state['show_help']
284+
if not show_help:
182285
return ""
183-
else:
184-
return self.template_help
286+
return self.template_help
185287

186288
def render_remotes_off(self):
187289
return "\n\n ** Press [e] to toggle display of remote branches. **\n"
188290

189291
def render_remotes_on(self):
190292
output_tmpl = "\n"
191293
render_fns = []
192-
remote_branches = [b for b in self._branches if b.is_remote]
294+
remote_branches = [b for b in self.state["branches"] if b.is_remote]
193295

194-
for remote_name in self.remotes:
296+
for remote_name in self.state["remotes"]:
195297
key = "branch_list_" + remote_name
196298
output_tmpl += "{" + key + "}\n"
197299
branches = [b for b in remote_branches if b.canonical_name.startswith(remote_name + "/")]
@@ -228,7 +330,7 @@ def get_selected_branches(self, ignore_current_branch=False):
228330
def select_branch(remote_name, branch_name):
229331
# type: (str, str) -> Branch
230332
canonical_name = "/".join(filter_((remote_name, branch_name)))
231-
for branch in self.interface._branches:
333+
for branch in self.interface.state["branches"]:
232334
if branch.canonical_name == canonical_name:
233335
return (
234336
branch._replace(
@@ -260,7 +362,7 @@ def select_branch(remote_name, branch_name):
260362
)
261363
] + [
262364
select_branch(remote_name, branch_name)
263-
for remote_name in self.interface.remotes
365+
for remote_name in self.interface.state["remotes"]
264366
for branch_name in ui.extract_by_selector(
265367
self.view,
266368
"meta.git-savvy.branches.branch.name",
@@ -525,9 +627,9 @@ class gs_branches_toggle_remotes(BranchInterfaceCommand):
525627

526628
def run(self, edit, show=None):
527629
if show is None:
528-
self.interface.show_remotes = not self.interface.show_remotes
630+
self.interface.update_state({"show_remotes": not self.interface.state["show_remotes"]})
529631
else:
530-
self.interface.show_remotes = show
632+
self.interface.update_state({"show_remotes": show})
531633
self.interface.render()
532634

533635

0 commit comments

Comments
 (0)