diff --git a/src/diffuse/main.py b/src/diffuse/main.py index f1f3f3a..e1e318f 100644 --- a/src/diffuse/main.py +++ b/src/diffuse/main.py @@ -26,7 +26,7 @@ import stat import webbrowser from gettext import gettext as _ -from typing import Optional +from typing import List, Optional from urllib.parse import urlparse from diffuse import constants @@ -35,9 +35,10 @@ from diffuse.dialogs import AboutDialog, FileChooserDialog, NumericDialog, Searc from diffuse.preferences import Preferences from diffuse.resources import theResources from diffuse.utils import LineEnding +from diffuse.vcs.vcs_interface import VcsInterface from diffuse.vcs.vcs_registry import VcsRegistry -from diffuse.widgets import FileDiffViewerBase -from diffuse.widgets import createMenu, LINE_MODE, CHAR_MODE, ALIGN_MODE +from diffuse.widgets import FileDiffViewerBase, EditMode +from diffuse.widgets import createMenu import gi # type: ignore gi.require_version('GObject', '2.0') @@ -57,7 +58,7 @@ theVCSs = VcsRegistry() # make this a Gtk.EventBox so signals can be connected for MMB and RMB button # presses. class NotebookTab(Gtk.EventBox): - def __init__(self, name, stock): + def __init__(self, name: str, stock: str) -> None: Gtk.EventBox.__init__(self) self.set_visible_window(False) hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) @@ -66,13 +67,16 @@ class NotebookTab(Gtk.EventBox): image.set_from_stock(stock, Gtk.IconSize.MENU) hbox.pack_start(image, False, False, 5) image.show() - self.label = label = Gtk.Label.new(name) + + label = Gtk.Label.new(name) # left justify the widget label.set_xalign(0.0) label.set_yalign(0.5) hbox.pack_start(label, True, True, 0) label.show() - self.button = button = Gtk.Button.new() + self.label = label + + button = Gtk.Button.new() button.set_relief(Gtk.ReliefStyle.NONE) image = Gtk.Image.new() image.set_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.MENU) @@ -81,13 +85,15 @@ class NotebookTab(Gtk.EventBox): button.set_tooltip_text(_('Close Tab')) hbox.pack_start(button, False, False, 0) button.show() + self.button = button + self.add(hbox) hbox.show() - def get_text(self): + def get_text(self) -> str: return self.label.get_text() - def set_text(self, s): + def set_text(self, s: str) -> None: self.label.set_text(s) @@ -99,7 +105,7 @@ class FileInfo: # name of codec used to translate the file contents to unicode text self.encoding = encoding # the VCS object - self.vcs = vcs + self.vcs: VcsInterface = vcs # revision used to retrieve file from the VCS self.revision = revision # alternate text to display instead of the actual file name @@ -147,7 +153,7 @@ class Diffuse(Gtk.Window): self.emit(s) # creates an appropriate title for the pane header - def updateTitle(self): + def updateTitle(self) -> None: ss = [] info = self.info if info.label is not None: @@ -166,7 +172,7 @@ class Diffuse(Gtk.Window): self.emit('title_changed') # set num edits - def setEdits(self, has_edits): + def setEdits(self, has_edits: bool) -> None: if self.has_edits != has_edits: self.has_edits = has_edits self.updateTitle() @@ -198,8 +204,8 @@ class Diffuse(Gtk.Window): self.show_all() # set the cursor label - def updateCursor(self, viewer, f): - if viewer.mode == CHAR_MODE and viewer.current_pane == f: + def updateCursor(self, viewer: FileDiffViewerBase, f: int) -> None: + if viewer.mode == EditMode.CHAR and viewer.current_pane == f: # # TODO: Find a fix for the column bug (resizing issue when editing a line) # j = viewer.current_char # if j > 0: @@ -212,7 +218,7 @@ class Diffuse(Gtk.Window): self.cursor.set_text(s) # set the format label - def setFormat(self, s): + def setFormat(self, s: LineEnding) -> None: v = [] if s & LineEnding.DOS_FORMAT: v.append('DOS') @@ -223,19 +229,19 @@ class Diffuse(Gtk.Window): self.format.set_text('/'.join(v)) # set the format label - def setEncoding(self, s): + def setEncoding(self, s: str) -> None: if s is None: s = '' self.encoding.set_text(s) - def __init__(self, n, prefs, title): - FileDiffViewerBase.__init__(self, n, prefs) + def __init__(self, n: int, prefs: Preferences, title: str) -> None: + super().__init__(n, prefs) self.title = title - self.status = '' + self.status: Optional[str] = '' - self.headers = [] - self.footers = [] + self.headers: List[Diffuse.FileDiffViewer.PaneHeader] = [] + self.footers: List[Diffuse.FileDiffViewer.PaneFooter] = [] for i in range(n): # pane header w = Diffuse.FileDiffViewer.PaneHeader() @@ -272,7 +278,7 @@ class Diffuse(Gtk.Window): # convenience method to request confirmation before loading a file if # it will cause existing edits to be lost - def loadFromInfo(self, f, info): + def loadFromInfo(self, f: int, info: FileInfo) -> None: if self.headers[f].has_edits: # warn users of any unsaved changes they might lose dialog = Gtk.MessageDialog( @@ -358,7 +364,7 @@ class Diffuse(Gtk.Window): # load a new file into pane 'f' # 'info' indicates the name of the file and how to retrieve it from the # version control system if applicable - def load(self, f, info): + def load(self, f: int, info: FileInfo) -> None: name = info.name encoding = info.encoding stat = None @@ -371,7 +377,7 @@ class Diffuse(Gtk.Window): if rev is None: # load the contents of a plain file with open(name, 'rb') as fd: - s = fd.read() + contents = fd.read() # get the file's modification times so we can detect changes stat = os.stat(name) else: @@ -379,12 +385,12 @@ class Diffuse(Gtk.Window): raise IOError('Not under version control.') fullname = os.path.abspath(name) # retrieve the revision from the version control system - s = info.vcs.getRevision(self.prefs, fullname, rev) + contents = info.vcs.getRevision(self.prefs, fullname, rev) # convert file contents to unicode if encoding is None: - s, encoding = self.prefs.convertToUnicode(s) + s, encoding = self.prefs.convertToUnicode(contents) else: - s = str(s, encoding=encoding) + s = str(contents, encoding=encoding) ss = utils.splitlines(s) except (IOError, OSError, UnicodeDecodeError, LookupError): # FIXME: this can occur before the toplevel window is drawn @@ -408,7 +414,7 @@ class Diffuse(Gtk.Window): self.setSyntax(syntax) # load a new file into pane 'f' - def open_file(self, f, reload=False): + def open_file(self, f: int, reload: bool = False) -> None: h = self.headers[f] info = h.info if not reload: @@ -487,7 +493,7 @@ class Diffuse(Gtk.Window): pass # save contents of pane 'f' to file - def save_file(self, f, save_as=False): + def save_file(self, f: int, save_as: bool = False) -> bool: h = self.headers[f] info = h.info name, encoding, rev, label = info.name, info.encoding, info.revision, info.label @@ -642,15 +648,15 @@ class Diffuse(Gtk.Window): self.updateStatus() # update the viewer's current status message - def updateStatus(self): - if self.mode == LINE_MODE: + def updateStatus(self) -> None: + if self.mode == EditMode.LINE: s = _( 'Press the enter key or double click to edit. Press the space bar or use the ' 'RMB menu to manually align.' ) - elif self.mode == CHAR_MODE: + elif self.mode == EditMode.CHAR: s = _('Press the escape key to finish editing.') - elif self.mode == ALIGN_MODE: + elif self.mode == EditMode.ALIGN: s = _( 'Select target line and press the space bar to align. Press the escape key to ' 'cancel.' @@ -661,7 +667,7 @@ class Diffuse(Gtk.Window): self.emit('status_changed', s) # gets the status bar text - def getStatus(self): + def getStatus(self) -> Optional[str]: return self.status # callback to display the cursor in a pane @@ -674,7 +680,7 @@ class Diffuse(Gtk.Window): self.footers[f].setFormat(fmt) def __init__(self, rc_dir): - Gtk.Window.__init__(self, type=Gtk.WindowType.TOPLEVEL) + super().__init__(type=Gtk.WindowType.TOPLEVEL) self.prefs = Preferences(os.path.join(rc_dir, 'prefs')) # number of created viewers (used to label some tabs) @@ -702,8 +708,8 @@ class Diffuse(Gtk.Window): self.connect('window-state-event', self.window_state_cb) # search history is application wide - self.search_pattern = None - self.search_history = [] + self.search_pattern: Optional[str] = None + self.search_history: List[str] = [] self.connect('delete-event', self.delete_cb) accel_group = Gtk.AccelGroup() @@ -968,7 +974,7 @@ class Diffuse(Gtk.Window): ) # load state information that should persist across sessions - def loadState(self, statepath): + def loadState(self, statepath: str) -> None: if os.path.isfile(statepath): try: f = open(statepath, 'r') @@ -998,7 +1004,7 @@ class Diffuse(Gtk.Window): self.maximize() # save state information that should persist across sessions - def saveState(self, statepath): + def saveState(self, statepath: str) -> None: try: ss = [] for k, v in self.bool_state.items(): @@ -1025,7 +1031,7 @@ class Diffuse(Gtk.Window): # returns True if the list of viewers can be closed. The user will be # given a chance to save any modified files before this method completes. - def confirmCloseViewers(self, viewers): + def confirmCloseViewers(self, viewers: List[FileDiffViewer]) -> bool: # make a list of modified files model = Gtk.ListStore.new([ GObject.TYPE_BOOLEAN, @@ -1144,12 +1150,12 @@ class Diffuse(Gtk.Window): menu.popup(None, None, None, event.button, event.time) # update window's title - def updateTitle(self, viewer): + def updateTitle(self, viewer: FileDiffViewer) -> None: title = self.notebook.get_tab_label(viewer).get_text() self.set_title(f'{title} - {constants.APP_NAME}') # update the message in the status bar - def setStatus(self, s): + def setStatus(self, s: Optional[str]) -> None: sb = self.statusbar context = sb.get_context_id('Message') sb.pop(context) @@ -1358,7 +1364,7 @@ class Diffuse(Gtk.Window): return True # returns the currently focused viewer - def getCurrentViewer(self) -> Optional[Gtk.Widget]: + def getCurrentViewer(self) -> FileDiffViewer: return self.notebook.get_nth_page(self.notebook.get_current_page()) # callback for the open file menu item @@ -1504,7 +1510,7 @@ class Diffuse(Gtk.Window): # request search parameters if force=True and then perform a search in the # current viewer pane - def find(self, force, reverse): + def find(self, force: bool, reverse: bool) -> None: viewer = self.getCurrentViewer() if force or self.search_pattern is None: # construct search dialog diff --git a/src/diffuse/preferences.py b/src/diffuse/preferences.py index 4b0553e..5a51744 100644 --- a/src/diffuse/preferences.py +++ b/src/diffuse/preferences.py @@ -24,7 +24,7 @@ import shlex import sys from gettext import gettext as _ -from typing import List +from typing import Any, Dict, Final, List, Optional, Tuple from diffuse import constants from diffuse import utils @@ -36,16 +36,16 @@ from gi.repository import Gtk # type: ignore # noqa: E402 # class to store preferences and construct a dialogue for manipulating them class Preferences: - def __init__(self, path): - self.bool_prefs = {} - self.int_prefs = {} - self.string_prefs = {} - self.int_prefs_min = {} - self.int_prefs_max = {} - self.string_prefs_enums = {} + def __init__(self, path: str) -> None: + self.path = path + self.bool_prefs: Dict[str, bool] = {} + self.string_prefs: Dict[str, str] = {} + self.int_prefs: Dict[str, int] = {} + self.int_prefs_min: Dict[str, int] = {} + self.int_prefs_max: Dict[str, int] = {} # find available encodings - self.encodings = sorted(set(encodings.aliases.aliases.values())) + self.encodings: List[Optional[str]] = sorted(set(encodings.aliases.aliases.values())) if utils.isWindows(): svk_bin = 'svk.bat' @@ -54,7 +54,7 @@ class Preferences: auto_detect_codecs = ['utf_8', 'utf_16', 'latin_1'] e = utils.norm_encoding(sys.getfilesystemencoding()) - if e not in auto_detect_codecs: + if e is not None and e not in auto_detect_codecs: # insert after UTF-8 as the default encoding may prevent UTF-8 from # being tried auto_detect_codecs.insert(2, e) @@ -124,7 +124,7 @@ class Preferences: ] # conditions used to determine if a preference should be greyed out - self.disable_when = { + self.disable_when: Final[Dict[str, Tuple[str, bool]]] = { 'display_right_margin': ('display_show_right_margin', False), 'display_ignore_whitespace_changes': ('display_ignore_whitespace', True), 'display_ignore_blanklines': ('display_ignore_whitespace', True), @@ -165,9 +165,9 @@ class Preferences: _('Version control system search order') ] ] - vcs_folders_template = ['FolderSet'] + vcs_folders_template: List[Any] = ['FolderSet'] for key, name, cmd in vcs: - temp = ['List'] + temp: List[Any] = ['List'] if key == 'rcs': # RCS uses multiple commands temp.extend([['File', key + '_bin_co', 'co', _('"co" command')], @@ -196,8 +196,8 @@ class Preferences: self.default_bool_prefs = self.bool_prefs.copy() self.default_int_prefs = self.int_prefs.copy() self.default_string_prefs = self.string_prefs.copy() + # load the user's preferences - self.path = path if os.path.isfile(self.path): try: with open(self.path, 'r', encoding='utf-8') as f: @@ -255,16 +255,16 @@ class Preferences: # display the dialogue and update the preference values if the accept # button was pressed - def runDialog(self, parent): + def runDialog(self, parent: Gtk.Widget) -> None: dialog = Gtk.Dialog(_('Preferences'), parent=parent, destroy_with_parent=True) dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT) dialog.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK) - widgets = {} + widgets: Dict[str, Gtk.Widget] = {} w = self._buildPrefsDialog(parent, widgets, self.template) # disable any preferences than are not relevant - for k, v in self.disable_when.items(): - p, t = v + for k, tuple_value in self.disable_when.items(): + p, t = tuple_value if widgets[p].get_active() == t: widgets[k].set_sensitive(False) dialog.vbox.add(w) @@ -280,15 +280,15 @@ class Preferences: self.string_prefs[k] = utils.null_to_empty(widgets[k].get_text()) try: ss = [] - for k, v in self.bool_prefs.items(): - if v != self.default_bool_prefs[k]: - ss.append(f'{k} {v}\n') - for k, v in self.int_prefs.items(): - if v != self.default_int_prefs[k]: - ss.append(f'{k} {v}\n') - for k, v in self.string_prefs.items(): - if v != self.default_string_prefs[k]: - v_escaped = v.replace('\\', '\\\\').replace('"', '\\"') + for k, bool_value in self.bool_prefs.items(): + if bool_value != self.default_bool_prefs[k]: + ss.append(f'{k} {bool_value}\n') + for k, int_value in self.int_prefs.items(): + if int_value != self.default_int_prefs[k]: + ss.append(f'{k} {int_value}\n') + for k, str_value in self.string_prefs.items(): + if str_value != self.default_string_prefs[k]: + v_escaped = str_value.replace('\\', '\\\\').replace('"', '\\"') ss.append(f'{k} "{v_escaped}"\n') ss.sort() with open(self.path, 'w', encoding='utf-8') as f: @@ -384,7 +384,7 @@ class Preferences: def setString(self, name: str, value: str) -> None: self.string_prefs[name] = value - def getEncodings(self): + def getEncodings(self) -> List[Optional[str]]: return self.encodings def _getDefaultEncodings(self) -> List[str]: @@ -439,7 +439,7 @@ class Preferences: # text entry widget with a button to help pick file names class _FileEntry(Gtk.Box): - def __init__(self, parent, title): + def __init__(self, parent: Gtk.Widget, title: str) -> None: Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) self.toplevel = parent self.title = title @@ -456,7 +456,7 @@ class _FileEntry(Gtk.Box): button.show() # action performed when the pick file button is pressed - def chooseFile(self, widget): + def chooseFile(self, widget: Gtk.Widget) -> None: dialog = Gtk.FileChooserDialog( self.title, self.toplevel, diff --git a/src/diffuse/resources.py b/src/diffuse/resources.py index ceb14e8..8a16fe7 100644 --- a/src/diffuse/resources.py +++ b/src/diffuse/resources.py @@ -31,7 +31,7 @@ import shlex from distutils import util from gettext import gettext as _ -from typing import Final +from typing import Dict, Final, List, Optional, Pattern, Set, Tuple from diffuse import utils @@ -45,134 +45,133 @@ class Resources: # default keybindings self.keybindings = {} self.keybindings_lookup = {} - set_binding = self.setKeyBinding - set_binding('menu', 'open_file', 'Ctrl+o') - set_binding('menu', 'open_file_in_new_tab', 'Ctrl+t') - set_binding('menu', 'open_modified_files', 'Shift+Ctrl+O') - set_binding('menu', 'open_commit', 'Shift+Ctrl+T') - set_binding('menu', 'reload_file', 'Shift+Ctrl+R') - set_binding('menu', 'save_file', 'Ctrl+s') - set_binding('menu', 'save_file_as', 'Shift+Ctrl+A') - set_binding('menu', 'save_all', 'Shift+Ctrl+S') - set_binding('menu', 'new_2_way_file_merge', 'Ctrl+2') - set_binding('menu', 'new_3_way_file_merge', 'Ctrl+3') - set_binding('menu', 'new_n_way_file_merge', 'Ctrl+4') - set_binding('menu', 'close_tab', 'Ctrl+w') - set_binding('menu', 'undo_close_tab', 'Shift+Ctrl+W') - set_binding('menu', 'quit', 'Ctrl+q') - set_binding('menu', 'undo', 'Ctrl+z') - set_binding('menu', 'redo', 'Shift+Ctrl+Z') - set_binding('menu', 'cut', 'Ctrl+x') - set_binding('menu', 'copy', 'Ctrl+c') - set_binding('menu', 'paste', 'Ctrl+v') - set_binding('menu', 'select_all', 'Ctrl+a') - set_binding('menu', 'clear_edits', 'Ctrl+r') - set_binding('menu', 'dismiss_all_edits', 'Ctrl+d') - set_binding('menu', 'find', 'Ctrl+f') - set_binding('menu', 'find_next', 'Ctrl+g') - set_binding('menu', 'find_previous', 'Shift+Ctrl+G') - set_binding('menu', 'go_to_line', 'Shift+Ctrl+L') - set_binding('menu', 'realign_all', 'Ctrl+l') - set_binding('menu', 'isolate', 'Ctrl+i') - set_binding('menu', 'first_difference', 'Shift+Ctrl+Up') - set_binding('menu', 'previous_difference', 'Ctrl+Up') - set_binding('menu', 'next_difference', 'Ctrl+Down') - set_binding('menu', 'last_difference', 'Shift+Ctrl+Down') - set_binding('menu', 'first_tab', 'Shift+Ctrl+Page_Up') - set_binding('menu', 'previous_tab', 'Ctrl+Page_Up') - set_binding('menu', 'next_tab', 'Ctrl+Page_Down') - set_binding('menu', 'last_tab', 'Shift+Ctrl+Page_Down') - set_binding('menu', 'shift_pane_right', 'Shift+Ctrl+parenright') - set_binding('menu', 'shift_pane_left', 'Shift+Ctrl+parenleft') - set_binding('menu', 'convert_to_upper_case', 'Ctrl+u') - set_binding('menu', 'convert_to_lower_case', 'Shift+Ctrl+U') - set_binding('menu', 'sort_lines_in_ascending_order', 'Ctrl+y') - set_binding('menu', 'sort_lines_in_descending_order', 'Shift+Ctrl+Y') - set_binding('menu', 'remove_trailing_white_space', 'Ctrl+k') - set_binding('menu', 'convert_tabs_to_spaces', 'Ctrl+b') - set_binding('menu', 'convert_leading_spaces_to_tabs', 'Shift+Ctrl+B') - set_binding('menu', 'increase_indenting', 'Shift+Ctrl+greater') - set_binding('menu', 'decrease_indenting', 'Shift+Ctrl+less') - set_binding('menu', 'convert_to_dos', 'Shift+Ctrl+E') - set_binding('menu', 'convert_to_mac', 'Shift+Ctrl+C') - set_binding('menu', 'convert_to_unix', 'Ctrl+e') - set_binding('menu', 'copy_selection_right', 'Shift+Ctrl+Right') - set_binding('menu', 'copy_selection_left', 'Shift+Ctrl+Left') - set_binding('menu', 'copy_left_into_selection', 'Ctrl+Right') - set_binding('menu', 'copy_right_into_selection', 'Ctrl+Left') - set_binding('menu', 'merge_from_left_then_right', 'Ctrl+m') - set_binding('menu', 'merge_from_right_then_left', 'Shift+Ctrl+M') - set_binding('menu', 'help_contents', 'F1') - set_binding('line_mode', 'enter_align_mode', 'space') - set_binding('line_mode', 'enter_character_mode', 'Return') - set_binding('line_mode', 'enter_character_mode', 'KP_Enter') - set_binding('line_mode', 'first_line', 'Home') - set_binding('line_mode', 'first_line', 'g') - set_binding('line_mode', 'extend_first_line', 'Shift+Home') - set_binding('line_mode', 'last_line', 'End') - set_binding('line_mode', 'last_line', 'Shift+G') - set_binding('line_mode', 'extend_last_line', 'Shift+End') - set_binding('line_mode', 'up', 'Up') - set_binding('line_mode', 'up', 'k') - set_binding('line_mode', 'extend_up', 'Shift+Up') - set_binding('line_mode', 'extend_up', 'Shift+K') - set_binding('line_mode', 'down', 'Down') - set_binding('line_mode', 'down', 'j') - set_binding('line_mode', 'extend_down', 'Shift+Down') - set_binding('line_mode', 'extend_down', 'Shift+J') - set_binding('line_mode', 'left', 'Left') - set_binding('line_mode', 'left', 'h') - set_binding('line_mode', 'extend_left', 'Shift+Left') - set_binding('line_mode', 'right', 'Right') - set_binding('line_mode', 'right', 'l') - set_binding('line_mode', 'extend_right', 'Shift+Right') - set_binding('line_mode', 'page_up', 'Page_Up') - set_binding('line_mode', 'page_up', 'Ctrl+u') - set_binding('line_mode', 'extend_page_up', 'Shift+Page_Up') - set_binding('line_mode', 'extend_page_up', 'Shift+Ctrl+U') - set_binding('line_mode', 'page_down', 'Page_Down') - set_binding('line_mode', 'page_down', 'Ctrl+d') - set_binding('line_mode', 'extend_page_down', 'Shift+Page_Down') - set_binding('line_mode', 'extend_page_down', 'Shift+Ctrl+D') - set_binding('line_mode', 'delete_text', 'BackSpace') - set_binding('line_mode', 'delete_text', 'Delete') - set_binding('line_mode', 'delete_text', 'x') - set_binding('line_mode', 'clear_edits', 'r') - set_binding('line_mode', 'isolate', 'i') - set_binding('line_mode', 'first_difference', 'Ctrl+Home') - set_binding('line_mode', 'first_difference', 'Shift+P') - set_binding('line_mode', 'previous_difference', 'p') - set_binding('line_mode', 'next_difference', 'n') - set_binding('line_mode', 'last_difference', 'Ctrl+End') - set_binding('line_mode', 'last_difference', 'Shift+N') - # set_binding('line_mode', 'copy_selection_right', 'Shift+L') - # set_binding('line_mode', 'copy_selection_left', 'Shift+H') - set_binding('line_mode', 'copy_left_into_selection', 'Shift+L') - set_binding('line_mode', 'copy_right_into_selection', 'Shift+H') - set_binding('line_mode', 'merge_from_left_then_right', 'm') - set_binding('line_mode', 'merge_from_right_then_left', 'Shift+M') - set_binding('align_mode', 'enter_line_mode', 'Escape') - set_binding('align_mode', 'align', 'space') - set_binding('align_mode', 'enter_character_mode', 'Return') - set_binding('align_mode', 'enter_character_mode', 'KP_Enter') - set_binding('align_mode', 'first_line', 'g') - set_binding('align_mode', 'last_line', 'Shift+G') - set_binding('align_mode', 'up', 'Up') - set_binding('align_mode', 'up', 'k') - set_binding('align_mode', 'down', 'Down') - set_binding('align_mode', 'down', 'j') - set_binding('align_mode', 'left', 'Left') - set_binding('align_mode', 'left', 'h') - set_binding('align_mode', 'right', 'Right') - set_binding('align_mode', 'right', 'l') - set_binding('align_mode', 'page_up', 'Page_Up') - set_binding('align_mode', 'page_up', 'Ctrl+u') - set_binding('align_mode', 'page_down', 'Page_Down') - set_binding('align_mode', 'page_down', 'Ctrl+d') - set_binding('character_mode', 'enter_line_mode', 'Escape') + self.setKeyBinding('menu', 'open_file', 'Ctrl+o') + self.setKeyBinding('menu', 'open_file_in_new_tab', 'Ctrl+t') + self.setKeyBinding('menu', 'open_modified_files', 'Shift+Ctrl+O') + self.setKeyBinding('menu', 'open_commit', 'Shift+Ctrl+T') + self.setKeyBinding('menu', 'reload_file', 'Shift+Ctrl+R') + self.setKeyBinding('menu', 'save_file', 'Ctrl+s') + self.setKeyBinding('menu', 'save_file_as', 'Shift+Ctrl+A') + self.setKeyBinding('menu', 'save_all', 'Shift+Ctrl+S') + self.setKeyBinding('menu', 'new_2_way_file_merge', 'Ctrl+2') + self.setKeyBinding('menu', 'new_3_way_file_merge', 'Ctrl+3') + self.setKeyBinding('menu', 'new_n_way_file_merge', 'Ctrl+4') + self.setKeyBinding('menu', 'close_tab', 'Ctrl+w') + self.setKeyBinding('menu', 'undo_close_tab', 'Shift+Ctrl+W') + self.setKeyBinding('menu', 'quit', 'Ctrl+q') + self.setKeyBinding('menu', 'undo', 'Ctrl+z') + self.setKeyBinding('menu', 'redo', 'Shift+Ctrl+Z') + self.setKeyBinding('menu', 'cut', 'Ctrl+x') + self.setKeyBinding('menu', 'copy', 'Ctrl+c') + self.setKeyBinding('menu', 'paste', 'Ctrl+v') + self.setKeyBinding('menu', 'select_all', 'Ctrl+a') + self.setKeyBinding('menu', 'clear_edits', 'Ctrl+r') + self.setKeyBinding('menu', 'dismiss_all_edits', 'Ctrl+d') + self.setKeyBinding('menu', 'find', 'Ctrl+f') + self.setKeyBinding('menu', 'find_next', 'Ctrl+g') + self.setKeyBinding('menu', 'find_previous', 'Shift+Ctrl+G') + self.setKeyBinding('menu', 'go_to_line', 'Shift+Ctrl+L') + self.setKeyBinding('menu', 'realign_all', 'Ctrl+l') + self.setKeyBinding('menu', 'isolate', 'Ctrl+i') + self.setKeyBinding('menu', 'first_difference', 'Shift+Ctrl+Up') + self.setKeyBinding('menu', 'previous_difference', 'Ctrl+Up') + self.setKeyBinding('menu', 'next_difference', 'Ctrl+Down') + self.setKeyBinding('menu', 'last_difference', 'Shift+Ctrl+Down') + self.setKeyBinding('menu', 'first_tab', 'Shift+Ctrl+Page_Up') + self.setKeyBinding('menu', 'previous_tab', 'Ctrl+Page_Up') + self.setKeyBinding('menu', 'next_tab', 'Ctrl+Page_Down') + self.setKeyBinding('menu', 'last_tab', 'Shift+Ctrl+Page_Down') + self.setKeyBinding('menu', 'shift_pane_right', 'Shift+Ctrl+parenright') + self.setKeyBinding('menu', 'shift_pane_left', 'Shift+Ctrl+parenleft') + self.setKeyBinding('menu', 'convert_to_upper_case', 'Ctrl+u') + self.setKeyBinding('menu', 'convert_to_lower_case', 'Shift+Ctrl+U') + self.setKeyBinding('menu', 'sort_lines_in_ascending_order', 'Ctrl+y') + self.setKeyBinding('menu', 'sort_lines_in_descending_order', 'Shift+Ctrl+Y') + self.setKeyBinding('menu', 'remove_trailing_white_space', 'Ctrl+k') + self.setKeyBinding('menu', 'convert_tabs_to_spaces', 'Ctrl+b') + self.setKeyBinding('menu', 'convert_leading_spaces_to_tabs', 'Shift+Ctrl+B') + self.setKeyBinding('menu', 'increase_indenting', 'Shift+Ctrl+greater') + self.setKeyBinding('menu', 'decrease_indenting', 'Shift+Ctrl+less') + self.setKeyBinding('menu', 'convert_to_dos', 'Shift+Ctrl+E') + self.setKeyBinding('menu', 'convert_to_mac', 'Shift+Ctrl+C') + self.setKeyBinding('menu', 'convert_to_unix', 'Ctrl+e') + self.setKeyBinding('menu', 'copy_selection_right', 'Shift+Ctrl+Right') + self.setKeyBinding('menu', 'copy_selection_left', 'Shift+Ctrl+Left') + self.setKeyBinding('menu', 'copy_left_into_selection', 'Ctrl+Right') + self.setKeyBinding('menu', 'copy_right_into_selection', 'Ctrl+Left') + self.setKeyBinding('menu', 'merge_from_left_then_right', 'Ctrl+m') + self.setKeyBinding('menu', 'merge_from_right_then_left', 'Shift+Ctrl+M') + self.setKeyBinding('menu', 'help_contents', 'F1') + self.setKeyBinding('line_mode', 'enter_align_mode', 'space') + self.setKeyBinding('line_mode', 'enter_character_mode', 'Return') + self.setKeyBinding('line_mode', 'enter_character_mode', 'KP_Enter') + self.setKeyBinding('line_mode', 'first_line', 'Home') + self.setKeyBinding('line_mode', 'first_line', 'g') + self.setKeyBinding('line_mode', 'extend_first_line', 'Shift+Home') + self.setKeyBinding('line_mode', 'last_line', 'End') + self.setKeyBinding('line_mode', 'last_line', 'Shift+G') + self.setKeyBinding('line_mode', 'extend_last_line', 'Shift+End') + self.setKeyBinding('line_mode', 'up', 'Up') + self.setKeyBinding('line_mode', 'up', 'k') + self.setKeyBinding('line_mode', 'extend_up', 'Shift+Up') + self.setKeyBinding('line_mode', 'extend_up', 'Shift+K') + self.setKeyBinding('line_mode', 'down', 'Down') + self.setKeyBinding('line_mode', 'down', 'j') + self.setKeyBinding('line_mode', 'extend_down', 'Shift+Down') + self.setKeyBinding('line_mode', 'extend_down', 'Shift+J') + self.setKeyBinding('line_mode', 'left', 'Left') + self.setKeyBinding('line_mode', 'left', 'h') + self.setKeyBinding('line_mode', 'extend_left', 'Shift+Left') + self.setKeyBinding('line_mode', 'right', 'Right') + self.setKeyBinding('line_mode', 'right', 'l') + self.setKeyBinding('line_mode', 'extend_right', 'Shift+Right') + self.setKeyBinding('line_mode', 'page_up', 'Page_Up') + self.setKeyBinding('line_mode', 'page_up', 'Ctrl+u') + self.setKeyBinding('line_mode', 'extend_page_up', 'Shift+Page_Up') + self.setKeyBinding('line_mode', 'extend_page_up', 'Shift+Ctrl+U') + self.setKeyBinding('line_mode', 'page_down', 'Page_Down') + self.setKeyBinding('line_mode', 'page_down', 'Ctrl+d') + self.setKeyBinding('line_mode', 'extend_page_down', 'Shift+Page_Down') + self.setKeyBinding('line_mode', 'extend_page_down', 'Shift+Ctrl+D') + self.setKeyBinding('line_mode', 'delete_text', 'BackSpace') + self.setKeyBinding('line_mode', 'delete_text', 'Delete') + self.setKeyBinding('line_mode', 'delete_text', 'x') + self.setKeyBinding('line_mode', 'clear_edits', 'r') + self.setKeyBinding('line_mode', 'isolate', 'i') + self.setKeyBinding('line_mode', 'first_difference', 'Ctrl+Home') + self.setKeyBinding('line_mode', 'first_difference', 'Shift+P') + self.setKeyBinding('line_mode', 'previous_difference', 'p') + self.setKeyBinding('line_mode', 'next_difference', 'n') + self.setKeyBinding('line_mode', 'last_difference', 'Ctrl+End') + self.setKeyBinding('line_mode', 'last_difference', 'Shift+N') + # self.setKeyBinding('line_mode', 'copy_selection_right', 'Shift+L') + # self.setKeyBinding('line_mode', 'copy_selection_left', 'Shift+H') + self.setKeyBinding('line_mode', 'copy_left_into_selection', 'Shift+L') + self.setKeyBinding('line_mode', 'copy_right_into_selection', 'Shift+H') + self.setKeyBinding('line_mode', 'merge_from_left_then_right', 'm') + self.setKeyBinding('line_mode', 'merge_from_right_then_left', 'Shift+M') + self.setKeyBinding('align_mode', 'enter_line_mode', 'Escape') + self.setKeyBinding('align_mode', 'align', 'space') + self.setKeyBinding('align_mode', 'enter_character_mode', 'Return') + self.setKeyBinding('align_mode', 'enter_character_mode', 'KP_Enter') + self.setKeyBinding('align_mode', 'first_line', 'g') + self.setKeyBinding('align_mode', 'last_line', 'Shift+G') + self.setKeyBinding('align_mode', 'up', 'Up') + self.setKeyBinding('align_mode', 'up', 'k') + self.setKeyBinding('align_mode', 'down', 'Down') + self.setKeyBinding('align_mode', 'down', 'j') + self.setKeyBinding('align_mode', 'left', 'Left') + self.setKeyBinding('align_mode', 'left', 'h') + self.setKeyBinding('align_mode', 'right', 'Right') + self.setKeyBinding('align_mode', 'right', 'l') + self.setKeyBinding('align_mode', 'page_up', 'Page_Up') + self.setKeyBinding('align_mode', 'page_up', 'Ctrl+u') + self.setKeyBinding('align_mode', 'page_down', 'Page_Down') + self.setKeyBinding('align_mode', 'page_down', 'Ctrl+d') + self.setKeyBinding('character_mode', 'enter_line_mode', 'Escape') # default colours - self.colours = { + self.colours: Dict[str, _Colour] = { 'alignment': _Colour(1.0, 1.0, 0.0), 'character_selection': _Colour(0.7, 0.7, 1.0), 'cursor': _Colour(0.0, 0.0, 0.0), @@ -192,7 +191,7 @@ class Resources: } # default floats - self.floats = { + self.floats: Dict[str, float] = { 'alignment_opacity': 1.0, 'character_difference_opacity': 0.4, 'character_selection_opacity': 0.4, @@ -202,29 +201,29 @@ class Resources: } # default options - self.options = { + self.options: Dict[str, str] = { 'log_print_output': 'False', 'log_print_stack': 'False', 'use_flatpak': 'False' } # default strings - self.strings = {} + self.strings: Dict[str, str] = {} # syntax highlighting support - self.syntaxes = {} - self.syntax_file_patterns = {} - self.syntax_magic_patterns = {} - self.current_syntax = None + self.syntaxes: Dict[str, _SyntaxParser] = {} + self.syntax_file_patterns: Dict[str, Pattern] = {} + self.syntax_magic_patterns: Dict[str, Pattern] = {} + self.current_syntax: Optional[_SyntaxParser] = None # list of imported resources files (we only import each file once) - self.resource_files = set() + self.resource_files: Set[str] = set() # special string resources self.setDifferenceColours('difference_1 difference_2 difference_3') # keyboard action processing - def setKeyBinding(self, ctx, s, v): + def setKeyBinding(self, ctx: str, s: str, v: str) -> None: action_tuple = (ctx, s) modifiers = Gdk.ModifierType(0) key = None @@ -282,7 +281,7 @@ class Resources: return [] # colours used for indicating differences - def setDifferenceColours(self, s): + def setDifferenceColours(self, s: str) -> None: colours = s.split() if len(colours) > 0: self.difference_colours = colours @@ -322,7 +321,7 @@ class Resources: return util.strtobool(self.getOption(option)) # string resources - def getString(self, symbol): + def getString(self, symbol: str) -> str: try: return self.strings[symbol] except KeyError: @@ -336,7 +335,7 @@ class Resources: def getSyntax(self, name): return self.syntaxes.get(name, None) - def guessSyntaxForFile(self, name, ss): + def guessSyntaxForFile(self, name: str, ss: List[str]) -> Optional[str]: name = os.path.basename(name) for key, pattern in self.syntax_file_patterns.items(): if pattern.search(name): @@ -350,7 +349,7 @@ class Resources: return None # parse resource files - def parse(self, file_name): + def parse(self, file_name: str) -> None: # only process files once if file_name in self.resource_files: return @@ -514,12 +513,12 @@ class Resources: file=file_name ) utils.logError(f'{error_msg}: {e.msg}') - except ValueError as e: + except ValueError: error_msg = _('Value error at line {line} of {file}').format( line=i + 1, file=file_name ) - utils.logError(f'{error_msg}: {e.msg}') + utils.logError(error_msg) except re.error: error_msg = _('Regex error at line {line} of {file}.') utils.logError(error_msg.format(line=i + 1, file=file_name)) @@ -558,7 +557,7 @@ class _Colour: class _SyntaxParser: # create a new state machine that begins in initial_state and classifies # all characters not matched by the patterns as default_token_type - def __init__(self, initial_state, default_token_type): + def __init__(self, initial_state: str, default_token_type: str) -> None: # initial state for the state machine when parsing a new file self.initial_state = initial_state # default classification of characters that are not explicitly matched @@ -567,30 +566,33 @@ class _SyntaxParser: # mappings from a state to a list of (pattern, token_type, next_state) # tuples indicating the new state for the state machine when 'pattern' # is matched and how to classify the matched characters - self.transitions_lookup = {initial_state: []} + self.transitions_lookup: Dict[str, List[Tuple[Pattern, str, str]]] = {initial_state: []} # Adds a new edge to the finite state machine from prev_state to # next_state. Characters will be identified as token_type when pattern is # matched. Any newly referenced state will be added. Patterns for edges # leaving a state will be tested in the order they were added to the finite # state machine. - def addPattern(self, prev_state, next_state, token_type, pattern): - lookup = self.transitions_lookup + def addPattern( + self, + prev_state: str, + next_state: str, + token_type: str, + pattern: Pattern) -> None: for state in prev_state, next_state: - if state not in lookup: - lookup[state] = [] - lookup[prev_state].append([pattern, token_type, next_state]) + if state not in self.transitions_lookup: + self.transitions_lookup[state] = [] + self.transitions_lookup[prev_state].append((pattern, token_type, next_state)) # given a string and an initial state, identify the final state and tokens def parse(self, state_name, s): - lookup = self.transitions_lookup - transitions, blocks, start = lookup[state_name], [], 0 + transitions, blocks, start = self.transitions_lookup[state_name], [], 0 while start < len(s): for pattern, token_type, next_state in transitions: m = pattern.match(s, start) if m is not None: end, state_name = m.span()[1], next_state - transitions = lookup[state_name] + transitions = self.transitions_lookup[state_name] break else: end, token_type = start + 1, self.default_token_type @@ -602,4 +604,4 @@ class _SyntaxParser: return state_name, blocks -theResources: Final[Resources] = Resources() +theResources: Final = Resources() diff --git a/src/diffuse/utils.py b/src/diffuse/utils.py index 9d71012..8b81a58 100644 --- a/src/diffuse/utils.py +++ b/src/diffuse/utils.py @@ -28,6 +28,7 @@ from gettext import gettext as _ from typing import Final, List, Optional, TextIO from diffuse import constants +from diffuse.preferences import Preferences from diffuse.resources import theResources import gi # type: ignore @@ -37,7 +38,7 @@ from gi.repository import Gtk # type: ignore # noqa: E402 # convenience class for displaying a message dialogue class MessageDialog(Gtk.MessageDialog): - def __init__(self, parent, message_type, s): + def __init__(self, parent: Gtk.Widget, message_type: Gtk.MessageType, s: str) -> None: if message_type == Gtk.MessageType.ERROR: buttons = Gtk.ButtonsType.OK else: @@ -54,7 +55,7 @@ class MessageDialog(Gtk.MessageDialog): # widget to help pick an encoding class EncodingMenu(Gtk.Box): - def __init__(self, prefs, autodetect=False): + def __init__(self, prefs: Preferences, autodetect: bool = False) -> None: Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) self.combobox = combobox = Gtk.ComboBoxText.new() self.encodings = prefs.getEncodings()[:] @@ -66,12 +67,12 @@ class EncodingMenu(Gtk.Box): self.pack_start(combobox, False, False, 0) combobox.show() - def set_text(self, encoding): + def set_text(self, encoding: Optional[str]) -> None: encoding = norm_encoding(encoding) if encoding in self.encodings: self.combobox.set_active(self.encodings.index(encoding)) - def get_text(self): + def get_text(self) -> Optional[str]: i = self.combobox.get_active() return self.encodings[i] if i >= 0 else None @@ -143,7 +144,7 @@ def relpath(a: str, b: str) -> str: # helper function prevent files from being confused with command line options # by prepending './' to the basename -def safeRelativePath(abspath1, name, prefs, cygwin_pref): +def safeRelativePath(abspath1: str, name: str, prefs: Preferences, cygwin_pref: str) -> str: s = os.path.join(os.curdir, relpath(abspath1, os.path.abspath(name))) if isWindows(): if prefs.getBool(cygwin_pref): @@ -163,43 +164,49 @@ def _use_flatpak() -> bool: # use popen to read the output of a command -def popenRead(dn, cmd, prefs, bash_pref, success_results=None): +def popenRead( + cwd: str, + cmd: List[str], + prefs: Preferences, + bash_pref: str, + success_results: List[int] = None) -> bytes: if success_results is None: success_results = [0] + + opt_cwd: Optional[str] = cwd if isWindows() and prefs.getBool(bash_pref): # launch the command from a bash shell is requested cmd = [ prefs.convertToNativePath('/bin/bash.exe'), '-l', '-c', - f"cd {_bash_escape(dn)}; {' '.join([ _bash_escape(arg) for arg in cmd ])}" + f"cd {_bash_escape(cwd)}; {' '.join([ _bash_escape(arg) for arg in cmd ])}" ] - dn = None + opt_cwd = None + # use subprocess.Popen to retrieve the file contents if isWindows(): - info = subprocess.STARTUPINFO() - info.dwFlags |= subprocess.STARTF_USESHOWWINDOW - info.wShowWindow = subprocess.SW_HIDE + info = subprocess.STARTUPINFO() # type: ignore + info.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore + info.wShowWindow = subprocess.SW_HIDE # type: ignore else: info = None + if _use_flatpak(): cmd = ['flatpak-spawn', '--host'] + cmd with subprocess.Popen( cmd, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=dn, + cwd=opt_cwd, startupinfo=info) as proc: - proc.stdin.close() - proc.stderr.close() - fd = proc.stdout - # read the command's output - s = fd.read() - fd.close() + output: bytes + if proc.stdout is not None: + # read the command's output + output = proc.stdout.read() + proc.stdout.close() if proc.wait() not in success_results: raise IOError('Command failed.') - return s + return output def len_minus_line_ending(s: str) -> int: @@ -227,9 +234,14 @@ def _strip_eols(ss: List[str]) -> List[str]: # use popen to read the output of a command -def popenReadLines(dn, cmd, prefs, bash_pref, success_results=None): +def popenReadLines( + cwd: str, + cmd: List[str], + prefs: Preferences, + bash_pref: str, + success_results: List[int] = None) -> List[str]: return _strip_eols(splitlines(popenRead( - dn, cmd, prefs, bash_pref, success_results).decode('utf-8', errors='ignore'))) + cwd, cmd, prefs, bash_pref, success_results).decode('utf-8', errors='ignore'))) def readconfiglines(fd: TextIO) -> List[str]: @@ -321,6 +333,7 @@ class LineEnding(IntFlag): Values can be used as flags in bitwise operations.''' + NO_FORMAT = 0 DOS_FORMAT = 1 MAC_FORMAT = 2 UNIX_FORMAT = 4 diff --git a/src/diffuse/vcs/bzr.py b/src/diffuse/vcs/bzr.py index 3277f01..3dee6db 100644 --- a/src/diffuse/vcs/bzr.py +++ b/src/diffuse/vcs/bzr.py @@ -20,13 +20,14 @@ import os from diffuse import utils +from diffuse.preferences import Preferences from diffuse.vcs.folder_set import FolderSet from diffuse.vcs.vcs_interface import VcsInterface # Bazaar support class Bzr(VcsInterface): - def getFileTemplate(self, prefs, name): + def getFileTemplate(self, prefs: Preferences, name: str) -> VcsInterface.PathRevisionList: # merge conflict left = name + '.OTHER' right = name + '.THIS' @@ -180,7 +181,7 @@ class Bzr(VcsInterface): result.append(m[k]) return result - def getRevision(self, prefs, name, rev): + def getRevision(self, prefs: Preferences, name: str, rev: str) -> bytes: return utils.popenRead( self.root, [ diff --git a/src/diffuse/vcs/cvs.py b/src/diffuse/vcs/cvs.py index 306f2e2..8b34bd5 100644 --- a/src/diffuse/vcs/cvs.py +++ b/src/diffuse/vcs/cvs.py @@ -22,13 +22,14 @@ import os from gettext import gettext as _ from diffuse import utils +from diffuse.preferences import Preferences from diffuse.vcs.folder_set import FolderSet from diffuse.vcs.vcs_interface import VcsInterface # CVS support class Cvs(VcsInterface): - def getFileTemplate(self, prefs, name): + def getFileTemplate(self, prefs: Preferences, name: str) -> VcsInterface.PathRevisionList: return [(name, 'BASE'), (name, None)] def getCommitTemplate(self, prefs, rev, names): @@ -85,10 +86,10 @@ class Cvs(VcsInterface): # sort the results return [modified[k] for k in sorted(modified.keys())] - def getRevision(self, prefs, name, rev): + def getRevision(self, prefs: Preferences, name: str, rev: str) -> bytes: if rev == 'BASE' and not os.path.exists(name): # find revision for removed files - for s in utils.popenReadLines( + lines = utils.popenReadLines( self.root, [ prefs.getString('cvs_bin'), @@ -97,9 +98,10 @@ class Cvs(VcsInterface): ], prefs, 'cvs_bash' - ): - if s.startswith(' Working revision:\t-'): - rev = s.split('\t')[1][1:] + ) + for line in lines: + if line.startswith(' Working revision:\t-'): + rev = line.split('\t')[1][1:] return utils.popenRead( self.root, [ diff --git a/src/diffuse/vcs/darcs.py b/src/diffuse/vcs/darcs.py index 28331f3..9dd25bf 100644 --- a/src/diffuse/vcs/darcs.py +++ b/src/diffuse/vcs/darcs.py @@ -19,14 +19,17 @@ import os +from typing import List + from diffuse import utils +from diffuse.preferences import Preferences from diffuse.vcs.folder_set import FolderSet from diffuse.vcs.vcs_interface import VcsInterface # Darcs support class Darcs(VcsInterface): - def getFileTemplate(self, prefs, name): + def getFileTemplate(self, prefs: Preferences, name: str) -> VcsInterface.PathRevisionList: return [(name, ''), (name, None)] def _getCommitTemplate(self, prefs, names, rev): @@ -135,8 +138,8 @@ class Darcs(VcsInterface): def getFolderTemplate(self, prefs, names): return self._getCommitTemplate(prefs, names, None) - def getRevision(self, prefs, name, rev): - args = [prefs.getString('darcs_bin'), 'show', 'contents'] + def getRevision(self, prefs: Preferences, name: str, rev: str) -> bytes: + args: List[str] = [prefs.getString('darcs_bin'), 'show', 'contents'] try: args.extend(['-n', str(int(rev))]) except ValueError: diff --git a/src/diffuse/vcs/folder_set.py b/src/diffuse/vcs/folder_set.py index df36555..aba091e 100644 --- a/src/diffuse/vcs/folder_set.py +++ b/src/diffuse/vcs/folder_set.py @@ -19,6 +19,8 @@ import os +from typing import List + class FolderSet: '''Utility class to help support Git and Monotone. @@ -27,18 +29,18 @@ class FolderSet: "mtn automate inventory." ''' - def __init__(self, names): - self.folders = f = [] + def __init__(self, names: List[str]) -> None: + self.folders: List[str] = [] for name in names: name = os.path.abspath(name) # ensure all names end with os.sep if not name.endswith(os.sep): name += os.sep - f.append(name) + self.folders.append(name) # returns True if the given abspath is a file that should be included in # the interesting file subset - def contains(self, abspath): + def contains(self, abspath: str) -> bool: if not abspath.endswith(os.sep): abspath += os.sep for f in self.folders: diff --git a/src/diffuse/vcs/git.py b/src/diffuse/vcs/git.py index 20b2d5f..46b19e6 100644 --- a/src/diffuse/vcs/git.py +++ b/src/diffuse/vcs/git.py @@ -27,7 +27,7 @@ from diffuse.vcs.vcs_interface import VcsInterface # Git support class Git(VcsInterface): - def getFileTemplate(self, prefs, name): + def getFileTemplate(self, prefs: Preferences, name: str) -> VcsInterface.PathRevisionList: return [(name, 'HEAD'), (name, None)] def getCommitTemplate(self, prefs, rev, names): @@ -150,7 +150,7 @@ class Git(VcsInterface): result.append(m[k]) return result - def getRevision(self, prefs, name, rev): + def getRevision(self, prefs: Preferences, name: str, rev: str) -> bytes: relpath = utils.relpath(self.root, os.path.abspath(name)).replace(os.sep, '/') return utils.popenRead( self.root, diff --git a/src/diffuse/vcs/hg.py b/src/diffuse/vcs/hg.py index 6dc540e..29b2e89 100644 --- a/src/diffuse/vcs/hg.py +++ b/src/diffuse/vcs/hg.py @@ -22,6 +22,7 @@ import os from typing import Optional from diffuse import utils +from diffuse.preferences import Preferences from diffuse.vcs.folder_set import FolderSet from diffuse.vcs.vcs_interface import VcsInterface @@ -29,7 +30,7 @@ from diffuse.vcs.vcs_interface import VcsInterface # Mercurial support class Hg(VcsInterface): def __init__(self, root: str): - VcsInterface.__init__(self, root) + super().__init__(root) self.working_rev: Optional[str] = None def _getPreviousRevision(self, prefs, rev): @@ -51,7 +52,7 @@ class Hg(VcsInterface): return self.working_rev return f'p1({rev})' - def getFileTemplate(self, prefs, name): + def getFileTemplate(self, prefs: Preferences, name: str) -> VcsInterface.PathRevisionList: return [(name, self._getPreviousRevision(prefs, None)), (name, None)] def _getCommitTemplate(self, prefs, names, cmd, rev): @@ -97,7 +98,7 @@ class Hg(VcsInterface): def getFolderTemplate(self, prefs, names): return self._getCommitTemplate(prefs, names, ['status', '-q'], None) - def getRevision(self, prefs, name, rev): + def getRevision(self, prefs: Preferences, name: str, rev: str) -> bytes: return utils.popenRead( self.root, [ diff --git a/src/diffuse/vcs/mtn.py b/src/diffuse/vcs/mtn.py index 7330e32..9aefbdb 100644 --- a/src/diffuse/vcs/mtn.py +++ b/src/diffuse/vcs/mtn.py @@ -21,27 +21,28 @@ import os import shlex from diffuse import utils +from diffuse.preferences import Preferences from diffuse.vcs.folder_set import FolderSet from diffuse.vcs.vcs_interface import VcsInterface # Monotone support class Mtn(VcsInterface): - def getFileTemplate(self, prefs, name): + def getFileTemplate(self, prefs: Preferences, name: str) -> VcsInterface.PathRevisionList: # FIXME: merge conflicts? return [(name, 'h:'), (name, None)] def getCommitTemplate(self, prefs, rev, names): # build command vcs_bin = prefs.getString('mtn_bin') - ss = utils.popenReadLines( + lines = utils.popenReadLines( self.root, [vcs_bin, 'automate', 'select', '-q', rev], prefs, 'mtn_bash') - if len(ss) != 1: + if len(lines) != 1: raise IOError('Ambiguous revision specifier') - args = [vcs_bin, 'automate', 'get_revision', ss[0]] + args = [vcs_bin, 'automate', 'get_revision', lines[0]] # build list of interesting files fs = FolderSet(names) pwd, isabs = os.path.abspath(os.curdir), False @@ -50,15 +51,15 @@ class Mtn(VcsInterface): # run command prev = None removed, added, modified, renamed = {}, {}, {}, {} - ss = utils.popenReadLines(self.root, args, prefs, 'mtn_bash') + lines = utils.popenReadLines(self.root, args, prefs, 'mtn_bash') i = 0 - while i < len(ss): + while i < len(lines): # process results - s = shlex.split(ss[i]) + line_args = shlex.split(lines[i]) i += 1 - if len(s) < 2: + if len(line_args) < 2: continue - arg, arg1 = s[0], s[1] + arg, arg1 = line_args[0], line_args[1] if arg == 'old_revision' and len(arg1) > 2: if prev is not None: break @@ -82,26 +83,27 @@ class Mtn(VcsInterface): if fs.contains(k): modified[arg1] = k elif arg == 'rename': - s = shlex.split(ss[i]) + line_args = shlex.split(lines[i]) i += 1 - if len(s) > 1 and s[0] == 'to': + if len(line_args) > 1 and line_args[0] == 'to': # renamed file k0 = os.path.join(self.root, prefs.convertToNativePath(arg1)) - k1 = os.path.join(self.root, prefs.convertToNativePath(s[1])) + k1 = os.path.join(self.root, prefs.convertToNativePath(line_args[1])) if fs.contains(k0) or fs.contains(k1): - renamed[s[1]] = (arg1, k0, k1) + renamed[line_args[1]] = (arg1, k0, k1) if removed or renamed: # remove directories removed_dirs = set() - for s in utils.popenReadLines( + lines = utils.popenReadLines( self.root, [vcs_bin, 'automate', 'get_manifest_of', prev], prefs, 'mtn_bash' - ): - s = shlex.split(s) - if len(s) > 1 and s[0] == 'dir': - removed_dirs.add(s[1]) + ) + for line in lines: + line_args = shlex.split(line) + if len(line_args) > 1 and line_args[0] == 'dir': + removed_dirs.add(line_args[1]) for k in removed_dirs: for m in removed, modified: if k in m: @@ -216,7 +218,7 @@ class Mtn(VcsInterface): result.append(m[k]) return result - def getRevision(self, prefs, name, rev): + def getRevision(self, prefs: Preferences, name: str, rev: str) -> bytes: return utils.popenRead( self.root, [ diff --git a/src/diffuse/vcs/rcs.py b/src/diffuse/vcs/rcs.py index 41fa203..d9bda95 100644 --- a/src/diffuse/vcs/rcs.py +++ b/src/diffuse/vcs/rcs.py @@ -22,12 +22,13 @@ import os from gettext import gettext as _ from diffuse import utils +from diffuse.preferences import Preferences from diffuse.vcs.vcs_interface import VcsInterface # RCS support class Rcs(VcsInterface): - def getFileTemplate(self, prefs, name): + def getFileTemplate(self, prefs: Preferences, name: str) -> VcsInterface.PathRevisionList: args = [ prefs.getString('rcs_bin_rlog'), '-L', @@ -153,7 +154,7 @@ class Rcs(VcsInterface): # sort the results return [[(k, r[k]), (k, None)] for k in sorted(r.keys())] - def getRevision(self, prefs, name, rev): + def getRevision(self, prefs: Preferences, name: str, rev: str) -> bytes: return utils.popenRead( self.root, [ diff --git a/src/diffuse/vcs/svk.py b/src/diffuse/vcs/svk.py index bd8dfe5..8cf3ca6 100644 --- a/src/diffuse/vcs/svk.py +++ b/src/diffuse/vcs/svk.py @@ -19,9 +19,10 @@ import os -from typing import Tuple +from typing import Optional, Tuple from diffuse import utils +from diffuse.preferences import Preferences from diffuse.vcs.svn import Svn @@ -41,14 +42,14 @@ class Svk(Svn): return s[0], s[4:] @staticmethod - def _getPreviousRevision(rev: str) -> str: + def _getPreviousRevision(rev: Optional[str]) -> str: if rev is None: return 'HEAD' if rev.endswith('@'): return str(int(rev[:-1]) - 1) + '@' return str(int(rev) - 1) - def getRevision(self, prefs, name, rev): + def getRevision(self, prefs: Preferences, name: str, rev: str) -> bytes: relpath = utils.relpath(self.root, os.path.abspath(name)).replace(os.sep, '/') return utils.popenRead( self.root, diff --git a/src/diffuse/vcs/svn.py b/src/diffuse/vcs/svn.py index fc14a56..8d54af9 100644 --- a/src/diffuse/vcs/svn.py +++ b/src/diffuse/vcs/svn.py @@ -24,6 +24,7 @@ from gettext import gettext as _ from typing import Optional, Tuple from diffuse import utils +from diffuse.preferences import Preferences from diffuse.vcs.folder_set import FolderSet from diffuse.vcs.vcs_interface import VcsInterface @@ -32,7 +33,7 @@ from diffuse.vcs.vcs_interface import VcsInterface # SVK support subclasses from this class Svn(VcsInterface): def __init__(self, root: str): - VcsInterface.__init__(self, root) + super().__init__(root) self.url: Optional[str] = None @staticmethod @@ -54,13 +55,13 @@ class Svn(VcsInterface): return s[0], s[k:] @staticmethod - def _getPreviousRevision(rev: str) -> str: + def _getPreviousRevision(rev: Optional[str]) -> str: if rev is None: return 'BASE' m = int(rev) return str(max(m > 1, 0)) - def _getURL(self, prefs): + def _getURL(self, prefs: Preferences) -> Optional[str]: if self.url is None: vcs, prefix = self._getVcs(), self._getURLPrefix() n = len(prefix) @@ -71,7 +72,7 @@ class Svn(VcsInterface): break return self.url - def getFileTemplate(self, prefs, name): + def getFileTemplate(self, prefs: Preferences, name: str) -> VcsInterface.PathRevisionList: # FIXME: verify this # merge conflict escaped_name = utils.globEscape(name) @@ -271,7 +272,7 @@ class Svn(VcsInterface): def getFolderTemplate(self, prefs, names): return self._getCommitTemplate(prefs, None, names) - def getRevision(self, prefs, name, rev): + def getRevision(self, prefs: Preferences, name: str, rev: str) -> bytes: vcs_bin = prefs.getString('svn_bin') if rev in ['BASE', 'COMMITTED', 'PREV']: return utils.popenRead( diff --git a/src/diffuse/vcs/vcs_interface.py b/src/diffuse/vcs/vcs_interface.py index 63800e8..088c74b 100644 --- a/src/diffuse/vcs/vcs_interface.py +++ b/src/diffuse/vcs/vcs_interface.py @@ -17,22 +17,35 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -class VcsInterface: +from abc import ABCMeta, abstractmethod +from typing import List, Optional, Tuple + +from diffuse.preferences import Preferences + + +class VcsInterface(metaclass=ABCMeta): """Interface for the VCSs.""" + PathRevisionPair = Tuple[Optional[str], Optional[str]] + PathRevisionList = List[PathRevisionPair] + def __init__(self, root: str): """The object will initialized with the repository's root folder.""" self.root = root - def getFileTemplate(self, prefs, name): + @abstractmethod + def getFileTemplate(self, prefs: Preferences, name: str) -> PathRevisionList: """Indicates which revisions to display for a file when none were explicitly requested.""" + @abstractmethod def getCommitTemplate(self, prefs, rev, names): """Indicates which file revisions to display for a commit.""" + @abstractmethod def getFolderTemplate(self, prefs, names): """Indicates which file revisions to display for a set of folders.""" - def getRevision(self, prefs, name, rev): + @abstractmethod + def getRevision(self, prefs: Preferences, name: str, rev: str) -> bytes: """Returns the contents of the specified file revision""" diff --git a/src/diffuse/vcs/vcs_registry.py b/src/diffuse/vcs/vcs_registry.py index 380c44a..84bedca 100644 --- a/src/diffuse/vcs/vcs_registry.py +++ b/src/diffuse/vcs/vcs_registry.py @@ -99,7 +99,7 @@ def _get_darcs_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]: def _get_git_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]: if 'GIT_DIR' in os.environ: try: - ss: List[str] = utils.popenReadLines( + lines: List[str] = utils.popenReadLines( path, [ prefs.getString('git_bin'), @@ -108,12 +108,12 @@ def _get_git_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]: ], prefs, 'git_bash') - if len(ss) > 0: + if len(lines) > 0: # be careful to handle trailing slashes dirs = path.split(os.sep) if dirs[-1] != '': dirs.append('') - ss = utils.strip_eol(ss[0]).split('/') + ss = utils.strip_eol(lines[0]).split('/') if ss[-1] != '': ss.append('') n = len(ss) @@ -186,7 +186,8 @@ def _get_svk_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]: # find working copies by parsing the config file with open(svkconfig, 'r', encoding='utf-8') as f: ss: List[str] = utils.readlines(f) - projs, sep = [], os.sep + projs: List[str] = [] + sep = os.sep # find the separator character for s in ss: if s.startswith(' sep: ') and len(s) > 7: diff --git a/src/diffuse/widgets.py b/src/diffuse/widgets.py index a57a9bb..904a3bc 100644 --- a/src/diffuse/widgets.py +++ b/src/diffuse/widgets.py @@ -21,8 +21,9 @@ import difflib import os import unicodedata +from enum import Flag, IntFlag, auto from gettext import gettext as _ -from typing import Any, Dict, List +from typing import Any, Callable, Dict, List, Optional, Tuple from diffuse import utils from diffuse.resources import theResources @@ -37,14 +38,12 @@ gi.require_version('PangoCairo', '1.0') from gi.repository import GObject, Gdk, Gtk, Pango, PangoCairo # type: ignore # noqa: E402 -# mapping to column width of a character (tab will never be in this map) -_char_width_cache: Dict[str, str] = {} - # the file diff viewer is always in one of these modes defining the cursor, # and hotkey behavior -LINE_MODE = 0 -CHAR_MODE = 1 -ALIGN_MODE = 2 +class EditMode(Flag): + LINE = auto() + CHAR = auto() + ALIGN = auto() # This is a replacement for Gtk.ScrolledWindow as it forced expose events to be @@ -151,7 +150,7 @@ class FileDiffViewerBase(Gtk.Grid): class Pane: def __init__(self) -> None: # list of lines displayed in this pane (including spacing lines) - self.lines: List[FileDiffViewerBase.Line] = [] + self.lines: List[Optional[FileDiffViewerBase.Line]] = [] # high water mark for line length in Pango units (used to determine # the required horizontal scroll range) self.line_lengths = 0 @@ -167,13 +166,13 @@ class FileDiffViewerBase(Gtk.Grid): # portion of the cache are cleared by setting entries to None self.diff_cache: List[List[Any]] = [] # mask indicating the type of line endings present - self.format = 0 + self.format: LineEnding = LineEnding.NO_FORMAT # number of lines with edits self.num_edits = 0 # class describing a single line of a pane class Line: - def __init__(self, line_number=None, text=None): + def __init__(self, line_number: Optional[int] = None, text: Optional[str] = None) -> None: # line number self.line_number = line_number # original text for the line @@ -181,13 +180,13 @@ class FileDiffViewerBase(Gtk.Grid): # flag indicating modifications are present self.is_modified = False # actual modified text - self.modified_text = None + self.modified_text: Optional[str] = None # cache used to speed up comparison of strings # this should be cleared whenever the comparison preferences change - self.compare_string = None + self.compare_string: Optional[str] = None # returns the current text for this line - def getText(self): + def getText(self) -> Optional[str]: if self.is_modified: return self.modified_text return self.text @@ -197,10 +196,14 @@ class FileDiffViewerBase(Gtk.Grid): if n < 2: raise ValueError('Invalid number of panes') - Gtk.Grid.__init__(self) + super().__init__() + + # mapping to column width of a character (tab will never be in this map) + self._char_width_cache: Dict[str, int] = {} + self.set_can_focus(True) self.prefs = prefs - self.string_width_cache = {} + self.string_width_cache: Dict[str, Optional[int]] = {} self.options = {} # diff blocks @@ -216,7 +219,7 @@ class FileDiffViewerBase(Gtk.Grid): self.diffmap_cache = None # editing mode - self.mode = LINE_MODE + self.mode = EditMode.LINE self.current_pane = 1 self.current_line = 0 self.current_char = 0 @@ -227,7 +230,7 @@ class FileDiffViewerBase(Gtk.Grid): self.cursor_column = -1 # keybindings - self._line_mode_actions = { + self._line_mode_actions: Dict[str, Callable] = { 'enter_align_mode': self._line_mode_enter_align_mode, 'enter_character_mode': self.setCharMode, 'first_line': self._first_line, @@ -260,7 +263,7 @@ class FileDiffViewerBase(Gtk.Grid): 'merge_from_left_then_right': self.merge_from_left_then_right, 'merge_from_right_then_left': self.merge_from_right_then_left } - self._align_mode_actions = { + self._align_mode_actions: Dict[str, Callable] = { 'enter_line_mode': self._align_mode_enter_line_mode, 'enter_character_mode': self.setCharMode, 'first_line': self._first_line, @@ -273,10 +276,10 @@ class FileDiffViewerBase(Gtk.Grid): 'page_down': self._line_mode_page_down, 'align': self._align_text } - self._character_mode_actions = { + self._character_mode_actions: Dict[str, Callable] = { 'enter_line_mode': self.setLineMode } - self._button_actions = { + self._button_actions: Dict[str, Callable] = { 'undo': self.undo, 'redo': self.redo, 'cut': self.cut, @@ -314,7 +317,7 @@ class FileDiffViewerBase(Gtk.Grid): } # create panes - self.dareas = [] + self.dareas: List[Gtk.DrawingArea] = [] self.panes: List[FileDiffViewerBase.Pane] = [] self.hadj = Gtk.Adjustment.new(0, 0, 0, 0, 0, 0) self.vadj = Gtk.Adjustment.new(0, 0, 0, 0, 0, 0) @@ -407,13 +410,13 @@ class FileDiffViewerBase(Gtk.Grid): # returns the 'column width' for a string -- used to help position # characters when tabs and other special characters are present # This is an inline loop over self.characterWidth() for performance reasons. - def stringWidth(self, s): + def stringWidth(self, s: str) -> int: if not self.prefs.getBool('display_show_whitespace'): s = utils.strip_eol(s) col = 0 for c in s: try: - w = _char_width_cache[c] + w = self._char_width_cache[c] except KeyError: v = ord(c) if v < 32: @@ -422,23 +425,23 @@ class FileDiffViewerBase(Gtk.Grid): w = tab_width - col % tab_width elif c == '\n': w = 1 - _char_width_cache[c] = w + self._char_width_cache[c] = w else: w = 2 - _char_width_cache[c] = w + self._char_width_cache[c] = w else: if unicodedata.east_asian_width(c) in 'WF': w = 2 else: w = 1 - _char_width_cache[c] = w + self._char_width_cache[c] = w col += w return col # returns the 'column width' for a single character created at column 'i' - def characterWidth(self, i, c): + def characterWidth(self, i: int, c: str) -> int: try: - return _char_width_cache[c] + return self._char_width_cache[c] except KeyError: v = ord(c) if v < 32: @@ -453,18 +456,18 @@ class FileDiffViewerBase(Gtk.Grid): w = 2 else: w = 1 - _char_width_cache[c] = w + self._char_width_cache[c] = w return w # translates a string into an array of the printable representation for # each character - def expand(self, s): + def expand(self, s: str) -> List[str]: visible = self.prefs.getBool('display_show_whitespace') if not visible: s = utils.strip_eol(s) tab_width = self.prefs.getInt('display_tab_width') col = 0 - result = [] + result: List[str] = [] for c in s: v = ord(c) if v <= 32: @@ -492,32 +495,32 @@ class FileDiffViewerBase(Gtk.Grid): col += self.characterWidth(col, c) return result - # changes the viewer's mode to LINE_MODE - def setLineMode(self): - if self.mode != LINE_MODE: - if self.mode == CHAR_MODE: + # changes the viewer's mode to EditMode.LINE + def setLineMode(self) -> None: + if self.mode != EditMode.LINE: + if self.mode == EditMode.CHAR: self._im_focus_out() self.im_context.reset() self._im_set_preedit(None) self.current_char = 0 self.selection_char = 0 self.dareas[self.current_pane].queue_draw() - elif self.mode == ALIGN_MODE: + elif self.mode == EditMode.ALIGN: self.dareas[self.align_pane].queue_draw() self.dareas[self.current_pane].queue_draw() self.align_pane = 0 self.align_line = 0 - self.mode = LINE_MODE + self.mode = EditMode.LINE self.emit('cursor_changed') self.emit('mode_changed') # changes the viewer's mode to CHAR_MODE - def setCharMode(self): - if self.mode != CHAR_MODE: - if self.mode == LINE_MODE: + def setCharMode(self) -> None: + if self.mode != EditMode.CHAR: + if self.mode == EditMode.LINE: self.cursor_column = -1 self.setCurrentChar(self.current_line, 0) - elif self.mode == ALIGN_MODE: + elif self.mode == EditMode.ALIGN: self.dareas[self.align_pane].queue_draw() self.cursor_column = -1 self.align_pane = 0 @@ -525,7 +528,7 @@ class FileDiffViewerBase(Gtk.Grid): self.setCurrentChar(self.current_line, 0) self._im_focus_in() self.im_context.reset() - self.mode = CHAR_MODE + self.mode = EditMode.CHAR self.emit('cursor_changed') self.emit('mode_changed') @@ -546,7 +549,7 @@ class FileDiffViewerBase(Gtk.Grid): return self.syntax # returns True if any pane contains edits - def hasEdits(self): + def hasEdits(self) -> bool: for pane in self.panes: if pane.num_edits > 0: return True @@ -561,7 +564,7 @@ class FileDiffViewerBase(Gtk.Grid): # openUndoBlock() should be called when the action from a user, like a # mouse button press, menu item, etc. may cause change to the diff viewer's # state - def openUndoBlock(self): + def openUndoBlock(self) -> None: self.undoblock = [] # all changes to the diff viewer's state should create an Undo object and @@ -574,19 +577,19 @@ class FileDiffViewerBase(Gtk.Grid): # all openUndoBlock() calls should also have a matching closeUndoBlock() # this method collects all Undos created since the openUndoBlock() call # and pushes them onto the undo stack as a single unit - def closeUndoBlock(self): + def closeUndoBlock(self) -> None: if len(self.undoblock) > 0: self.redos = [] self.undos.append(self.undoblock) self.undoblock = None # 'undo' action - def undo(self): + def undo(self) -> None: self.undoblock, old_block = None, self.undoblock - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: # avoid implicit preedit commit when an undo changes the mode self.im_context.reset() - if self.mode in (LINE_MODE, CHAR_MODE): + if self.mode in (EditMode.LINE, EditMode.CHAR): if len(self.undos) > 0: # move the block to the redo stack block = self.undos.pop() @@ -597,10 +600,10 @@ class FileDiffViewerBase(Gtk.Grid): self.undoblock = old_block # 'redo' action - def redo(self): + def redo(self) -> None: self.undoblock, old_block = None, self.undoblock - if self.mode in (LINE_MODE, CHAR_MODE): - if self.mode == CHAR_MODE: + if self.mode in (EditMode.LINE, EditMode.CHAR): + if self.mode == EditMode.CHAR: # avoid implicit preedit commit when an redo changes the mode self.im_context.reset() if len(self.redos) > 0: @@ -613,7 +616,7 @@ class FileDiffViewerBase(Gtk.Grid): self.undoblock = old_block # returns the width of the viewport's line number column in Pango units - def getLineNumberWidth(self): + def getLineNumberWidth(self) -> int: # find the maximum number of digits for a line number from all panes n = 0 if self.prefs.getBool('display_show_line_numbers'): @@ -624,7 +627,7 @@ class FileDiffViewerBase(Gtk.Grid): return n # returns the width of a string in Pango units - def getTextWidth(self, text): + def getTextWidth(self, text: str) -> int: if len(text) == 0: return 0 layout = self.create_pango_layout(text) @@ -635,7 +638,7 @@ class FileDiffViewerBase(Gtk.Grid): # set 'compute_width' to False if the high water mark for line length can # be used to determine the required width for the viewport, use True for # this value otherwise - def updateSize(self, compute_width, f=None): + def updateSize(self, compute_width: bool, f: Optional[int] = None) -> None: digit_width, stringWidth = self.digit_width, self.stringWidth string_width_cache = self.string_width_cache if compute_width: @@ -656,10 +659,10 @@ class FileDiffViewerBase(Gtk.Grid): text.append(line.modified_text) for s in text: if s is not None: - w = string_width_cache.get(s, None) - if w is None: - string_width_cache[s] = w = stringWidth(s) - pane.line_lengths = max(pane.line_lengths, digit_width * w) + swc = string_width_cache.get(s, None) + if swc is None: + string_width_cache[s] = swc = stringWidth(s) + pane.line_lengths = max(pane.line_lengths, digit_width * swc) # compute the maximum extents num_lines, line_lengths = 0, 0 for pane in self.panes: @@ -685,14 +688,14 @@ class FileDiffViewerBase(Gtk.Grid): self.vadj.step_increment = self.font_height # returns a line from the specified pane and offset - def getLine(self, f, i): + def getLine(self, f: int, i: int) -> Optional[Line]: lines = self.panes[f].lines if i < len(lines): return lines[i] return None # returns the text for the specified line - def getLineText(self, f, i): + def getLineText(self, f: int, i: int) -> Optional[str]: line = self.getLine(f, i) if line is not None: return line.getText() @@ -700,7 +703,7 @@ class FileDiffViewerBase(Gtk.Grid): # Undo for changes to the cached line ending style class SetFormatUndo: - def __init__(self, f, fmt, old_format): + def __init__(self, f: int, fmt: LineEnding, old_format: LineEnding) -> None: self.data = (f, fmt, old_format) def undo(self, viewer): @@ -712,7 +715,7 @@ class FileDiffViewerBase(Gtk.Grid): viewer.setFormat(f, fmt) # sets the cached line ending style - def setFormat(self, f, fmt): + def setFormat(self, f: int, fmt: LineEnding) -> None: pane = self.panes[f] if self.undoblock is not None: # create an Undo object for the action @@ -722,7 +725,7 @@ class FileDiffViewerBase(Gtk.Grid): # Undo for the creation of Line objects class InstanceLineUndo: - def __init__(self, f, i, reverse): + def __init__(self, f: int, i: int, reverse: bool) -> None: self.data = (f, i, reverse) def undo(self, viewer): @@ -735,7 +738,7 @@ class FileDiffViewerBase(Gtk.Grid): # creates an instance of a Line object for the specified pane and offset # deletes an instance when 'reverse' is set to True - def instanceLine(self, f, i, reverse=False): + def instanceLine(self, f: int, i: int, reverse: bool = False) -> None: if self.undoblock is not None: # create an Undo object for the action self.addUndo(FileDiffViewerBase.InstanceLineUndo(f, i, reverse)) @@ -759,7 +762,7 @@ class FileDiffViewerBase(Gtk.Grid): f, i, _, _, is_modified, text = self.data viewer.updateLineText(f, i, is_modified, text) - def getMapFlags(self, f, i): + def getMapFlags(self, f: int, i: int) -> int: flags = 0 compare_text = self.getCompareString(f, i) if f > 0 and self.getCompareString(f - 1, i) != compare_text: @@ -822,7 +825,7 @@ class FileDiffViewerBase(Gtk.Grid): # Undo for inserting a spacing line in a single pane class InsertNullUndo: - def __init__(self, f, i, reverse): + def __init__(self, f: int, i: int, reverse: bool) -> None: self.data = (f, i, reverse) def undo(self, viewer): @@ -836,7 +839,7 @@ class FileDiffViewerBase(Gtk.Grid): # insert a spacing line at line 'i' in pane 'f' # this caller must ensure the blocks and number of lines in each pane # are valid again - def insertNull(self, f, i, reverse): + def insertNull(self, f: int, i: int, reverse: bool) -> None: if self.undoblock is not None: # create an Undo object for the action self.addUndo(FileDiffViewerBase.InsertNullUndo(f, i, reverse)) @@ -885,7 +888,7 @@ class FileDiffViewerBase(Gtk.Grid): # Undo for alignment changes class AlignmentChangeUndo: - def __init__(self, finished): + def __init__(self, finished: bool) -> None: self.data = finished def undo(self, viewer): @@ -897,7 +900,7 @@ class FileDiffViewerBase(Gtk.Grid): viewer.alignmentChange(finished) # update viewer in response to alignment changes - def alignmentChange(self, finished): + def alignmentChange(self, finished: bool) -> None: if self.undoblock is not None: # create an Undo object for the action self.addUndo(FileDiffViewerBase.AlignmentChangeUndo(finished)) @@ -947,7 +950,7 @@ class FileDiffViewerBase(Gtk.Grid): self.blocks = blocks # insert 'n' blank lines in all panes - def insertLines(self, i, n): + def insertLines(self, i: int, n: int) -> None: # insert lines self.updateAlignment(i, 0, [n * [None] for pane in self.panes]) pre, post = _cut_blocks(i, self.blocks) @@ -966,7 +969,7 @@ class FileDiffViewerBase(Gtk.Grid): self.diffmap.queue_draw() # remove a line - def removeSpacerLines(self, i, n, skip=-1): + def removeSpacerLines(self, i: int, n: int, skip: int = -1) -> int: npanes, removed = len(self.panes), [] for j in range(i, i + n): for f in range(npanes): @@ -1055,7 +1058,7 @@ class FileDiffViewerBase(Gtk.Grid): self.diffmap.queue_draw() # create a hash for a line to use for line matching - def _alignmentHash(self, line): + def _alignmentHash(self, line: Line) -> Optional[str]: text = line.getText() if text is None: return None @@ -1076,15 +1079,14 @@ class FileDiffViewerBase(Gtk.Grid): if pref('align_ignore_whitespace_changes'): # replace all blocks of white space with a single space pc = True - r = [] - append = r.append + r: List[str] = [] for c in text: if c in utils.whitespace: if pc: - append(' ') + r.append(' ') pc = False else: - append(c) + r.append(c) pc = True text = ''.join(r) if pref('align_ignore_case'): @@ -1110,9 +1112,8 @@ class FileDiffViewerBase(Gtk.Grid): s1, s2 = mlines n1, n2 = 0, 0 # hash lines according to the alignment preferences - a = self._alignmentHash - t1 = [a(s) for s in s1] - t2 = [a(s) for s in s2] + t1 = [self._alignmentHash(s) for s in s1] + t2 = [self._alignmentHash(s) for s in s2] # align s1 and s2 by inserting spacer lines # this will be used to determine which lines from the inner lists of # lines should be neighbours @@ -1253,8 +1254,9 @@ class FileDiffViewerBase(Gtk.Grid): # refresh the lines to contain new objects with updated line numbers and # no local edits - def bakeEdits(self, f): - pane, lines, line_num = self.panes[f], [], 0 + def bakeEdits(self, f: int) -> None: + lines: List[Optional[FileDiffViewerBase.Line]] = [] + pane, line_num = self.panes[f], 0 for i in range(len(pane.lines)): s = self.getLineText(f, i) if s is None: @@ -1267,7 +1269,7 @@ class FileDiffViewerBase(Gtk.Grid): self.replaceLines(f, pane.lines, lines, pane.max_line_number, line_num) # update the contents for a line, creating the line if necessary - def updateText(self, f, i, text, is_modified=True): + def updateText(self, f: int, i: int, text: Optional[str], is_modified: bool = True) -> None: if self.panes[f].lines[i] is None: self.instanceLine(f, i) self.updateLineText(f, i, is_modified, text) @@ -1282,7 +1284,7 @@ class FileDiffViewerBase(Gtk.Grid): pane = self.panes[f] nlines = len(pane.lines) line0, line1 = self.selection_line, self.current_line - if self.mode == LINE_MODE: + if self.mode == EditMode.LINE: col0, col1 = 0, 0 if line1 < line0: line0, line1 = line1, line0 @@ -1349,7 +1351,7 @@ class FileDiffViewerBase(Gtk.Grid): for i in range(delta): self.updateText(f, line0 + n_need + i, None) # update selection - if self.mode == LINE_MODE: + if self.mode == EditMode.LINE: self.setCurrentLine(f, line0 + max(n_need, 1) - 1, line0) else: self.setCurrentChar(cur_line, lastcol) @@ -1523,7 +1525,7 @@ class FileDiffViewerBase(Gtk.Grid): self.selection_line = selection_line self.selection_char = selection_char self.cursor_column = cursor_column - if mode == CHAR_MODE: + if mode == EditMode.CHAR: self.setCurrentChar(self.current_line, self.current_char, True) else: self.setCurrentLine(self.current_pane, self.current_line, self.selection_line) @@ -1533,7 +1535,7 @@ class FileDiffViewerBase(Gtk.Grid): self.dareas[old_f].queue_draw() # queue a range of lines for redrawing - def _queue_draw_lines(self, f, line0, line1=None): + def _queue_draw_lines(self, f: int, line0: int, line1: Optional[int] = None) -> None: if line1 is None: line1 = line0 elif line0 > line1: @@ -1543,7 +1545,7 @@ class FileDiffViewerBase(Gtk.Grid): darea.queue_draw_area(0, line0 * h - int(self.vadj.get_value()), w, (line1 - line0 + 1) * h) # scroll vertically to ensure the current line is visible - def _ensure_line_is_visible(self, i): + def _ensure_line_is_visible(self, i: int) -> None: h = self.font_height lower = i * h upper = lower + h @@ -1555,9 +1557,9 @@ class FileDiffViewerBase(Gtk.Grid): elif upper > v + ps: vadj.set_value(upper - ps) - # change the current selection in LINE_MODE + # change the current selection in EditMode.LINE # use extend=True to extend the selection - def setCurrentLine(self, f, i, selection=None): + def setCurrentLine(self, f: int, i: int, selection: Optional[int] = None) -> None: # remember old cursor position so we can just redraw what is necessary old_f = self.current_pane line0, line1 = self.current_line, self.selection_line @@ -1569,10 +1571,7 @@ class FileDiffViewerBase(Gtk.Grid): # update cursor self.current_pane = f self.current_line = i - if selection is None: - self.selection_line = i - else: - self.selection_line = selection + self.selection_line = selection if selection is not None else i self.emit('cursor_changed') @@ -1585,9 +1584,9 @@ class FileDiffViewerBase(Gtk.Grid): self._ensure_line_is_visible(i) # returns True if the line has preedit text - def hasPreedit(self, f, i): + def hasPreedit(self, f: int, i: int) -> bool: return ( - self.mode == CHAR_MODE and + self.mode == EditMode.CHAR and self.current_pane == f and self.current_line == i and self.im_preedit is not None @@ -1605,7 +1604,7 @@ class FileDiffViewerBase(Gtk.Grid): # inform input method about cursor motion def _cursor_position_changed(self, recompute): - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: # update input method h = self.font_height if recompute: @@ -1626,15 +1625,17 @@ class FileDiffViewerBase(Gtk.Grid): self.im_context.set_cursor_location(rect) # get the position of the cursor in Pango units - def _get_cursor_x_offset(self): + def _get_cursor_x_offset(self) -> int: j = self.current_char if j > 0: - text = self.getLineText(self.current_pane, self.current_line)[:j] - return self.getTextWidth(''.join(self.expand(text))) + text = self.getLineText(self.current_pane, self.current_line) + if text is not None: + text = text[:j] + return self.getTextWidth(''.join(self.expand(text))) return 0 # scroll to ensure the current cursor position is visible - def _ensure_cursor_is_visible(self): + def _ensure_cursor_is_visible(self) -> None: current_line = self.current_line # find the cursor's horizontal range @@ -1699,7 +1700,7 @@ class FileDiffViewerBase(Gtk.Grid): f = self.current_pane start, end = self.selection_line, self.current_line # find extents of selection - if self.mode == LINE_MODE: + if self.mode == EditMode.LINE: if end < start: start, end = end, start end += 1 @@ -1722,12 +1723,12 @@ class FileDiffViewerBase(Gtk.Grid): return ''.join([s for s in ss if s is not None]) # expands the selection to include everything - def select_all(self): - if self.mode in (LINE_MODE, CHAR_MODE): + def select_all(self) -> None: + if self.mode in (EditMode.LINE, EditMode.CHAR): f = self.current_pane self.selection_line = 0 self.current_line = len(self.panes[f].lines) - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: self.selection_char = 0 self.current_char = 0 self.dareas[f].queue_draw() @@ -1735,7 +1736,7 @@ class FileDiffViewerBase(Gtk.Grid): # returns the index of the last character in text that should be left of # 'x' _pixels from the edge of the darea widget # if partial=True, include characters only partially to the left of 'x' - def _getPickedCharacter(self, text, x, partial): + def _getPickedCharacter(self, text: Optional[str], x: int, partial: bool) -> int: if text is None: return 0 n = len(text) @@ -1757,7 +1758,7 @@ class FileDiffViewerBase(Gtk.Grid): if y < 0: x, y = -1, 0 i = min(y // self.font_height, len(self.panes[f].lines)) - if self.mode == CHAR_MODE and f == self.current_pane: + if self.mode == EditMode.CHAR and f == self.current_pane: text = utils.strip_eol(self.getLineText(f, i)) j = self._getPickedCharacter(text, x, True) if extend: @@ -1766,9 +1767,9 @@ class FileDiffViewerBase(Gtk.Grid): si, sj = None, None self.setCurrentChar(i, j, si, sj) else: - if self.mode == ALIGN_MODE: + if self.mode == EditMode.ALIGN: extend = False - elif self.mode == CHAR_MODE: + elif self.mode == EditMode.CHAR: self.setLineMode() if extend and f == self.current_pane: selection = self.selection_line @@ -1787,17 +1788,17 @@ class FileDiffViewerBase(Gtk.Grid): # left mouse button if event.type == Gdk.EventType._2BUTTON_PRESS: # double click - if self.mode == ALIGN_MODE: + if self.mode == EditMode.ALIGN: self.setLineMode() - if self.mode == LINE_MODE: + if self.mode == EditMode.LINE: # change to CHAR_MODE self.setCurrentLine(f, i) # silently switch mode so the viewer does not scroll yet. - self.mode = CHAR_MODE + self.mode = EditMode.CHAR self._im_focus_in() self.button_press(f, x, y, False) self.emit('mode_changed') - elif self.mode == CHAR_MODE and self.current_pane == f: + elif self.mode == EditMode.CHAR and self.current_pane == f: # select word text = utils.strip_eol(self.getLineText(f, i)) if text is not None: @@ -1813,7 +1814,7 @@ class FileDiffViewerBase(Gtk.Grid): self.setCurrentChar(i, j, i, k) elif event.type == Gdk.EventType._3BUTTON_PRESS: # triple click, select a whole line - if self.mode == CHAR_MODE and self.current_pane == f: + if self.mode == EditMode.CHAR and self.current_pane == f: i2 = min(i + 1, nlines) self.setCurrentChar(i2, 0, i, 0) else: @@ -1823,7 +1824,7 @@ class FileDiffViewerBase(Gtk.Grid): self.button_press(f, x, y, extend) elif event.button == 2: # middle mouse button, paste primary selection - if self.mode == CHAR_MODE and f == self.current_pane: + if self.mode == EditMode.CHAR and f == self.current_pane: self.button_press(f, x, y, False) self.openUndoBlock() Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY).request_text( @@ -1833,11 +1834,11 @@ class FileDiffViewerBase(Gtk.Grid): elif event.button == 3: # right mouse button, raise context sensitive menu can_align = ( - self.mode == LINE_MODE and + self.mode == EditMode.LINE and (f in (self.current_pane + 1, f == self.current_pane - 1))) - can_isolate = self.mode == LINE_MODE and f == self.current_pane - can_merge = self.mode == LINE_MODE and f != self.current_pane - can_select = self.mode in (LINE_MODE, CHAR_MODE) and f == self.current_pane + can_isolate = self.mode == EditMode.LINE and f == self.current_pane + can_merge = self.mode == EditMode.LINE and f != self.current_pane + can_select = self.mode in (EditMode.LINE, EditMode.CHAR) and f == self.current_pane can_swap = (f != self.current_pane) menu = createMenu([ @@ -1945,7 +1946,7 @@ class FileDiffViewerBase(Gtk.Grid): # returns a hash of a string that can be used to quickly compare strings # according to the display preferences - def getCompareString(self, f, i): + def getCompareString(self, f: int, i: int) -> Optional[str]: line = self.getLine(f, i) if line is None: return None @@ -2137,14 +2138,14 @@ class FileDiffViewerBase(Gtk.Grid): preedit_bg_colour = (colour * alpha).over(preedit_bg_colour) cr.set_source_rgba(colour.red, colour.green, colour.blue, alpha) cr.paint() - if self.mode == ALIGN_MODE: + if self.mode == EditMode.ALIGN: # draw align if self.align_pane == f and self.align_line == i: colour = theResources.getColour('alignment') alpha = theResources.getFloat('alignment_opacity') cr.set_source_rgba(colour.red, colour.green, colour.blue, alpha) cr.paint() - elif self.mode == LINE_MODE: + elif self.mode == EditMode.LINE: # draw line selection if self.current_pane == f: start, end = self.selection_line, self.current_line @@ -2155,7 +2156,7 @@ class FileDiffViewerBase(Gtk.Grid): alpha = theResources.getFloat('line_selection_opacity') cr.set_source_rgba(colour.red, colour.green, colour.blue, alpha) cr.paint() - elif self.mode == CHAR_MODE: + elif self.mode == EditMode.CHAR: # draw char selection if self.current_pane == f and text is not None: start, end = self.selection_line, self.current_line @@ -2287,7 +2288,7 @@ class FileDiffViewerBase(Gtk.Grid): if self.current_pane == f and self.current_line == i: # draw the cursor and preedit text - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: x_pos = x_start + _pixels(self._get_cursor_x_offset()) if has_preedit: # we have preedit text @@ -2313,7 +2314,7 @@ class FileDiffViewerBase(Gtk.Grid): cr.move_to(x_pos + 0.5, y_start) cr.rel_line_to(0, h) cr.stroke() - elif self.mode in (LINE_MODE, ALIGN_MODE): + elif self.mode in (EditMode.LINE, EditMode.ALIGN): # draw the line editing cursor colour = theResources.getColour('cursor') cr.set_source_rgb(colour.red, colour.green, colour.blue) @@ -2486,11 +2487,11 @@ class FileDiffViewerBase(Gtk.Grid): # 'enter_align_mode' keybinding action def _line_mode_enter_align_mode(self): - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: self._im_focus_out() self.im_context.reset() self._im_set_preedit(None) - self.mode = ALIGN_MODE + self.mode = EditMode.ALIGN self.selection_line = self.current_line self.align_pane = self.current_pane self.align_line = self.current_line @@ -2553,7 +2554,7 @@ class FileDiffViewerBase(Gtk.Grid): self.setCurrentLine(self.current_pane, self.current_line - delta, selection) # 'extend_page_up' keybinding action - def _line_mode_extend_page_up(self): + def _line_mode_extend_page_up(self) -> None: self._line_mode_page_up(self.selection_line) # 'page_down' keybinding action @@ -2562,20 +2563,20 @@ class FileDiffViewerBase(Gtk.Grid): self.setCurrentLine(self.current_pane, self.current_line + delta, selection) # 'extend_page_down' keybinding action - def _line_mode_extend_page_down(self): + def _line_mode_extend_page_down(self) -> None: self._line_mode_page_down(self.selection_line) # 'delete_text' keybinding action - def _delete_text(self): + def _delete_text(self) -> None: self.replaceText('') # 'enter_line_mode' keybinding action - def _align_mode_enter_line_mode(self): + def _align_mode_enter_line_mode(self) -> None: self.selection_line = self.current_line self.setLineMode() # 'align' keybinding action - def _align_text(self): + def _align_text(self) -> None: f1 = self.align_pane line1 = self.align_line line2 = self.current_line @@ -2587,18 +2588,18 @@ class FileDiffViewerBase(Gtk.Grid): self.align(self.current_pane, line2, line1) # give the input method focus - def _im_focus_in(self): + def _im_focus_in(self) -> None: if self.has_focus: self.im_context.focus_in() # remove input method focus - def _im_focus_out(self): + def _im_focus_out(self) -> None: if self.has_focus: self.im_context.focus_out() # input method callback for committed text def im_commit_cb(self, im, s): - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: self.openUndoBlock() self.replaceText(s) self.closeUndoBlock() @@ -2606,7 +2607,7 @@ class FileDiffViewerBase(Gtk.Grid): # update the cached preedit text def _im_set_preedit(self, p): self.im_preedit = p - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: f, i = self.current_pane, self.current_line self._queue_draw_lines(f, i) if f > 0: @@ -2617,7 +2618,7 @@ class FileDiffViewerBase(Gtk.Grid): # queue a redraw for location of preedit text def im_preedit_changed_cb(self, im): - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: s, a, c = self.im_context.get_preedit_string() if len(s) > 0: # we have preedit text, draw that instead @@ -2629,7 +2630,7 @@ class FileDiffViewerBase(Gtk.Grid): # callback to respond to retrieve_surrounding signals from input methods def im_retrieve_surrounding_cb(self, im): - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: # notify input method of text surrounding the cursor s = utils.null_to_empty(self.getLineText(self.current_pane, self.current_line)) im.set_surrounding(s, len(s), self.current_char) @@ -2637,13 +2638,13 @@ class FileDiffViewerBase(Gtk.Grid): # callback for 'focus_in_event' def focus_in_cb(self, widget, event): self.has_focus = True - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: # notify the input method of the focus change self._im_focus_in() # callback for 'focus_out_event' def focus_out_cb(self, widget, event): - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: # notify the input method of the focus change self._im_focus_out() self.has_focus = False @@ -2651,7 +2652,7 @@ class FileDiffViewerBase(Gtk.Grid): # callback for keyboard events # only keypresses that are not handled by menu item accelerators reach here def key_press_cb(self, widget, event): - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: # update input method if self.im_context.filter_keypress(event): return True @@ -2661,13 +2662,13 @@ class FileDiffViewerBase(Gtk.Grid): if event.state & Gdk.ModifierType.LOCK_MASK: mask ^= Gdk.ModifierType.SHIFT_MASK self.openUndoBlock() - if self.mode == LINE_MODE: + if self.mode == EditMode.LINE: # check if the keyval matches a line mode action action = theResources.getActionForKey('line_mode', event.keyval, mask) if action in self._line_mode_actions: self._line_mode_actions[action]() retval = True - elif self.mode == CHAR_MODE: + elif self.mode == EditMode.CHAR: f = self.current_pane if event.state & Gdk.ModifierType.SHIFT_MASK: si, sj = self.selection_line, self.selection_char @@ -2762,7 +2763,7 @@ class FileDiffViewerBase(Gtk.Grid): if text is not None and j < len(text): c = _get_character_class(text[j]) if ( - c != WHITESPACE_CLASS and + c != CharacterClass.WHITESPACE and (j < 1 or j - 1 >= len(text) or _get_character_class(text[j - 1]) != c) ): break @@ -2919,7 +2920,7 @@ class FileDiffViewerBase(Gtk.Grid): # handle all other printable characters elif len(event.string) > 0: self.replaceText(event.string) - elif self.mode == ALIGN_MODE: + elif self.mode == EditMode.ALIGN: # check if the keyval matches an align mode action action = theResources.getActionForKey('align_mode', event.keyval, mask) if action in self._align_mode_actions: @@ -2929,19 +2930,19 @@ class FileDiffViewerBase(Gtk.Grid): return retval # 'copy' action - def copy(self): - if self.mode in (LINE_MODE, CHAR_MODE): + def copy(self) -> None: + if self.mode in (EditMode.LINE, EditMode.CHAR): self._set_clipboard_text(Gdk.SELECTION_CLIPBOARD, self.getSelectedText()) # 'cut' action - def cut(self): - if self.mode in (LINE_MODE, CHAR_MODE): + def cut(self) -> None: + if self.mode in (EditMode.LINE, EditMode.CHAR): self.copy() self.replaceText('') # callback used when receiving clipboard text def receive_clipboard_text_cb(self, clipboard, text, data): - if self.mode in (LINE_MODE, CHAR_MODE): + if self.mode in (EditMode.LINE, EditMode.CHAR): # there is no guarantee this will be called before finishing # Gtk.Clipboard.get so we may need to create our own undo block needs_block = (self.undoblock is None) @@ -2952,13 +2953,13 @@ class FileDiffViewerBase(Gtk.Grid): self.closeUndoBlock() # 'paste' action - def paste(self): + def paste(self) -> None: Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD).request_text( self.receive_clipboard_text_cb, None) # 'clear_edits' action - def clear_edits(self): + def clear_edits(self) -> None: self.setLineMode() self.recordEditMode() f = self.current_pane @@ -2977,8 +2978,8 @@ class FileDiffViewerBase(Gtk.Grid): self.recordEditMode() # 'dismiss_all_edits' action - def dismiss_all_edits(self): - if self.mode in (LINE_MODE, CHAR_MODE): + def dismiss_all_edits(self) -> None: + if self.mode in (EditMode.LINE, EditMode.CHAR): self.bakeEdits(self.current_pane) # callback for find menu item @@ -3038,7 +3039,7 @@ class FileDiffViewerBase(Gtk.Grid): return False # move cursor to a given line - def go_to_line(self, i): + def go_to_line(self, i: int) -> None: f, idx = self.current_pane, 0 if i > 0: # search for a line matching that number @@ -3057,7 +3058,7 @@ class FileDiffViewerBase(Gtk.Grid): # recompute viewport size and redraw as the display preferences may have # changed - def prefsUpdated(self): + def prefsUpdated(self) -> None: # clear cache as tab width may have changed self.string_width_cache = {} self.setFont( @@ -3075,12 +3076,12 @@ class FileDiffViewerBase(Gtk.Grid): self.diffmap.queue_draw() # 'realign_all' action - def realign_all(self): + def realign_all(self) -> None: self.setLineMode() f = self.current_pane self.recordEditMode() - lines = [] - blocks = [] + lines: List[List[FileDiffViewerBase.Line]] = [] + blocks: List[int] = [] for pane in self.panes: # create a new list of lines with no spacers newlines = [[line for line in pane.lines if line is not None]] @@ -3114,7 +3115,7 @@ class FileDiffViewerBase(Gtk.Grid): self.closeUndoBlock() # 'isolate' action - def isolate(self): + def isolate(self) -> None: self.setLineMode() self.recordEditMode() f = self.current_pane @@ -3157,7 +3158,7 @@ class FileDiffViewerBase(Gtk.Grid): # returns True if line 'i' in pane 'f' has an edit or is different from its # neighbour - def hasEditsOrDifference(self, f, i): + def hasEditsOrDifference(self, f: int, i: int) -> bool: line = self.getLine(f, i) if line is not None and line.is_modified: return True @@ -3167,7 +3168,7 @@ class FileDiffViewerBase(Gtk.Grid): (f + 1 < len(self.panes) and text != self.getCompareString(f + 1, i))) # returns True if there are any differences - def hasDifferences(self): + def hasDifferences(self) -> bool: n = len(self.panes) nlines = len(self.panes[0].lines) for i in range(nlines): @@ -3178,13 +3179,13 @@ class FileDiffViewerBase(Gtk.Grid): return False # scroll the viewport so _pixels at position 'y' are centred - def centre_view_about_y(self, y): + def centre_view_about_y(self, y: int) -> None: vadj = self.vadj y = min(max(0, y - vadj.get_page_size() / 2), vadj.get_upper() - vadj.get_page_size()) vadj.set_value(y) # move the cursor from line 'i' to the next difference in direction 'delta' - def go_to_difference(self, i, delta): + def go_to_difference(self, i: int, delta: int) -> None: f = self.current_pane nlines = len(self.panes[f].lines) # back up to beginning of difference @@ -3210,31 +3211,31 @@ class FileDiffViewerBase(Gtk.Grid): self.setCurrentLine(f, start, i) # 'first_difference' action - def first_difference(self): + def first_difference(self) -> None: self.setLineMode() self.go_to_difference(0, 1) # 'previous_difference' action - def previous_difference(self): + def previous_difference(self) -> None: self.setLineMode() i = min(self.current_line, self.selection_line) - 1 self.go_to_difference(i, -1) # 'next_difference' action - def next_difference(self): + def next_difference(self) -> None: self.setLineMode() i = max(self.current_line, self.selection_line) + 1 self.go_to_difference(i, 1) # 'last_difference' action - def last_difference(self): + def last_difference(self) -> None: self.setLineMode() i = len(self.panes[self.current_pane].lines) self.go_to_difference(i, -1) # Undo for changes to the pane ordering class SwapPanesUndo: - def __init__(self, f_dst, f_src): + def __init__(self, f_dst: int, f_src: int) -> None: self.data = (f_dst, f_src) def undo(self, viewer): @@ -3246,7 +3247,7 @@ class FileDiffViewerBase(Gtk.Grid): viewer.swapPanes(f_dst, f_src) # swap the contents of two panes - def swapPanes(self, f_dst, f_src): + def swapPanes(self, f_dst: int, f_src: int) -> None: if self.undoblock is not None: self.addUndo(FileDiffViewerBase.SwapPanesUndo(f_dst, f_src)) self.current_pane = f_dst @@ -3267,9 +3268,9 @@ class FileDiffViewerBase(Gtk.Grid): self.emit('swapped_panes', f_dst, f_src) # swap the contents of two panes - def swap_panes(self, f_dst, f_src): + def swap_panes(self, f_dst: int, f_src: int) -> None: if 0 <= f_dst < len(self.panes): - if self.mode == ALIGN_MODE: + if self.mode == EditMode.ALIGN: self.setLineMode() self.recordEditMode() self.swapPanes(f_dst, f_src) @@ -3282,19 +3283,19 @@ class FileDiffViewerBase(Gtk.Grid): self.closeUndoBlock() # 'shift_pane_left' action - def shift_pane_left(self): + def shift_pane_left(self) -> None: f = self.current_pane self.swap_panes(f - 1, f) # 'shift_pane_right' action - def shift_pane_right(self): + def shift_pane_right(self) -> None: f = self.current_pane self.swap_panes(f + 1, f) # 'convert_to_upper_case' action - def _convert_case(self, to_upper): + def _convert_case(self, to_upper: bool) -> None: # find range of characters to operate upon - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: start, end = self.current_line, self.selection_line j0, j1 = self.current_char, self.selection_char if end < start or (start == end and j1 < j0): @@ -3333,16 +3334,16 @@ class FileDiffViewerBase(Gtk.Grid): self.updateText(f, i, s) # 'convert_to_upper_case' action - def convert_to_upper_case(self): + def convert_to_upper_case(self) -> None: self._convert_case(True) # 'convert_to_lower_case' action - def convert_to_lower_case(self): + def convert_to_lower_case(self) -> None: self._convert_case(False) # sort lines - def _sort_lines(self, descending): - if self.mode != CHAR_MODE: + def _sort_lines(self, descending: bool) -> None: + if self.mode != EditMode.CHAR: self.setLineMode() self.recordEditMode() f = self.current_pane @@ -3353,7 +3354,7 @@ class FileDiffViewerBase(Gtk.Grid): # get set of lines ss = [self.getLineText(f, i) for i in range(start, end + 1)] # create sorted list, removing any nulls - temp = [s for s in ss if s is not None] + temp: List[Optional[str]] = [s for s in ss if s is not None] temp.sort() if descending: temp.reverse() @@ -3363,22 +3364,22 @@ class FileDiffViewerBase(Gtk.Grid): # update line if it changed if ss[i] != s: self.updateText(f, start + i, s) - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: # ensure the cursor position is valid self.setCurrentChar(self.current_line, 0, self.selection_line, 0) self.recordEditMode() # 'sort_lines_in_ascending_order' action - def sort_lines_in_ascending_order(self): + def sort_lines_in_ascending_order(self) -> None: self._sort_lines(False) # 'sort_lines_in_descending_order' action - def sort_lines_in_descending_order(self): + def sort_lines_in_descending_order(self) -> None: self._sort_lines(True) # 'remove_trailing_white_space' action - def remove_trailing_white_space(self): - if self.mode != CHAR_MODE: + def remove_trailing_white_space(self) -> None: + if self.mode != EditMode.CHAR: self.setLineMode() self.recordEditMode() f = self.current_pane @@ -3397,15 +3398,15 @@ class FileDiffViewerBase(Gtk.Grid): # update line if it changed if n < old_n: self.updateText(f, i, text[:n] + text[old_n:]) - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: # ensure the cursor position is valid self.setCurrentChar(self.current_line, 0, self.selection_line, 0) self.recordEditMode() # 'convert_tabs_to_spaces' action - def convert_tabs_to_spaces(self): + def convert_tabs_to_spaces(self) -> None: # find range of characters to operate upon - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: start, end = self.current_line, self.selection_line j0, j1 = self.current_char, self.selection_char if end < start or (start == end and j1 < j0): @@ -3450,14 +3451,14 @@ class FileDiffViewerBase(Gtk.Grid): # update line only if it changed if text != s: self.updateText(f, i, s) - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: # ensure the cursor position is valid self.setCurrentChar(end, j1, start, j0) self.recordEditMode() # 'convert_leading_spaces_to_tabs' action - def convert_leading_spaces_to_tabs(self): - if self.mode != CHAR_MODE: + def convert_leading_spaces_to_tabs(self) -> None: + if self.mode != EditMode.CHAR: self.setLineMode() self.recordEditMode() f = self.current_pane @@ -3480,14 +3481,14 @@ class FileDiffViewerBase(Gtk.Grid): # update line only if it changed if text != s: self.updateText(f, i, s) - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: # ensure the cursor position is valid self.setCurrentChar(self.current_line, 0, self.selection_line, 0) self.recordEditMode() # adjust indenting of the selected lines by 'offset' soft tabs - def _adjust_indenting(self, offset): - if self.mode != CHAR_MODE: + def _adjust_indenting(self, offset: int) -> None: + if self.mode != EditMode.CHAR: self.setLineMode() # find range of lines to operate upon f = self.current_pane @@ -3513,20 +3514,20 @@ class FileDiffViewerBase(Gtk.Grid): tab_width = self.prefs.getInt('display_tab_width') s = '\t' * (ws // tab_width) + ' ' * (ws % tab_width) self.updateText(f, i, s + text[j:]) - if self.mode == CHAR_MODE: + if self.mode == EditMode.CHAR: # ensure the cursor position is valid self.setCurrentChar(self.current_line, 0, self.selection_line, 0) self.recordEditMode() # 'increase_indenting' action - def increase_indenting(self): + def increase_indenting(self) -> None: self._adjust_indenting(1) # 'decrease_indenting' action - def decrease_indenting(self): + def decrease_indenting(self) -> None: self._adjust_indenting(-1) - def convert_format(self, fmt): + def convert_format(self, fmt: LineEnding) -> None: self.setLineMode() self.recordEditMode() f = self.current_pane @@ -3539,19 +3540,19 @@ class FileDiffViewerBase(Gtk.Grid): self.setFormat(f, fmt) # 'convert_to_dos' action - def convert_to_dos(self): + def convert_to_dos(self) -> None: self.convert_format(LineEnding.DOS_FORMAT) # 'convert_to_mac' action - def convert_to_mac(self): + def convert_to_mac(self) -> None: self.convert_format(LineEnding.MAC_FORMAT) # 'convert_to_unix' action - def convert_to_unix(self): + def convert_to_unix(self) -> None: self.convert_format(LineEnding.UNIX_FORMAT) # copies the selected range of lines from pane 'f_src' to 'f_dst' - def merge_lines(self, f_dst, f_src): + def merge_lines(self, f_dst: int, f_src: int) -> None: self.recordEditMode() self.setLineMode() pane = self.panes[f_dst] @@ -3581,25 +3582,25 @@ class FileDiffViewerBase(Gtk.Grid): self.closeUndoBlock() # 'copy_selection_right' action - def copy_selection_right(self): + def copy_selection_right(self) -> None: f = self.current_pane + 1 if 0 < f < len(self.panes): self.merge_lines(f, f - 1) # 'copy_selection_left' action - def copy_selection_left(self): + def copy_selection_left(self) -> None: f = self.current_pane - 1 if f >= 0 and f + 1 < len(self.panes): self.merge_lines(f, f + 1) # 'copy_left_into_selection' action - def copy_left_into_selection(self): + def copy_left_into_selection(self) -> None: f = self.current_pane if 0 < f < len(self.panes): self.merge_lines(f, f - 1) # 'copy_right_into_selection' action - def copy_right_into_selection(self): + def copy_right_into_selection(self) -> None: f = self.current_pane if f >= 0 and f + 1 < len(self.panes): self.merge_lines(f, f + 1) @@ -3740,18 +3741,19 @@ def createMenu(specs, radio=None, accel_group=None): return menu -ALPHANUMERIC_CLASS = 0 -WHITESPACE_CLASS = 1 -OTHER_CLASS = 2 +class CharacterClass(IntFlag): + ALPHANUMERIC = 0 + WHITESPACE = 1 + OTHER = 2 # maps similar types of characters to a group -def _get_character_class(c): +def _get_character_class(c: str) -> CharacterClass: if c.isalnum() or c == '_': - return ALPHANUMERIC_CLASS + return CharacterClass.ALPHANUMERIC if c.isspace(): - return WHITESPACE_CLASS - return OTHER_CLASS + return CharacterClass.WHITESPACE + return CharacterClass.OTHER # patience diff with difflib-style fallback @@ -3966,36 +3968,36 @@ def _lcs_approx(a, b): # True if the string ends with '\r\n' -def _has_dos_line_ending(s): +def _has_dos_line_ending(s: str) -> bool: return s.endswith('\r\n') # True if the string ends with '\r' -def _has_mac_line_ending(s): +def _has_mac_line_ending(s: str) -> bool: return s.endswith('\r') # True if the string ends with '\n' but not '\r\n' -def _has_unix_line_ending(s): +def _has_unix_line_ending(s: str) -> bool: return s.endswith('\n') and not s.endswith('\r\n') # returns the format mask for a list of strings -def _get_format(ss): - flags = 0 - for s in ss: - if s is not None: - if _has_dos_line_ending(s): +def _get_format(lines: List[Optional[str]]) -> LineEnding: + flags: LineEnding = LineEnding.NO_FORMAT + for line in lines: + if line is not None: + if _has_dos_line_ending(line): flags |= LineEnding.DOS_FORMAT - elif _has_mac_line_ending(s): + elif _has_mac_line_ending(line): flags |= LineEnding.MAC_FORMAT - elif _has_unix_line_ending(s): + elif _has_unix_line_ending(line): flags |= LineEnding.UNIX_FORMAT return flags # convenience method to change the line ending of a string -def _convert_to_format(s, fmt): +def _convert_to_format(s: Optional[str], fmt: LineEnding) -> Optional[str]: if s is not None and fmt != 0: old_format = _get_format([s]) if old_format != 0 and (old_format & fmt) == 0: @@ -4025,14 +4027,14 @@ def _convert_to_format(s, fmt): # in this array so 'blocks' will be an empty array when there are no lines. A # 'cut' at location 'i' means a line 'i-1' and line 'i' belong to different # sections -def _create_block(n): +def _create_block(n: int) -> List[int]: if n > 0: return [n] return [] # returns the two sets of blocks after cutting at 'i' -def _cut_blocks(i, blocks): +def _cut_blocks(i: int, blocks: List[int]) -> Tuple[List[int], List[int]]: pre, post, nlines = [], [], 0 for b in blocks: if nlines >= i: @@ -4048,7 +4050,7 @@ def _cut_blocks(i, blocks): # returns a set of blocks containing all of the cuts in the inputs -def _merge_blocks(leftblocks, rightblocks): +def _merge_blocks(leftblocks: List[int], rightblocks: List[int]) -> List[int]: leftblocks, rightblocks, b = leftblocks[:], rightblocks[:], [] while len(leftblocks) > 0: nleft, nright = leftblocks[0], rightblocks[0] @@ -4122,14 +4124,14 @@ def _remove_null_lines(blocks, lines_set): # returns true if the string only contains whitespace characters -def _is_blank(s): +def _is_blank(s: str) -> bool: for c in utils.whitespace: s = s.replace(c, '') return len(s) == 0 # use Pango.SCALE instead of Pango.PIXELS to avoid overflow exception -def _pixels(size): +def _pixels(size: int) -> int: return int(size / Pango.SCALE + 0.5)