def set_status(branch_name): _, _, inserted, modified, deleted = contents template = (settings.get('status_bar_text') if _HAVE_JINJA2 else None) if template: # render the template using jinja2 library text = jinja2.environment.Template(''.join(template)).render( repo=self.git_handler.repository_name, compare=self.git_handler.format_compare_against(), branch=branch_name, state=file_state, deleted=len(deleted), inserted=len(inserted), modified=len(modified)) else: # Render hardcoded text if jinja is not available. parts = [] parts.append('On %s' % branch_name) compare = self.git_handler.format_compare_against() if compare not in ('HEAD', branch_name): parts.append('Comparing against %s' % compare) count = len(inserted) if count: parts.append('%d+' % count) count = len(deleted) if count: parts.append('%d-' % count) count = len(modified) if count: parts.append(u'%d≠' % count) text = ', '.join(parts) # add text and try to be the left most one self.git_handler.view.set_status('00_git_gutter', text)
def jump(self, all_changes, current_row): if settings.get("next_prev_change_wrap", True): default = all_changes[-1] else: default = all_changes[0] return next((change for change in reversed(all_changes) if change < current_row), default)
def _check_ignored_or_untracked(self, contents): """Check diff result and invoke gutter and status message update. Arguments: contentes - a tuble of ([inserted], [modified], [deleted]) lines """ if self.git_handler.in_repo() is False: show_untracked = settings.get( 'show_markers_on_untracked_file', False) # need to check for ignored or untracked file if show_untracked: def bind_ignored_or_untracked(is_ignored): if is_ignored: self._bind_files('ignored') else: def bind_untracked(is_untracked): if is_untracked: self._bind_files('untracked') else: # file was staged but empty self._bind_files('inserted') self.git_handler.untracked().then(bind_untracked) self.git_handler.ignored().then(bind_ignored_or_untracked) # show_untracked was set to false recently so clear gutter elif self.show_untracked: self._clear_all() self._update_status(0, 0, 0, "", "") self.show_untracked = show_untracked # update the if lines changed elif self.diff_results is None or self.diff_results != contents: self.diff_results = contents self._update_ui(contents)
def next_jump(self, all_changes, current_row): if settings.get('next_prev_change_wrap', True): default = all_changes[0] else: default = all_changes[-1] return next((change for change in all_changes if change > current_row), default)
def prev_jump(self, all_changes, current_row): if settings.get('next_prev_change_wrap', True): default = all_changes[-1] else: default = all_changes[0] return next((change for change in reversed(all_changes) if change < current_row), default)
def on_hover(self, view, point, hover_zone): if hover_zone != sublime.HOVER_GUTTER: return # don't let the popup flicker / fight with other packages if view.is_popup_visible(): return if not settings.get("enable_hover_diff_popup"): return show_diff_popup(view, point, flags=sublime.HIDE_ON_MOUSE_MOVE_AWAY)
def on_hover(self, view, point, hover_zone): if hover_zone != sublime.HOVER_GUTTER: return # don't let the popup flicker / fight with other packages if view.is_popup_visible(): return if not settings.get("enable_hover_diff_popup"): return view.run_command( 'git_gutter_diff_popup', args={'point': point, 'flags': sublime.HIDE_ON_MOUSE_MOVE_AWAY})
def run(self, force_refresh=False): self.view = self.window.active_view() if not self.view: # View is not ready yet, try again later. sublime.set_timeout(self.run, 1) return self.clear_all() self.show_in_minimap = settings.get('show_in_minimap', True) show_untracked = settings.get('show_markers_on_untracked_file', False) if ViewCollection.untracked(self.view): if show_untracked: self.bind_files('untracked') elif ViewCollection.ignored(self.view): if show_untracked: self.bind_files('ignored') else: # If the file is untracked there is no need to execute the diff # update if force_refresh: ViewCollection.clear_git_time(self.view) inserted, modified, deleted = ViewCollection.diff(self.view) self.lines_removed(deleted) self.bind_icons('inserted', inserted) self.bind_icons('changed', modified) if(ViewCollection.show_status(self.view) != "none"): if(ViewCollection.show_status(self.view) == 'all'): branch = ViewCollection.current_branch( self.view).decode("utf-8").strip() else: branch = "" self.update_status(len(inserted), len(modified), len(deleted), ViewCollection.get_compare(self.view), branch) else: self.update_status(0, 0, 0, "", "")
def _update_status(self, file_state, contents): """Update status message. The method joins and renders the lines read from 'status_bar_text' setting to the status bar using the jinja2 library to fill in all the state information of the open file. Arguments: file_state (string): The git status of the open file. contents (tuble): The result of git_handler.diff(), with the information about the modifications of the file. Scheme: (first, last, [inserted], [modified], [deleted]) """ if not settings.get('show_status_bar_text', False): self.git_handler.view.erase_status('00_git_gutter') return def set_status(branch_name): _, _, inserted, modified, deleted = contents template = (settings.get('status_bar_text') if _HAVE_JINJA2 else None) if template: # render the template using jinja2 library text = jinja2.environment.Template(''.join(template)).render( repo=self.git_handler.repository_name, compare=self.git_handler.format_compare_against(), branch=branch_name, state=file_state, deleted=len(deleted), inserted=len(inserted), modified=len(modified)) else: # Render hardcoded text if jinja is not available. parts = [] parts.append('On %s' % branch_name) compare = self.git_handler.format_compare_against() if compare not in ('HEAD', branch_name): parts.append('Comparing against %s' % compare) count = len(inserted) if count: parts.append('%d+' % count) count = len(deleted) if count: parts.append('%d-' % count) count = len(modified) if count: parts.append(u'%d≠' % count) text = ', '.join(parts) # add text and try to be the left most one self.git_handler.view.set_status('00_git_gutter', text) self.git_handler.git_current_branch().then(set_status)
def _get_protected_regions(self): """Create a list of line start points of all protected lines. A protected region describes a line which is occupied by a higher prior gutter icon which must not be overwritten by GitGutter. Returns: frozenset: A list of protected lines' start points. """ view = self.git_handler.view keys = settings.get('protected_regions', []) return frozenset( view.line(reg).a for key in keys for reg in view.get_regions(key))
def on_hover(self, view, point, hover_zone): if hover_zone != sublime.HOVER_GUTTER: return # don't let the popup flicker / fight with other packages if view.is_popup_visible(): return if not settings.get("enable_hover_diff_popup"): return view.run_command('git_gutter_diff_popup', args={ 'point': point, 'flags': sublime.HIDE_ON_MOUSE_MOVE_AWAY })
def _is_region_protected(self, region): # Load protected Regions from Settings protected_regions = settings.get('protected_regions', []) # List of Lists of Regions sets = [self.view.get_regions(r) for r in protected_regions] # List of Regions regions = [r for rs in sets for r in rs] # get the line of the region (gutter icon applies to whole line) region_line = self.view.line(region) for r in regions: if r.contains(region) or region_line.contains(r): return True return False
def is_region_protected(self, region): # Load protected Regions from Settings protected_regions = settings.get('protected_regions', []) # List of Lists of Regions sets = [self.view.get_regions(r) for r in protected_regions] # List of Regions regions = [r for rs in sets for r in rs] # get the line of the region (gutter icon applies to whole line) region_line = self.view.line(region) for r in regions: if r.contains(region) or region_line.contains(r): return True return False
def debounce(self, view, event_type): key = (event_type, view.file_name()) this_keypress = time.time() self.latest_keypresses[key] = this_keypress def callback(): latest_keypress = self.latest_keypresses.get(key, None) if this_keypress == latest_keypress: view.run_command('git_gutter') if ST3: set_timeout = sublime.set_timeout_async else: set_timeout = sublime.set_timeout set_timeout(callback, settings.get("debounce_delay"))
def debounce(self, view, event_type): key = (event_type, view.file_name()) this_keypress = time.time() self._latest_keypresses[key] = this_keypress def callback(): latest_keypress = self._latest_keypresses.get(key, None) if this_keypress == latest_keypress: view.run_command('git_gutter') if ST3: set_timeout = sublime.set_timeout_async else: set_timeout = sublime.set_timeout set_timeout(callback, settings.get("debounce_delay"))
def debounce(self, view, event_type, func): if self.non_blocking(): key = (event_type, view.file_name()) this_keypress = time.time() self.latest_keypresses[key] = this_keypress def callback(): latest_keypress = self.latest_keypresses.get(key, None) if this_keypress == latest_keypress: func(view) if ST3: set_timeout = sublime.set_timeout_async else: set_timeout = sublime.set_timeout set_timeout(callback, settings.get("debounce_delay")) else: func(view)
def on_hover(view, point, hover_zone): """Open diff popup if user hovers the mouse over the gutter area. Arguments: view (View): The view which received the event. point (Point): The text position where the mouse hovered hover_zone (int): The context the event was triggered in """ if hover_zone != sublime.HOVER_GUTTER: return # don't let the popup flicker / fight with other packages if view.is_popup_visible(): return if not settings.get('enable_hover_diff_popup'): return view.run_command('git_gutter_diff_popup', { 'point': point, 'flags': sublime.HIDE_ON_MOUSE_MOVE_AWAY })
def set_compare_against(self, compare_against, refresh=False): """Apply a new branch/commit/tag string the view is compared to. If one of the settings 'focus_change_mode' or 'live_mode' is true, the view, is automatically compared by 'on_activate' event when returning from a quick panel and therefore the command 'git_gutter' can be omitted. This assumption can be overridden by 'refresh' for commands that do not show a quick panel. Arguments: compare_against (string): The branch, commit or tag as returned from 'git show-ref' to compare the view against refresh (bool): True to force git diff and update GUI """ settings.set_compare_against(self._git_tree, compare_against) self.invalidate_git_file() if refresh or not any( settings.get(key, True) for key in ('focus_change_mode', 'live_mode')): self.view.run_command('git_gutter') # refresh UI
def _check_ignored_or_untracked(self, contents): show_untracked = settings.get('show_markers_on_untracked_file', False) if show_untracked and self._are_all_lines_added(contents): def bind_ignored_or_untracked(is_ignored): if is_ignored: self._bind_files('ignored') else: def bind_untracked(is_untracked): if is_untracked: self._bind_files('untracked') else: self._lazy_update_ui(contents) self.git_handler.untracked().then(bind_untracked) self.git_handler.ignored().then(bind_ignored_or_untracked) return self._lazy_update_ui(contents)
def debounce(self, view, event_type): """Invoke evaluation of changes after some idle time. Arguments: view (View): The view to perform evaluation for event_type (string): The event identifier """ key = view.id() this_event = time.time() self._latest_events.setdefault(key, {})[event_type] = this_event def callback(): """Run git_gutter command for most recent event.""" if not self.is_view_visible(view): return view_events = self._latest_events.get(key, {}) if this_event == view_events.get(event_type, None): view.run_command('git_gutter', {'event_type': list(view_events.keys())}) self._latest_events[key] = {} # Run command delayed and asynchronous if supported. set_timeout(callback, max(300, settings.get('debounce_delay', 1000)))
def _check_ignored_or_untracked(self, contents): """Check diff result and invoke gutter and status message update. Arguments: contents (tuble): The result of git_handler.diff(), with the information about the modifications of the file. Scheme: (first, last, [inserted], [modified], [deleted]) """ # nothing to update if contents is None: self._busy = False return if not self.git_handler.in_repo(): show_untracked = settings.get('show_markers_on_untracked_file', False) def bind_ignored_or_untracked(is_ignored): if is_ignored: event = 'ignored' self._update_status(event, (0, 0, [], [], [])) if show_untracked: self._bind_files(event) else: def bind_untracked(is_untracked): event = 'untracked' if is_untracked else 'inserted' self._update_status(event, (0, 0, [], [], [])) if show_untracked: self._bind_files(event) self.git_handler.untracked().then(bind_untracked) self.git_handler.ignored().then(bind_ignored_or_untracked) else: self._update_ui(contents)
def non_blocking(self, default=True): return settings.get('non_blocking', default)
def process_diff_line_change(self, line_nr, diff_str): hunk_re = '^@@ \-(\d+),?(\d*) \+(\d+),?(\d*) @@' hunks = re.finditer(hunk_re, diff_str, re.MULTILINE) # we also want to extract the position of the surrounding changes first_change = prev_change = next_change = None for hunk in hunks: start = int(hunk.group(3)) size = int(hunk.group(4) or 1) if first_change is None: first_change = start # special handling to also match the line below deleted # content if size == 0 and line_nr == start + 1: pass # continue if the hunk is before the line elif start + size < line_nr: prev_change = start continue # break if the hunk is after the line elif line_nr < start: break # in the following the line is inside the hunk try: next_hunk = next(hunks) hunk_end = next_hunk.start() next_change = int(next_hunk.group(3)) except: hunk_end = len(diff_str) # if wrap is disable avoid wrapping wrap = settings.get('next_prev_change_wrap', True) if not wrap: if prev_change is None: prev_change = start if next_change is None: next_change = start # if prev change is None set it to the wrap around the # document: prev -> last hunk, next -> first hunk if prev_change is None: try: remaining_hunks = list(hunks) if remaining_hunks: last_hunk = remaining_hunks[-1] prev_change = int(last_hunk.group(3)) elif next_change is not None: prev_change = next_change else: prev_change = start except: prev_change = start if next_change is None: next_change = first_change # extract the content of the hunk hunk_content = diff_str[hunk.start():hunk_end] # store all deleted lines (starting with -) hunk_lines = hunk_content.splitlines()[1:] deleted_lines = [ line[1:] for line in hunk_lines if line.startswith("-") ] added_lines = [ line[1:] for line in hunk_lines if line.startswith("+") ] meta = { "added_lines": added_lines, "first_change": first_change, "next_change": next_change, "prev_change": prev_change } return (deleted_lines, start, size, meta) return ([], -1, -1, {})
def show_diff_popup(view, point, flags=0): if not _MDPOPUPS_INSTALLED: return line = view.rowcol(point)[0] + 1 lines, start, size, meta = ViewCollection.diff_line_change(view, line) if start == -1: return # extract the type of the hunk: removed, modified, (x)or added is_removed = size == 0 is_modified = not is_removed and bool(lines) is_added = not is_removed and not is_modified def navigate(href): if href == "hide": view.hide_popup() elif href == "revert": new_text = "\n".join(lines) # (removed) if there is no text to remove, set the # region to the end of the line, where the hunk starts # and add a new line to the start of the text if is_removed: if start != 0: # set the start and the end to the end of the start line start_point = end_point = view.text_point(start, 0) - 1 # add a leading newline before inserting the text new_text = "\n" + new_text else: # (special handling for deleted at the start of the file) # if we are before the start we need to set the start # to 0 and add the newline behind the text start_point = end_point = 0 new_text = new_text + "\n" # (modified/added) # set the start point to the start of the hunk # and the end point to the end of the hunk else: start_point = view.text_point(start - 1, 0) end_point = view.text_point(start + size - 1, 0) # (modified) if there is text to insert, we # don't want to capture the trailing newline, # because we insert lines without a trailing newline if is_modified and end_point != view.size(): end_point -= 1 replace_param = { "a": start_point, "b": end_point, "text": new_text } view.run_command("git_gutter_replace_text", replace_param) # hide the popup and update the gutter view.hide_popup() view.window().run_command("git_gutter") elif href == "copy": sublime.set_clipboard("\n".join(lines)) copy_message = " ".join(l.strip() for l in lines) sublime.status_message("Copied: " + copy_message) elif href in ["next_change", "prev_change", "first_change"]: next_line = meta.get(href, line) pt = view.text_point(next_line - 1, 0) def show_new_popup(): if view.visible_region().contains(pt): show_diff_popup(view, pt, flags=flags) else: sublime.set_timeout(show_new_popup, 10) view.show_at_center(pt) show_new_popup() # write the symbols/text for each button use_icons = settings.get("diff_popup_use_icon_buttons") # the buttons as a map from the href to the caption/icon button_descriptions = { "hide": chr(0x00D7) if use_icons else "(close)", "copy": chr(0x2398) if use_icons else "(copy)", "revert": chr(0x27F2) if use_icons else "(revert)", "first_change": chr(0x2912) if use_icons else "(first)", "prev_change": chr(0x2191) if use_icons else "(previous)", "next_change": chr(0x2193) if use_icons else "(next)" } def is_button_enabled(k): if k in ["first_change", "next_change", "prev_change"]: return meta.get(k, start) != start return True buttons = {} for k, v in button_descriptions.items(): if is_button_enabled(k): buttons[k] = '[{0}]({1})'.format(v, k) else: buttons[k] = v if not is_added: # (modified/removed) show the button line above the content, # which in git lang = mdpopups.get_language_from_view(view) or "" # strip the indent to the minimal indentation is_tab_indent = any(l.startswith("\t") for l in lines) indent_char = "\t" if is_tab_indent else " " min_indent = min(len(l) - len(l.lstrip(indent_char)) for l in lines) source_content = "\n".join(l[min_indent:] for l in lines) # replace spaces by non-breakable ones to avoid line wrapping source_content = source_content.replace(" ", "\u00A0") button_line = ( '{hide} ' '{first_change} {prev_change} {next_change} ' '{copy} {revert}' .format(**buttons) ) content = ( '{button_line}\n' '``` {lang}\n' '{source_content}\n' '```' .format(**locals()) ) else: # (added) only show the button line without the copy button # (there is nothing to show or copy) button_line = ( '{hide} ' '{first_change} {prev_change} {next_change} ' '{revert}' .format(**buttons) ) content = button_line css = '' if _MD_POPUPS_USE_WRAPPER_CLASS: wrapper_class = "git-gutter" if use_icons: css = 'div.git-gutter a { text-decoration: none; }' else: wrapper_class = "" if use_icons: css = 'a { text-decoration: none; }' location = view.line(point).a window_width = int(view.viewport_extent()[0]) mdpopups.show_popup( view, content, location=location, on_navigate=navigate, wrapper_class=wrapper_class, css=css, flags=flags, max_width=window_width)
def focus_change_mode(self, default=True): return settings.get('focus_change_mode', default)
def live_mode(self, default=True): return settings.get('live_mode', default)
def focus_change_mode(): """Evaluate changes every time a view gets the focus.""" return settings.get('focus_change_mode', True)
def live_mode(): """Evaluate changes every time the view is modified.""" return settings.get('live_mode', True)
def diff_line_change(self, row): """Use cached diff result to extract the changes of a certain line. Arguments: row (int): The row to find the changes for Returns: tuple: The tuple contains 4 items of information about changes around the row with (deleted_lines, start, size, meta). """ hunk_re = r'^@@ \-(\d+),?(\d*) \+(\d+),?(\d*) @@' hunks = re.finditer(hunk_re, self._git_diff_cache, re.MULTILINE) # we also want to extract the position of the surrounding changes first_change = prev_change = next_change = None for hunk in hunks: _, _, start, size = hunk.groups() start = int(start) size = int(size or 1) if first_change is None: first_change = start # special handling to also match the line below deleted # content if size == 0 and row == start + 1: pass # continue if the hunk is before the line elif start + size < row: prev_change = start continue # break if the hunk is after the line elif row < start: break # in the following the line is inside the hunk try: next_hunk = next(hunks) hunk_end = next_hunk.start() next_change = int(next_hunk.group(3)) except: hunk_end = len(self._git_diff_cache) # if wrap is disable avoid wrapping wrap = settings.get('next_prev_change_wrap', True) if not wrap: if prev_change is None: prev_change = start if next_change is None: next_change = start # if prev change is None set it to the wrap around the # document: prev -> last hunk, next -> first hunk if prev_change is None: try: remaining_hunks = list(hunks) if remaining_hunks: last_hunk = remaining_hunks[-1] prev_change = int(last_hunk.group(3)) elif next_change is not None: prev_change = next_change else: prev_change = start except: prev_change = start if next_change is None: next_change = first_change # extract the content of the hunk hunk_content = self._git_diff_cache[hunk.start():hunk_end] # store all deleted lines (starting with -) hunk_lines = hunk_content.splitlines()[1:] deleted_lines = [ line[1:] for line in hunk_lines if line.startswith("-") ] added_lines = [ line[1:] for line in hunk_lines if line.startswith("+") ] meta = { "added_lines": added_lines, "first_change": first_change, "next_change": next_change, "prev_change": prev_change } return (deleted_lines, start, size, meta) return ([], -1, -1, {})
def process_diff_line_change(self, diff_str, line_nr): hunk_re = '^@@ \-(\d+),?(\d*) \+(\d+),?(\d*) @@' hunks = re.finditer(hunk_re, diff_str, re.MULTILINE) # we also want to extract the position of the surrounding changes first_change = prev_change = next_change = None for hunk in hunks: start = int(hunk.group(3)) size = int(hunk.group(4) or 1) if first_change is None: first_change = start # special handling to also match the line below deleted # content if size == 0 and line_nr == start + 1: pass # continue if the hunk is before the line elif start + size < line_nr: prev_change = start continue # break if the hunk is after the line elif line_nr < start: break # in the following the line is inside the hunk try: next_hunk = next(hunks) hunk_end = next_hunk.start() next_change = int(next_hunk.group(3)) except: hunk_end = len(diff_str) # extract the content of the hunk hunk_content = diff_str[hunk.start():hunk_end] # store all deleted lines (starting with -) lines = [line[1:] for line in hunk_content.split("\n")[1:] if line.startswith("-")] # if wrap is disable avoid wrapping wrap = settings.get('next_prev_change_wrap', True) if not wrap: if prev_change is None: prev_change = start if next_change is None: next_change = start # if prev change is None set it to the wrap around the # document: prev -> last hunk, next -> first hunk if prev_change is None: try: remaining_hunks = list(hunks) if remaining_hunks: last_hunk = remaining_hunks[-1] prev_change = int(last_hunk.group(3)) elif next_change is not None: prev_change = next_change else: prev_change = start except: prev_change = start if next_change is None: next_change = first_change meta = { "first_change": first_change, "next_change": next_change, "prev_change": prev_change } return lines, start, size, meta return [], -1, -1, {}