From caf3fb176235b77370ea00e1bd603803edbeee09 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 28 Feb 2016 17:10:28 +0200 Subject: [PATCH 01/36] - Implemented custom input prompts via PromtEditor - Added PromptEditor class for promts - Began refactoring Viewer into smaller modules --- suplemon/editor.py | 69 +++++++++++++++++++ suplemon/main.py | 1 + suplemon/ui.py | 49 +++++-------- suplemon/viewer.py | 166 ++++++++++++++++++++++++--------------------- 4 files changed, 174 insertions(+), 111 deletions(-) diff --git a/suplemon/editor.py b/suplemon/editor.py index b7d41e8..75acdde 100644 --- a/suplemon/editor.py +++ b/suplemon/editor.py @@ -812,3 +812,72 @@ def run_operation(self, operation): self.app.trigger_event_after(operation) return result return False + + +class PromptEditor(Editor): + """An input prompt based on the Editor.""" + def __init__(self, app, window): + Editor.__init__(self, app, window) + self.ready = 0 + self.input_func = lambda: False + self.caption = "" + + def set_config(self, config): + """Set the configuration for the editor.""" + # Override showing line numbers + config["show_line_nums"] = False + Editor.set_config(self, config) + + def set_input_source(self, input_func): + # Set the input function to use while looping for input + self.input_func = input_func + + def on_ready(self): + """Accepts the current input.""" + self.ready = 1 + return + + def on_cancel(self): + """Cancels the input prompt.""" + self.set_data("") + self.ready = 1 + return + + def line_offset(self): + """Get the x coordinate of beginning of line.""" + return len(self.caption)+1 + + def render_line_contents(self, line, pos, x_offset, max_len): + """Render the prompt line.""" + x_offset = self.line_offset() + # Render the caption + self.window.addstr(pos[1], 0, self.caption) + # Render input + self.render_line_normal(line, pos, x_offset, max_len) + + def handle_input(self, event): + """Handle special bindings for the prompt.""" + name = event.key_name + if name in ["ctrl+c", "escape"]: + self.on_cancel() + return False + if name == "enter": + self.on_ready() + return False + + return Editor.handle_input(self, event) + + def get_input(self, caption="", initial=""): + """Get text input from the user via the prompt.""" + self.caption = caption + self.set_data(initial) + self.end() + self.resize() + self.render() + while not self.ready: + event = self.input_func(True) # blocking + if event: + self.handle_input(event) + self.resize() + self.render() + return self.get_data() diff --git a/suplemon/main.py b/suplemon/main.py index e44aa5a..d89a225 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -543,6 +543,7 @@ def setup_editor(self, editor): """Setup an editor instance with configuration.""" config = self.config["editor"] editor.set_config(config) + editor.init() ########################################################################### # File operations diff --git a/suplemon/ui.py b/suplemon/ui.py index 8cd57d4..ed4972e 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -6,6 +6,7 @@ import os import logging +from .editor import Editor, PromptEditor from .key_mappings import key_map # Curses can't be imported yet but we'll @@ -406,42 +407,24 @@ def show_legend(self): x += len(key[1])+2 self.legend_win.refresh() - def show_capture_status(self, s="", value=""): - """Show status when capturing input.""" - self.status_win.erase() - self.status_win.addstr(0, 0, s, curses.A_REVERSE) - self.status_win.addstr(0, len(s), value) - - def _process_query_key(self, key): - """Process keystrokes from the Textbox window.""" - if key in [3, 27]: # Support canceling query with Ctrl+C or ESC - raise KeyboardInterrupt - # Standardize some keycodes - rewrite = { - 127: 263, - 8: 263, - } - # self.logger.debug("Query key input: {0}".format(str(key))) - if key in rewrite.keys(): - key = rewrite[key] - return key - def _query(self, text, initial=""): """Ask for text input via the status bar.""" - self.show_capture_status(text, initial) - self.text_input = curses.textpad.Textbox(self.status_win) - try: - out = self.text_input.edit(self._process_query_key) - except: - return False - # If input begins with prompt, remove the prompt text - if len(out) >= len(text): - if out[:len(text)] == text: - out = out[len(text):] - if len(out) > 0 and out[-1] == " ": - out = out[:-1] - out = out.rstrip("\r\n") + # Disable render blocking + blocking = self.app.block_rendering + self.app.block_rendering = 0 + + # Create our text input + self.text_input = PromptEditor(self.app, self.status_win) + self.text_input.set_config(self.app.config["editor"].copy()) + self.text_input.set_input_source(self.get_input) + + # Get input from the user + out = self.text_input.get_input(text, initial) + + # Restore render blocking + self.app.block_rendering = blocking + return out def query(self, text, initial=""): diff --git a/suplemon/viewer.py b/suplemon/viewer.py index 6f9e3dd..322f90f 100644 --- a/suplemon/viewer.py +++ b/suplemon/viewer.py @@ -21,7 +21,7 @@ pygments = False -class Viewer: +class BaseViewer: def __init__(self, app, window): """ Handle Viewer initialization @@ -50,66 +50,8 @@ def __init__(self, app, window): self.x_scroll = 0 self.cursors = [Cursor()] - # Lexer for translating tokens to strings - self.lexer = None - # Built in syntax definition (for commenting etc.) - self.syntax = None - # Normal Pygments lexer - self.pygments_syntax = None - - self.setup_linelight() - if self.app.config["editor"]["show_highlighting"]: - self.setup_highlight() - - def setup_linelight(self): - """Setup line based highlighting.""" - ext = self.file_extension - # Check if a file extension is redefined - # Maps e.g. 'scss' to 'css' - if ext in self.extension_map.keys(): - ext = self.extension_map[ext] # Use it - curr_path = os.path.dirname(os.path.realpath(__file__)) - - filename = ext + ".py" - path = os.path.join(curr_path, "linelight", filename) - module = False - if os.path.isfile(path): - try: - module = imp.load_source(ext, path) - except: - self.logger.error("Failed to load syntax file '{0}'!".format(path), exc_info=True) - else: - return False - - if not module or "Syntax" not in dir(module): - self.logger.error("File doesn't match API!") - return False - self.syntax = module.Syntax() - - def setup_highlight(self): - """Setup Pygments based highlighting.""" - if not pygments: - # If Pygments lib not available - self.logger.info("Pygments not available, please install python3-pygments for proper syntax highlighting.") - return False - self.lexer = Lexer(self.app) - ext = self.file_extension.lower() - if not ext: - return False - # Check if a file extension is redefined - # Maps e.g. 'scss' to 'css' - if ext in self.extension_map.keys(): - ext = self.extension_map[ext] # Use it - try: - self.pygments_syntax = pygments.lexers.get_lexer_by_name(ext) - self.logger.debug("Loaded Pygments lexer '{0}'.".format(ext)) - except: - self.logger.debug("Failed to load Pygments lexer '{0}'.".format(ext)) - return False - if ext == "php": - # Hack to highlight PHP even without tags - self.pygments_syntax.options.update({"startinline": 1}) - self.pygments_syntax.startinline = 1 + def init(self): + pass def size(self): """Get editor size (x,y). (Deprecated, use get_size).""" @@ -183,20 +125,6 @@ def get_lines_with_cursors(self): line_nums.sort() return line_nums - def get_line_color(self, raw_line): - """Return a color based on line contents. - - :param str raw_line: The line from which to get a color value. - :return: A color value for given raw_data. - :rtype: int - """ - if self.syntax: - try: - return self.syntax.get_color(raw_line) - except: - return 0 - return 0 - def get_data(self): """Get editor contents. @@ -266,7 +194,7 @@ def set_file_extension(self, ext): if ext and ext != self.file_extension: self.file_extension = ext self.setup_linelight() - if self.app.config["editor"]["show_highlighting"]: + if self.config["show_highlighting"]: self.setup_highlight() def add_cursor(self, cursor): @@ -346,10 +274,10 @@ def render_line_contents(self, line, pos, x_offset, max_len): :param x_offset: Offset from left edge of screen. Currently same as x position. :param max_len: Maximum amount of chars that will fit on screen. """ - show_highlighting = self.app.config["editor"]["show_highlighting"] + show_highlighting = self.config["show_highlighting"] if pygments and show_highlighting and self.pygments_syntax and self.app.themes.current_theme: self.render_line_pygments(line, pos, x_offset, max_len) - elif self.app.config["editor"]["show_line_colors"]: + elif self.config["show_line_colors"]: self.render_line_linelight(line, pos, x_offset, max_len) else: self.render_line_normal(line, pos, x_offset, max_len) @@ -621,3 +549,85 @@ def purge_line_cursors(self, line_no): for line_cursors in cursor: self.remove_cursor(cursor) return True + + +class Viewer(BaseViewer): + def __init__(self, app, window): + BaseViewer.__init__(self, app, window) + + # Lexer for translating tokens to strings + self.lexer = None + # Built in syntax definition (for commenting etc.) + self.syntax = None + # Normal Pygments lexer + self.pygments_syntax = None + + self.setup_linelight() + + def init(self): + if self.config["show_highlighting"]: + self.setup_highlight() + + def setup_linelight(self): + """Setup line based highlighting.""" + ext = self.file_extension + # Check if a file extension is redefined + # Maps e.g. 'scss' to 'css' + if ext in self.extension_map.keys(): + ext = self.extension_map[ext] # Use it + curr_path = os.path.dirname(os.path.realpath(__file__)) + + filename = ext + ".py" + path = os.path.join(curr_path, "linelight", filename) + module = False + if os.path.isfile(path): + try: + module = imp.load_source(ext, path) + except: + self.logger.error("Failed to load syntax file '{0}'!".format(path), exc_info=True) + else: + return False + + if not module or "Syntax" not in dir(module): + self.logger.error("File doesn't match API!") + return False + self.syntax = module.Syntax() + + def setup_highlight(self): + """Setup Pygments based highlighting.""" + if not pygments: + # If Pygments lib not available + self.logger.info("Pygments not available, please install python3-pygments for proper syntax highlighting.") + return False + self.lexer = Lexer(self.app) + ext = self.file_extension.lower() + if not ext: + return False + # Check if a file extension is redefined + # Maps e.g. 'scss' to 'css' + if ext in self.extension_map.keys(): + ext = self.extension_map[ext] # Use it + try: + self.pygments_syntax = pygments.lexers.get_lexer_by_name(ext) + self.logger.debug("Loaded Pygments lexer '{0}'.".format(ext)) + except: + self.logger.debug("Failed to load Pygments lexer '{0}'.".format(ext)) + return False + if ext == "php": + # Hack to highlight PHP even without tags + self.pygments_syntax.options.update({"startinline": 1}) + self.pygments_syntax.startinline = 1 + + def get_line_color(self, raw_line): + """Return a color based on line contents. + + :param str raw_line: The line from which to get a color value. + :return: A color value for given raw_data. + :rtype: int + """ + if self.syntax: + try: + return self.syntax.get_color(raw_line) + except: + return 0 + return 0 From 3d197cc77aaf717b964cc3bf9367d62b5127402f Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 28 Feb 2016 17:25:48 +0200 Subject: [PATCH 02/36] Polishing. --- suplemon/editor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/suplemon/editor.py b/suplemon/editor.py index 75acdde..d764cf0 100644 --- a/suplemon/editor.py +++ b/suplemon/editor.py @@ -871,9 +871,13 @@ def get_input(self, caption="", initial=""): """Get text input from the user via the prompt.""" self.caption = caption self.set_data(initial) - self.end() + self.end() # Move to the end of the initial text + + # TODO: Still can't figure out why resize is needed for succesful render() self.resize() self.render() + + # Run the input loop until ready while not self.ready: event = self.input_func(True) # blocking if event: From 3362eaaa195caadb23864f83ea2c50515bc5cdf9 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 28 Feb 2016 18:01:04 +0200 Subject: [PATCH 03/36] Return False when promt is canceled. --- suplemon/editor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/suplemon/editor.py b/suplemon/editor.py index d764cf0..f4a238c 100644 --- a/suplemon/editor.py +++ b/suplemon/editor.py @@ -726,7 +726,7 @@ def find(self, what, findall=False): y += 1 if not new_cursors: - self.app.set_status("Can't find '" + what + "'") + self.app.set_status("Can't find '{0}'".format(what)) # self.last_find = "" return else: @@ -819,6 +819,7 @@ class PromptEditor(Editor): def __init__(self, app, window): Editor.__init__(self, app, window) self.ready = 0 + self.canceled = 0 self.input_func = lambda: False self.caption = "" @@ -841,6 +842,7 @@ def on_cancel(self): """Cancels the input prompt.""" self.set_data("") self.ready = 1 + self.canceled = 1 return def line_offset(self): @@ -884,4 +886,6 @@ def get_input(self, caption="", initial=""): self.handle_input(event) self.resize() self.render() + if self.canceled: + return False return self.get_data() From c31737ee9731e7497eea3fa8747ca216940cb254 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 28 Feb 2016 18:03:30 +0200 Subject: [PATCH 04/36] Better string formatting. --- suplemon/helpers.py | 2 +- suplemon/main.py | 10 +++++----- suplemon/modules/battery.py | 4 ++-- suplemon/modules/linter.py | 4 ++-- suplemon/ui.py | 20 ++++++++++---------- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/suplemon/helpers.py b/suplemon/helpers.py index 245513c..fdd26aa 100644 --- a/suplemon/helpers.py +++ b/suplemon/helpers.py @@ -47,7 +47,7 @@ def multisplit(data, delimiters): def get_error_info(): """Return info about last error.""" - msg = str(traceback.format_exc()) + "\n" + str(sys.exc_info()) + msg = "{0}\n{1}".format(str(traceback.format_exc()), str(sys.exc_info())) return msg diff --git a/suplemon/main.py b/suplemon/main.py index d89a225..136c132 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -445,7 +445,7 @@ def run_command(self, data): self.logger.exception("Running command failed!") return False else: - self.set_status("Command '" + cmd + "' not found.") + self.set_status("Command '{0}' not found.".format(cmd)) return False return True @@ -560,7 +560,7 @@ def open(self): return True if not self.open_file(name): - self.set_status("Failed to load '" + name + "'") + self.set_status("Failed to load '{0}'".format(name)) return False self.switch_to_file(self.last_file_index()) return True @@ -583,11 +583,11 @@ def save_file(self, file=False): if not f.get_name(): return self.save_file_as(f) if f.save(): - self.set_status("Saved [" + helpers.curr_time_sec() + "] '" + f.name + "'") + self.set_status("Saved [{0}] '{1}'".format(helpers.curr_time_sec(), f.name)) if f.path() == self.config.path(): self.reload_config() return True - self.set_status("Couldn't write to '" + f.name + "'") + self.set_status("Couldn't write to '{0}'".format(f.name)) return False def save_file_as(self, file=False): @@ -601,7 +601,7 @@ def save_file_as(self, file=False): def reload_file(self): """Reload the current file.""" - if self.ui.query_bool("Reload '" + self.get_file().name + "'?"): + if self.ui.query_bool("Reload '{0}'?".format(self.get_file().name)): if self.get_file().reload(): return True return False diff --git a/suplemon/modules/battery.py b/suplemon/modules/battery.py index 494e77e..b849c3e 100644 --- a/suplemon/modules/battery.py +++ b/suplemon/modules/battery.py @@ -32,9 +32,9 @@ def value_str(self): val = self.value() if val: if self.app.config["app"]["use_unicode_symbols"]: - return "\u26A1" + str(val) + "%" + return "\u26A1{0}%".format(str(val)) else: - return "BAT " + str(val) + "%" + return "BAT {0}%".format(str(val)) return "" def get_status(self): diff --git a/suplemon/modules/linter.py b/suplemon/modules/linter.py index ee95c2d..830eb99 100644 --- a/suplemon/modules/linter.py +++ b/suplemon/modules/linter.py @@ -28,7 +28,7 @@ def run(self, app, editor, args): """Run the linting command.""" editor = self.app.get_file().get_editor() count = self.get_msg_count(editor) - status = str(count) + " lines with linting errors in this file." + status = "{0} lines with linting errors in this file.".format(str(count)) self.app.set_status(status) def mainloop(self, event): @@ -41,7 +41,7 @@ def mainloop(self, event): line_no = cursor.y + 1 msg = self.get_msgs_on_line(editor, cursor.y) if msg: - self.app.set_status("Line " + str(line_no) + ": " + msg) + self.app.set_status("Line {0}: {1}".format(str(line_no),msg)) def lint_current_file(self, event): self.lint_file(self.app.get_file()) diff --git a/suplemon/ui.py b/suplemon/ui.py index ed4972e..7d2717c 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -286,10 +286,10 @@ def show_top_status(self): display = self.app.config["display"] head_parts = [] if display["show_app_name"]: - name_str = "Suplemon Editor v" + self.app.version + " -" + name_str = "Suplemon Editor v{0} -".format(self.app.version) if self.app.config["app"]["use_unicode_symbols"]: logo = "\u2688" # Simple lemon (filled) - name_str = " " + logo + " " + name_str + name_str = " {0} {1}".format(logo, name_str) head_parts.append(name_str) # Add module statuses to the status bar @@ -328,7 +328,7 @@ def file_list_str(self): append += ["", is_changed_symbol][f.is_changed()] fname = prepend + f.name + append if not str_list: - str_list.append("[" + fname + "]") + str_list.append("[{0}]".format(fname)) else: str_list.append(fname) return " ".join(str_list) @@ -338,15 +338,15 @@ def show_bottom_status(self): editor = self.app.get_editor() size = self.get_size() cur = editor.get_cursor() - data = "@ " + str(cur[0]) + "," + str(cur[1]) + " " + \ - "cur:" + str(len(editor.cursors)) + " " + \ - "buf:" + str(len(editor.get_buffer())) + data = "@ {0},{1} cur:{2} buf:{3}".format( + str(cur[0]), + str(cur[1]), + str(len(editor.cursors)), + str(len(editor.get_buffer())) + ) + if self.app.config["app"]["debug"]: data += " cs:"+str(editor.current_state)+" hist:"+str(len(editor.history)) # Undo / Redo debug - # if editor.last_find: - # find = editor.last_find - # if len(find) > 10:find = find[:10]+"..." - # data = "find:'"+find+"' " + data # Add module statuses to the status bar for name in self.app.modules.modules.keys(): From c368529072169a48741d13fd8ce577db0395f83e Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 28 Feb 2016 18:16:14 +0200 Subject: [PATCH 05/36] Linting. --- suplemon/main.py | 2 +- suplemon/modules/linter.py | 2 +- suplemon/ui.py | 2 +- suplemon/viewer.py | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/suplemon/main.py b/suplemon/main.py index 136c132..df1851d 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -149,7 +149,7 @@ def load(self): self.load_files() self.running = 1 self.trigger_event_after("app_loaded") - + def on_input(self, event): # Handle the input or give it to the editor if not self.handle_input(event): diff --git a/suplemon/modules/linter.py b/suplemon/modules/linter.py index 830eb99..076b247 100644 --- a/suplemon/modules/linter.py +++ b/suplemon/modules/linter.py @@ -41,7 +41,7 @@ def mainloop(self, event): line_no = cursor.y + 1 msg = self.get_msgs_on_line(editor, cursor.y) if msg: - self.app.set_status("Line {0}: {1}".format(str(line_no),msg)) + self.app.set_status("Line {0}: {1}".format(str(line_no), msg)) def lint_current_file(self, event): self.lint_file(self.app.get_file()) diff --git a/suplemon/ui.py b/suplemon/ui.py index 7d2717c..9369669 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -6,7 +6,7 @@ import os import logging -from .editor import Editor, PromptEditor +from .editor import PromptEditor from .key_mappings import key_map # Curses can't be imported yet but we'll diff --git a/suplemon/viewer.py b/suplemon/viewer.py index 322f90f..9e863d8 100644 --- a/suplemon/viewer.py +++ b/suplemon/viewer.py @@ -237,7 +237,7 @@ def render(self): """Render the editor curses window.""" if self.app.block_rendering: return - + self.window.erase() i = 0 max_y = self.get_size()[1] @@ -390,7 +390,7 @@ def render_cursors(self): """Render editor window cursors.""" if self.app.block_rendering: return - + max_x, max_y = self.get_size() for cursor in self.cursors: x = cursor.x - self.x_scroll + self.line_offset() @@ -458,7 +458,7 @@ def move_cursors(self, delta=None, noupdate=False): """Move all cursors with delta. To avoid refreshing the screen set noupdate to True.""" if self.app.block_rendering: noupdate = True - + for cursor in self.cursors: if delta: if delta[0] != 0 and cursor.x >= 0: From 310e3ac99cca75f14ad689f3c8c6ff12eefad2d1 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 28 Feb 2016 19:40:17 +0200 Subject: [PATCH 06/36] Refactored keymap to separate file with Sublime Text syntax. --- suplemon/config.py | 58 +++++++++++++++++++++----------- suplemon/config/defaults.json | 63 ++--------------------------------- suplemon/config/keymap.json | 63 +++++++++++++++++++++++++++++++++++ suplemon/editor.py | 2 +- suplemon/main.py | 23 +++++++++---- suplemon/ui.py | 1 - 6 files changed, 121 insertions(+), 89 deletions(-) create mode 100644 suplemon/config/keymap.json diff --git a/suplemon/config.py b/suplemon/config.py index 0d93a0a..27c5455 100644 --- a/suplemon/config.py +++ b/suplemon/config.py @@ -14,12 +14,15 @@ class Config: def __init__(self, app): self.app = app self.logger = logging.getLogger(__name__) - self.default_filename = "defaults.json" + self.default_config_filename = "defaults.json" + self.default_keymap_filename = "keymap.json" self.config_filename = "suplemon-config.json" + self.keymap_filename = "suplemon-keymap.json" self.home_dir = os.path.expanduser("~") self.fpath = os.path.join(self.home_dir, ".config", "suplemon") self.defaults = {} + self.keymap = {} self.config = {} def init(self): @@ -29,6 +32,9 @@ def init(self): def path(self): return os.path.join(self.fpath, self.config_filename) + def keymap_path(self): + return os.path.join(self.fpath, self.keymap_filename) + def set_path(self, path): parts = os.path.split(path) self.fpath = parts[0] @@ -48,17 +54,43 @@ def load(self): self.logger.info("Failed to load config file '{0}'.".format(path)) self.config = dict(self.defaults) return False + self.load_keys() return config - def load_defaults(self): - path = os.path.join(self.app.path, "config", self.default_filename) + def load_keys(self): + path = self.keymap_path() + keymap = False if not os.path.exists(path): + self.logger.debug("Keymap file '{0}' doesn't exist.".format(path)) return False - defaults = self.load_config_file(path) - if not defaults: - self.logger.warning("Failed to load default config file! ('{0}')".format(path)) + keymap = self.load_config_file(path) + if not keymap: + self.logger.info("Failed to load keymap file '{0}'.".format(path)) return False - self.defaults = defaults + self.keymap[-1:] = keymap # Append the user key map + return True + + def load_defaults(self): + if not self.load_default_config() or not self.load_default_keys(): + return False + return True + + def load_default_config(self): + path = os.path.join(self.app.path, "config", self.default_config_filename) + config = self.load_config_file(path) + if not config: + self.logger.error("Failed to load default config file '{0}'!".format(path)) + return False + self.defaults = config + return True + + def load_default_keys(self): + path = os.path.join(self.app.path, "config", self.default_keymap_filename) + config = self.load_config_file(path) + if not config: + self.logger.error("Failed to load default keymap file '{0}'!".format(path)) + return False + self.keymap = config return True def reload(self): @@ -82,20 +114,8 @@ def merge_defaults(self, config): for sec_key in curr_item.keys(): if sec_key not in config[prim_key].keys(): config[prim_key][sec_key] = curr_item[sec_key] - self.merge_keys(config) return config - def merge_keys(self, config): - """Fill in config with default keys.""" - # Do merge for app and editor keys - for dest in ["app", "editor"]: - key_config = config[dest]["keys"] - key_defaults = self.defaults[dest]["keys"] - for key in key_defaults.keys(): - # Fill in each key that's not defined yet - if key not in key_config.keys(): - key_config[key] = key_defaults[key] - def load_config_file(self, path): try: f = open(path) diff --git a/suplemon/config/defaults.json b/suplemon/config/defaults.json index 7404405..92af126 100644 --- a/suplemon/config/defaults.json +++ b/suplemon/config/defaults.json @@ -19,27 +19,7 @@ // How long curses will wait to detect ESC key "escdelay": 50, // Wether to use special unicode symbols for decoration - "use_unicode_symbols": true, - // Key bindings for app functions - "keys": { - "ctrl+h": "help", - "ctrl+s": "save_file", - "ctrl+e": "run_command", - "ctrl+f": "find", - "ctrl+g": "go_to", - "ctrl+o": "open", - "ctrl+w": "close_file", - "ctrl+n": "new_file", - "ctrl+q": "ask_exit", - "ctrl+p": "comment", - "ctrl+pageup": "next_file", - "ctrl+pagedown": "prev_file", - "f1": "save_file_as", - "f2": "reload_file", - "f7": "toggle_whitespace", - "f8": "toggle_mouse", - "f11": "toggle_fullscreen" - } + "use_unicode_symbols": true }, // Editor settings "editor": { @@ -91,46 +71,7 @@ // Wether to use copy/paste across multiple files "use_global_buffer": true, // Find with regex by default - "regex_find": false, - // Key bindings for editor functions - "keys": { - "up": "arrow_up", - "down": "arrow_down", - "left": "arrow_left", - "right": "arrow_right", - "enter": "enter", - "backspace": "backspace", - "delete": "delete", - "insert": "insert", - "tab": "tab", - "shift+tab": "untab", - "home": "home", - "end": "end", - "escape": "escape", - "pageup": "page_up", - "pagedown": "page_down", - "f5": "undo", - "f6": "redo", - "f9": "toggle_line_nums", - "f10": "toggle_line_ends", - "f11": "toggle_highlight", - "alt+up": "new_cursor_up", - "alt+down": "new_cursor_down", - "alt+left": "new_cursor_left", - "alt+right": "new_cursor_right", - "alt+pageup": "push_up", - "alt+pagedown": "push_down", - "ctrl+c": "copy", - "ctrl+x": "cut", - "ctrl+k": "duplicate_line", - "ctrl+v": "insert", - "ctrl+d": "find_next", - "ctrl+a": "find_all", - "ctrl+left": "jump_left", - "ctrl+right": "jump_right", - "ctrl+up": "jump_up", - "ctrl+down": "jump_down" - } + "regex_find": false }, // UI Display Settings "display": { diff --git a/suplemon/config/keymap.json b/suplemon/config/keymap.json new file mode 100644 index 0000000..e33f797 --- /dev/null +++ b/suplemon/config/keymap.json @@ -0,0 +1,63 @@ +// Suplemon Default Key Map + +// This file contains the default key map for Suplemon and should not be edited. +// If the file doesn't exist, or if it has errors Suplemon can't run. +// Suplemon supports single line comments in JSON as seen here. + +[ + // App + {"keys": "ctrl+h", "command": "help"}, + {"keys": "ctrl+s", "command": "save_file"}, + {"keys": "ctrl+e", "command": "run_command"}, + {"keys": "ctrl+f", "command": "find"}, + {"keys": "ctrl+g", "command": "go_to"}, + {"keys": "ctrl+o", "command": "open"}, + {"keys": "ctrl+w", "command": "close_file"}, + {"keys": "ctrl+n", "command": "new_file"}, + {"keys": "ctrl+q", "command": "ask_exit"}, + {"keys": "ctrl+p", "command": "comment"}, + {"keys": "ctrl+pageup", "command": "next_file"}, + {"keys": "ctrl+pagedown", "command": "prev_file"}, + {"keys": "f1", "command": "save_file_as"}, + {"keys": "f2", "command": "reload_file"}, + {"keys": "f7", "command": "toggle_whitespace"}, + {"keys": "f8", "command": "toggle_mouse"}, + {"keys": "f11", "command": "toggle_fullscreen"}, + // Editor + {"keys": "up", "command": "arrow_up"}, + {"keys": "down", "command": "arrow_down"}, + {"keys": "left", "command": "arrow_left"}, + {"keys": "right", "command": "arrow_right"}, + {"keys": "enter", "command": "enter"}, + {"keys": "backspace", "command": "backspace"}, + {"keys": "delete", "command": "delete"}, + {"keys": "insert", "command": "insert"}, + {"keys": "tab", "command": "tab"}, + {"keys": "shift+tab", "command": "untab"}, + {"keys": "home", "command": "home"}, + {"keys": "end", "command": "end"}, + {"keys": "escape", "command": "escape"}, + {"keys": "pageup", "command": "page_up"}, + {"keys": "pagedown", "command": "page_down"}, + {"keys": "f5", "command": "undo"}, + {"keys": "f6", "command": "redo"}, + {"keys": "f9", "command": "toggle_line_nums"}, + {"keys": "f10", "command": "toggle_line_ends"}, + {"keys": "f11", "command": "toggle_highlight"}, + {"keys": "alt+up", "command": "new_cursor_up"}, + {"keys": "alt+down", "command": "new_cursor_down"}, + {"keys": "alt+left", "command": "new_cursor_left"}, + {"keys": "alt+right", "command": "new_cursor_right"}, + {"keys": "alt+pageup", "command": "push_up"}, + {"keys": "alt+pagedown", "command": "push_down"}, + {"keys": "ctrl+c", "command": "copy"}, + {"keys": "ctrl+x", "command": "cut"}, + {"keys": "ctrl+k", "command": "duplicate_line"}, + {"keys": "ctrl+v", "command": "insert"}, + {"keys": "ctrl+d", "command": "find_next"}, + {"keys": "ctrl+a", "command": "find_all"}, + {"keys": "ctrl+left", "command": "jump_left"}, + {"keys": "ctrl+right", "command": "jump_right"}, + {"keys": "ctrl+up", "command": "jump_up"}, + {"keys": "ctrl+down", "command": "jump_down"} +] \ No newline at end of file diff --git a/suplemon/editor.py b/suplemon/editor.py index f4a238c..490d35c 100644 --- a/suplemon/editor.py +++ b/suplemon/editor.py @@ -112,7 +112,7 @@ def get_buffer(self): def get_key_bindings(self): """Get list of editor key bindings.""" - return self.config["keys"] + return self.app.get_key_bindings() def set_buffer(self, buffer): """Sets local or global buffer depending on config.""" diff --git a/suplemon/main.py b/suplemon/main.py index df1851d..90b436d 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -211,7 +211,10 @@ def get_file_index(self, file_obj): def get_key_bindings(self): """Return the list of key bindings.""" - return self.config["app"]["keys"] + bindings = {} + for binding in self.config.keymap: + bindings[binding["keys"]] = binding["command"] + return bindings def get_event_bindings(self): """Return the dict of event bindings.""" @@ -225,12 +228,12 @@ def set_key_binding(self, key, operation): :param key: What key or key combination to bind. :param str operation: Which operation to run. """ - self.get_key_bindings()[key] = operation + self.config.keymap.prepend({"keys": key, "command": operation}) def set_event_binding(self, event, when, callback): """Bind a callback to be run before or after an event. - Bind callback to run before or after event occurs. Th when parameter + Bind callback to run before or after event occurs. The when parameter should be 'before' or 'after'. If using 'before' the callback can inhibit running the event if it returns True @@ -294,14 +297,18 @@ def handle_key(self, event): :rtype: boolean """ key_bindings = self.get_key_bindings() + + operation = None if event.key_name in key_bindings.keys(): operation = key_bindings[event.key_name] elif event.key_code in key_bindings.keys(): operation = key_bindings[event.key_code] - else: - return False - self.run_operation(operation) - return True + + if operation in self.operations.keys(): + self.run_operation(operation) + return True + + return False def handle_mouse(self, event): """Handle a mouse input event. @@ -433,6 +440,8 @@ def run_command(self, data): """Run editor commands.""" parts = data.split(" ") cmd = parts[0].lower() + if cmd in self.operations.keys(): + return self.run_operation(cmd) args = " ".join(parts[1:]) self.logger.debug("Looking for command '{0}'".format(cmd)) if cmd in self.modules.modules.keys(): diff --git a/suplemon/ui.py b/suplemon/ui.py index 9369669..8971605 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -469,7 +469,6 @@ def get_input(self, blocking=True): event.set_key_name("ctrl+c") return event except: - self.logger.debug("Failed to get input!") # No input available return False finally: From a3555966a4d8b2b53f943d67f6ce518ba73b4fa8 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 28 Feb 2016 20:00:25 +0200 Subject: [PATCH 07/36] Tweaked keymap syntax and added Ctrl+Z for undo. --- suplemon/config/keymap.json | 106 ++++++++++++++++++------------------ suplemon/key_mappings.py | 2 +- suplemon/main.py | 5 +- 3 files changed, 57 insertions(+), 56 deletions(-) diff --git a/suplemon/config/keymap.json b/suplemon/config/keymap.json index e33f797..5c1c011 100644 --- a/suplemon/config/keymap.json +++ b/suplemon/config/keymap.json @@ -6,58 +6,58 @@ [ // App - {"keys": "ctrl+h", "command": "help"}, - {"keys": "ctrl+s", "command": "save_file"}, - {"keys": "ctrl+e", "command": "run_command"}, - {"keys": "ctrl+f", "command": "find"}, - {"keys": "ctrl+g", "command": "go_to"}, - {"keys": "ctrl+o", "command": "open"}, - {"keys": "ctrl+w", "command": "close_file"}, - {"keys": "ctrl+n", "command": "new_file"}, - {"keys": "ctrl+q", "command": "ask_exit"}, - {"keys": "ctrl+p", "command": "comment"}, - {"keys": "ctrl+pageup", "command": "next_file"}, - {"keys": "ctrl+pagedown", "command": "prev_file"}, - {"keys": "f1", "command": "save_file_as"}, - {"keys": "f2", "command": "reload_file"}, - {"keys": "f7", "command": "toggle_whitespace"}, - {"keys": "f8", "command": "toggle_mouse"}, - {"keys": "f11", "command": "toggle_fullscreen"}, + {"keys": ["ctrl+h"], "command": "help"}, + {"keys": ["ctrl+s"], "command": "save_file"}, + {"keys": ["ctrl+e"], "command": "run_command"}, + {"keys": ["ctrl+f"], "command": "find"}, + {"keys": ["ctrl+g"], "command": "go_to"}, + {"keys": ["ctrl+o"], "command": "open"}, + {"keys": ["ctrl+w"], "command": "close_file"}, + {"keys": ["ctrl+n"], "command": "new_file"}, + {"keys": ["ctrl+q"], "command": "ask_exit"}, + {"keys": ["ctrl+p"], "command": "comment"}, + {"keys": ["ctrl+pageup"], "command": "next_file"}, + {"keys": ["ctrl+pagedown"], "command": "prev_file"}, + {"keys": ["f1"], "command": "save_file_as"}, + {"keys": ["f2"], "command": "reload_file"}, + {"keys": ["f7"], "command": "toggle_whitespace"}, + {"keys": ["f8"], "command": "toggle_mouse"}, + {"keys": ["f11"], "command": "toggle_fullscreen"}, // Editor - {"keys": "up", "command": "arrow_up"}, - {"keys": "down", "command": "arrow_down"}, - {"keys": "left", "command": "arrow_left"}, - {"keys": "right", "command": "arrow_right"}, - {"keys": "enter", "command": "enter"}, - {"keys": "backspace", "command": "backspace"}, - {"keys": "delete", "command": "delete"}, - {"keys": "insert", "command": "insert"}, - {"keys": "tab", "command": "tab"}, - {"keys": "shift+tab", "command": "untab"}, - {"keys": "home", "command": "home"}, - {"keys": "end", "command": "end"}, - {"keys": "escape", "command": "escape"}, - {"keys": "pageup", "command": "page_up"}, - {"keys": "pagedown", "command": "page_down"}, - {"keys": "f5", "command": "undo"}, - {"keys": "f6", "command": "redo"}, - {"keys": "f9", "command": "toggle_line_nums"}, - {"keys": "f10", "command": "toggle_line_ends"}, - {"keys": "f11", "command": "toggle_highlight"}, - {"keys": "alt+up", "command": "new_cursor_up"}, - {"keys": "alt+down", "command": "new_cursor_down"}, - {"keys": "alt+left", "command": "new_cursor_left"}, - {"keys": "alt+right", "command": "new_cursor_right"}, - {"keys": "alt+pageup", "command": "push_up"}, - {"keys": "alt+pagedown", "command": "push_down"}, - {"keys": "ctrl+c", "command": "copy"}, - {"keys": "ctrl+x", "command": "cut"}, - {"keys": "ctrl+k", "command": "duplicate_line"}, - {"keys": "ctrl+v", "command": "insert"}, - {"keys": "ctrl+d", "command": "find_next"}, - {"keys": "ctrl+a", "command": "find_all"}, - {"keys": "ctrl+left", "command": "jump_left"}, - {"keys": "ctrl+right", "command": "jump_right"}, - {"keys": "ctrl+up", "command": "jump_up"}, - {"keys": "ctrl+down", "command": "jump_down"} + {"keys": ["up"], "command": "arrow_up"}, + {"keys": ["down"], "command": "arrow_down"}, + {"keys": ["left"], "command": "arrow_left"}, + {"keys": ["right"], "command": "arrow_right"}, + {"keys": ["enter"], "command": "enter"}, + {"keys": ["backspace"], "command": "backspace"}, + {"keys": ["delete"], "command": "delete"}, + {"keys": ["insert"], "command": "insert"}, + {"keys": ["tab"], "command": "tab"}, + {"keys": ["shift+tab"], "command": "untab"}, + {"keys": ["home"], "command": "home"}, + {"keys": ["end"], "command": "end"}, + {"keys": ["escape"], "command": "escape"}, + {"keys": ["pageup"], "command": "page_up"}, + {"keys": ["pagedown"], "command": "page_down"}, + {"keys": ["ctrl+z", "f5"], "command": "undo"}, + {"keys": ["f6"], "command": "redo"}, + {"keys": ["f9"], "command": "toggle_line_nums"}, + {"keys": ["f10"], "command": "toggle_line_ends"}, + {"keys": ["f11"], "command": "toggle_highlight"}, + {"keys": ["alt+up"], "command": "new_cursor_up"}, + {"keys": ["alt+down"], "command": "new_cursor_down"}, + {"keys": ["alt+left"], "command": "new_cursor_left"}, + {"keys": ["alt+right"], "command": "new_cursor_right"}, + {"keys": ["alt+pageup"], "command": "push_up"}, + {"keys": ["alt+pagedown"], "command": "push_down"}, + {"keys": ["ctrl+c"], "command": "copy"}, + {"keys": ["ctrl+x"], "command": "cut"}, + {"keys": ["ctrl+k"], "command": "duplicate_line"}, + {"keys": ["ctrl+v"], "command": "insert"}, + {"keys": ["ctrl+d"], "command": "find_next"}, + {"keys": ["ctrl+a"], "command": "find_all"}, + {"keys": ["ctrl+left"], "command": "jump_left"}, + {"keys": ["ctrl+right"], "command": "jump_right"}, + {"keys": ["ctrl+up"], "command": "jump_up"}, + {"keys": ["ctrl+down"], "command": "jump_down"} ] \ No newline at end of file diff --git a/suplemon/key_mappings.py b/suplemon/key_mappings.py index 75b2713..c0ee2a3 100644 --- a/suplemon/key_mappings.py +++ b/suplemon/key_mappings.py @@ -67,7 +67,7 @@ "^W": "ctrl+w", "^X": "ctrl+x", "^Y": "ctrl+y", - # "^Z": "ctrl+z", # Conflicts with suspend + "^Z": "ctrl+z", # Conflicts with suspend 544: "ctrl+left", 559: "ctrl+right", diff --git a/suplemon/main.py b/suplemon/main.py index 90b436d..c71f202 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -213,7 +213,8 @@ def get_key_bindings(self): """Return the list of key bindings.""" bindings = {} for binding in self.config.keymap: - bindings[binding["keys"]] = binding["command"] + for key in binding["keys"]: + bindings[key] = binding["command"] return bindings def get_event_bindings(self): @@ -228,7 +229,7 @@ def set_key_binding(self, key, operation): :param key: What key or key combination to bind. :param str operation: Which operation to run. """ - self.config.keymap.prepend({"keys": key, "command": operation}) + self.config.keymap.prepend({"keys": [key], "command": operation}) def set_event_binding(self, event, when, callback): """Bind a callback to be run before or after an event. From eaf380af5ec24c38ece8d359bde39749d254c8aa Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 29 Feb 2016 00:58:43 +0200 Subject: [PATCH 08/36] Feature improvements - Implemented tab indicators - Improved running modules from key bindings - Added promt for creating missing dirs during save_as --- README.md | 2 +- suplemon/config/defaults.json | 4 ++++ suplemon/main.py | 25 ++++++++++++++++++++----- suplemon/viewer.py | 19 +++++++++++++++++++ 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2c60c95..f1b13fd 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ http://github.com/richrd/suplemon ![Suplemon in action](http://bittemple.org/misc/suplemon/suplemon-demo.gif) -## Get it! +## Try it! You can just clone the repo, and try Suplemon, or also install it system wide. diff --git a/suplemon/config/defaults.json b/suplemon/config/defaults.json index 92af126..c347aeb 100644 --- a/suplemon/config/defaults.json +++ b/suplemon/config/defaults.json @@ -58,6 +58,10 @@ }, // Wether to visually show white space chars "show_white_space": false, + // Show tab indicators in whitespace + "show_tab_indicators": true, + // Tab indicator charatrer + "tab_indicator_character": "\u203A", // Line numbering "show_line_nums": true, // Naive line highlighting diff --git a/suplemon/main.py b/suplemon/main.py index c71f202..581f510 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -308,6 +308,8 @@ def handle_key(self, event): if operation in self.operations.keys(): self.run_operation(operation) return True + elif operation in self.modules.modules.keys(): + self.run_module(operation) return False @@ -443,22 +445,28 @@ def run_command(self, data): cmd = parts[0].lower() if cmd in self.operations.keys(): return self.run_operation(cmd) + args = " ".join(parts[1:]) self.logger.debug("Looking for command '{0}'".format(cmd)) if cmd in self.modules.modules.keys(): self.logger.debug("Trying to run command '{0}'".format(cmd)) self.get_editor().store_action_state(cmd) - try: - self.modules.modules[cmd].run(self, self.get_editor(), args) - except: - self.set_status("Running command failed!") - self.logger.exception("Running command failed!") + if not self.run_module(cmd, args): return False else: self.set_status("Command '{0}' not found.".format(cmd)) return False return True + def run_module(self, module_name, args=""): + try: + self.modules.modules[module_name].run(self, self.get_editor(), args) + return True + except: + self.set_status("Running command failed!") + self.logger.exception("Running command failed!") + return False + def run_operation(self, operation): """Run an app core operation.""" # Support arbitrary callables. TODO: deprecate @@ -606,6 +614,13 @@ def save_file_as(self, file=False): name = self.ui.query("Save as:", f.name) if not name: return False + dir = os.path.dirname(name) + if not os.path.exists(dir): + if self.ui.query_bool("The path doesn't exist, do you want to create it?"): + self.logger.debug("Creating missing folders in save path.") + os.makedirs(dir) + else: + return False f.set_name(name) return self.save_file(f) diff --git a/suplemon/viewer.py b/suplemon/viewer.py index 9e863d8..5db28d8 100644 --- a/suplemon/viewer.py +++ b/suplemon/viewer.py @@ -301,6 +301,7 @@ def render_line_pygments(self, line, pos, x_offset, max_len): # support multi line comment syntax etc. It should also perform # better, since we only need to re-highlight lines when they change. tokens = self.lexer.lex(line_data, self.pygments_syntax) + first_token = True for token in tokens: if token[1] == "\n": break @@ -311,6 +312,9 @@ def render_line_pygments(self, line, pos, x_offset, max_len): # TODO: get whitespace color from theme pair = 9 # Gray text on normal background curs_color = curses.color_pair(pair) + # Only add tab indicators to the inital whitespace + if first_token and self.config["show_tab_indicators"]: + text = self.add_tab_indicators(text) self.window.addstr(y, x_offset, text, curs_color) else: # Color with pygments @@ -324,6 +328,8 @@ def render_line_pygments(self, line, pos, x_offset, max_len): self.window.addstr(y, x_offset, text, curs_color) else: self.window.addstr(y, x_offset, text) + if first_token: + first_token = False x_offset += len(text) def render_line_linelight(self, line, pos, x_offset, max_len): @@ -341,6 +347,19 @@ def render_line_normal(self, line, pos, x_offset, max_len): line_data = self._prepare_line_for_rendering(line_data, max_len) self.window.addstr(y, x_offset, line_data) + def add_tab_indicators(self, data): + new_data = "" + i = 0 + for char in data: + if i == 0: + new_data += self.config["tab_indicator_character"] + else: + new_data += char + i += 1 + if i > self.config["tab_width"]-1: + i = 0 + return new_data + def replace_whitespace(self, data): """Replace unsafe whitespace with alternative safe characters From 3927eaccbbbc85bd5ada7b28cd09ee565d86d7cb Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 29 Feb 2016 11:20:40 +0200 Subject: [PATCH 09/36] Fixed logging on Python 2.6 and disabled unicode symbols on Python 2.X --- suplemon/logger.py | 10 ++++++++-- suplemon/main.py | 11 ++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/suplemon/logger.py b/suplemon/logger.py index 7fe70c6..d80d9ec 100644 --- a/suplemon/logger.py +++ b/suplemon/logger.py @@ -20,7 +20,10 @@ def __init__(self, capacity, fd_target): :param object fd_target: File descriptor to write output to (e.g. `sys.stdout`) """ # Call our BufferingHandler init - super(BufferingTargetHandler, self).__init__(capacity) + if issubclass(BufferingTargetHandler, object): + super(BufferingTargetHandler, self).__init__(capacity) + else: + BufferingHandler.__init__(self, capacity) # Save target for later self._fd_target = fd_target @@ -41,7 +44,10 @@ def close(self): self.release() # Then, run our normal close actions - super(BufferingTargetHandler, self).close() + if issubclass(BufferingTargetHandler, object): + super(BufferingTargetHandler, self).close() + else: + BufferingHandler.close(self) # Initialize logging diff --git a/suplemon/main.py b/suplemon/main.py index 581f510..f288515 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -44,6 +44,9 @@ def __init__(self, filenames=None, config_file=None): self.global_buffer = [] self.event_bindings = {} + # Maximum amount of inputs to process at once + self.max_input = 100 + # Save filenames for later self.filenames = filenames @@ -84,6 +87,12 @@ def init(self): # Can't run without config return False self.config.load() + + # Unicode symbols don't play nice with Python 2 so disable them + if sys.version_info[0] < 3: + self.config["app"]["use_unicode_symbols"] = False + + # Configure logger self.debug = self.config["app"]["debug"] debug_level = self.config["app"]["debug_level"] self.logger.debug("Setting debug_level to {0}.".format(debug_level)) @@ -166,7 +175,7 @@ def main_loop(self): # Run through max 100 inputs (so the view is updated at least every 100 characters) i = 0 - while i < 100: + while i < self.max_input: event = self.ui.get_input(False) # non-blocking if not event: From b6e51094bb7c089a26ef8efe0c3e93596cd1e193 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 1 Mar 2016 21:57:28 +0200 Subject: [PATCH 10/36] Mapped Ctrl+Y to undo. --- suplemon/config/keymap.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suplemon/config/keymap.json b/suplemon/config/keymap.json index 5c1c011..90190b3 100644 --- a/suplemon/config/keymap.json +++ b/suplemon/config/keymap.json @@ -40,7 +40,7 @@ {"keys": ["pageup"], "command": "page_up"}, {"keys": ["pagedown"], "command": "page_down"}, {"keys": ["ctrl+z", "f5"], "command": "undo"}, - {"keys": ["f6"], "command": "redo"}, + {"keys": ["ctrl+y", "f6"], "command": "redo"}, {"keys": ["f9"], "command": "toggle_line_nums"}, {"keys": ["f10"], "command": "toggle_line_ends"}, {"keys": ["f11"], "command": "toggle_highlight"}, From 00d7ac73224c03d8b474dda4fd0e54c801f542c2 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 1 Mar 2016 22:48:09 +0200 Subject: [PATCH 11/36] Restructured config command, added keymap command, and described usage in readme. --- README.md | 5 ++++- suplemon/modules/config.py | 19 +++++++++++-------- suplemon/modules/keymap.py | 39 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 suplemon/modules/keymap.py diff --git a/README.md b/README.md index f1b13fd..eff0a22 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ Suplemon is licensed under the MIT license. ## Configuration +### Main Config The suplemon config file is stored at ```~/.config/suplemon/suplemon-config.json```. The best way to edit it is to run the ```config``` command (Run commands via ```Ctrl+E```). @@ -104,8 +105,10 @@ That way Suplemon will automatically reload the configuration when you save the To view the default configuration and see what options are available run ```config defaults``` via ```Ctrl+E```. +### Keymap Config -## Keyboard shortcuts +Below are the default key mappings used in suplemon. They can be edited by running the ```keymap``` command. +To view the default keymap file run ```keymap default``` * Ctrl + Q > Exit diff --git a/suplemon/modules/config.py b/suplemon/modules/config.py index e021a00..264ee91 100644 --- a/suplemon/modules/config.py +++ b/suplemon/modules/config.py @@ -11,14 +11,8 @@ class Config(Module): def run(self, app, editor, args): if args == "defaults": # Open the default config in a new file only for viewing - path = os.path.join(app.path, "config/defaults.json") - f = open(path) - data = f.read() - f.close() - file = app.new_file() - file.set_name("defaults.json") - file.set_data(data) - app.switch_to_file(app.last_file_index()) + path = os.path.join(app.path, "config", "defaults.json") + self.open_read_only(app, path) else: # Open the user config file for editing path = app.config.path() @@ -30,6 +24,15 @@ def run(self, app, editor, args): app.new_file(path) app.switch_to_file(app.last_file_index()) + def open_read_only(self, app, path): + f = open(path) + data = f.read() + f.close() + file = app.new_file() + file.set_name("defaults.json") + file.set_data(data) + app.switch_to_file(app.last_file_index()) + module = { "class": Config, "name": "config", diff --git a/suplemon/modules/keymap.py b/suplemon/modules/keymap.py new file mode 100644 index 0000000..18732ae --- /dev/null +++ b/suplemon/modules/keymap.py @@ -0,0 +1,39 @@ +# -*- encoding: utf-8 + +import os + +from suplemon.suplemon_module import Module + + +class Keymap(Module): + """Shortcut to openning the current config file.""" + + def run(self, app, editor, args): + if args == "defaults": + # Open the default config in a new file only for viewing + path = os.path.join(app.path, "config", "keymap.json") + self.open_read_only(app, path) + else: + # Open the user config file for editing + path = app.config.keymap_path() + f = app.file_is_open(path) + if f: + app.switch_to_file(app.get_file_index(f)) + else: + if not app.open_file(path): + app.new_file(path) + app.switch_to_file(app.last_file_index()) + + def open_read_only(self, app, path): + f = open(path) + data = f.read() + f.close() + file = app.new_file() + file.set_name("keymap.json") + file.set_data(data) + app.switch_to_file(app.last_file_index()) + +module = { + "class": Keymap, + "name": "keymap", +} From 290c3195fe5c7c20da00e2f0bfbe50c35bacd471 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 1 Mar 2016 23:25:44 +0200 Subject: [PATCH 12/36] Refactored Viewer and Editor for better feature separation. --- suplemon/editor.py | 210 +++++-------------------------------- suplemon/main.py | 8 -- suplemon/ui.py | 5 +- suplemon/viewer.py | 255 ++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 254 insertions(+), 224 deletions(-) diff --git a/suplemon/editor.py b/suplemon/editor.py index 490d35c..5900f5c 100644 --- a/suplemon/editor.py +++ b/suplemon/editor.py @@ -51,8 +51,6 @@ def __init__(self, app, window): """ Viewer.__init__(self, app, window) - # Copy/paste buffer - self.buffer = [] # Last search used in 'find' self.last_find = "" # History of editor states for undo/redo @@ -62,9 +60,9 @@ def __init__(self, app, window): # Last editor action that was used (for undo/redo) self.last_action = None - self.operations = { - "home": self.home, # Home - "end": self.end, # End + def init(self): + Viewer.init(self) + operations = { "backspace": self.backspace, # Backspace "delete": self.delete, # Delete "insert": self.insert, # Insert @@ -72,10 +70,6 @@ def __init__(self, app, window): "tab": self.tab, # Tab "untab": self.untab, # Shift + Tab "escape": self.escape, # Escape - "arrow_right": self.arrow_right, # Arrow Right - "arrow_left": self.arrow_left, # Arrow Left - "arrow_up": self.arrow_up, # Arrow Up - "arrow_down": self.arrow_down, # Arrow Down "new_cursor_up": self.new_cursor_up, # Alt + Up "new_cursor_down": self.new_cursor_down, # Alt + Down "new_cursor_left": self.new_cursor_left, # Alt + Left @@ -94,25 +88,9 @@ def __init__(self, app, window): "duplicate_line": self.duplicate_line, # Ctrl + W "find_next": self.find_next, # Ctrl + D "find_all": self.find_all, # Ctrl + A - "jump_left": self.jump_left, # Ctrl + Left - "jump_right": self.jump_right, # Ctrl + Right - "jump_up": self.jump_up, # Ctrl + Up - "jump_down": self.jump_down, # Ctrl + Down } - - def get_buffer(self): - """Returns the current buffer. - - Returns the local buffer or the global buffer depending on config. - """ - if self.app.config["editor"]["use_global_buffer"]: - return self.app.global_buffer - else: - return self.buffer - - def get_key_bindings(self): - """Get list of editor key bindings.""" - return self.app.get_key_bindings() + for key in operations.keys(): + self.operations[key] = operations[key] def set_buffer(self, buffer): """Sets local or global buffer depending on config.""" @@ -176,6 +154,19 @@ def restore_state(self, index=None): self.current_state = index self.refresh() + def handle_input(self, event): + done = Viewer.handle_input(self, event) + if not done: + key = event.key_code + name = event.key_name + if isinstance(key, str): + self.type(key) + return True + elif name and not name.startswith("KEY_"): + self.type(name) + return True + return False + def undo(self): """Undo the last command or change.""" self.last_action = "undo" @@ -193,102 +184,6 @@ def redo(self): # Cursor operations # - def arrow_right(self): - """Move cursors right.""" - for cursor in self.cursors: - line = self.lines[cursor.y] - if cursor.y != len(self.lines)-1 and (cursor.x >= len(line) or len(line) == 0): - cursor.move_down() - cursor.set_x(0) - elif cursor.x < len(self.lines[cursor.y]) and len(line) > 0: - cursor.move_right() - self.move_cursors() - self.scroll_down() - - def arrow_left(self): - """Move cursors left.""" - for cursor in self.cursors: - if cursor.y != 0 and cursor.x == 0: - cursor.move_up() - cursor.set_x(len(self.lines[cursor.y])+1) - self.move_cursors((-1, 0)) - self.scroll_up() - - def arrow_up(self): - """Move cursors up.""" - self.move_cursors((0, -1)) - self.scroll_up() - - def arrow_down(self): - """Move cursors down.""" - self.move_cursors((0, 1)) - self.scroll_down() - - def jump_left(self): - """Jump one 'word' to the left.""" - chars = self.config["punctuation"] - for cursor in self.cursors: - line = self.lines[cursor.y] - if cursor.x == 0: - if cursor.y > 0: - # Jump to end of previous line - cursor.set_x(len(self.lines[cursor.y-1])) - cursor.move_up() - continue - if cursor.x <= len(line): - cur_chr = line[cursor.x-1] - else: - cur_chr = line[cursor.x] - while cursor.x > 0: - next = cursor.x-2 - if next < 0: - next = 0 - if cur_chr == " ": - cursor.move_left() - if line[next] != " ": - break - else: - cursor.move_left() - if line[next] in chars: - break - self.move_cursors() - - def jump_right(self): - """Jump one 'word' to the right.""" - chars = self.config["punctuation"] - for cursor in self.cursors: - line = self.lines[cursor.y] - if cursor.x == len(line): - if cursor.y < len(self.lines): - # Jump to start of next line - cursor.set_x(0) - cursor.move_down() - continue - cur_chr = line[cursor.x] - while cursor.x < len(line): - next = cursor.x+1 - if next == len(line): - next -= 1 - if cur_chr == " ": - cursor.move_right() - if line[next] != " ": - break - else: - cursor.move_right() - if line[next] in chars: - break - self.move_cursors() - - def jump_up(self): - """Jump up 3 lines.""" - self.move_cursors((0, -3)) - self.scroll_up() - - def jump_down(self): - """Jump down 3 lines.""" - self.move_cursors((0, 3)) - self.scroll_down() - def new_cursor_up(self): """Add a new cursor one line up.""" x = self.get_cursor().x @@ -344,34 +239,6 @@ def escape(self): self.move_cursors() self.render() - def page_up(self): - """Move half a page up.""" - amount = int(self.get_size()[1]/2) * -1 - self.move_cursors((0, amount)) - self.scroll_up() - - def page_down(self): - """Move half a page down.""" - amount = int(self.get_size()[1]/2) - self.move_cursors((0, amount)) - self.scroll_down() - - def home(self): - """Move to start of line or text on that line.""" - for cursor in self.cursors: - wspace = helpers.whitespace(self.lines[cursor.y]) - if cursor.x == wspace: - cursor.set_x(0) - else: - cursor.set_x(wspace) - self.move_cursors() - - def end(self): - """Move to end of line.""" - for cursor in self.cursors: - cursor.set_x(len(self.lines[cursor.y])) - self.move_cursors() - # # Text editing operations # @@ -682,6 +549,12 @@ def go_to_pos(self, line_no, col=0): self.scroll_to_line(cur.y) self.move_cursors() + def find_query(self): + """Find in file via user input.""" + what = self.app.ui.query("Find:", self.last_find) + if what: + self.find(what) + def find(self, what, findall=False): """Find what in data (from top to bottom). Adds a cursor when found.""" # Sorry for this colossal function @@ -778,41 +651,6 @@ def duplicate_line(self): self.move_cursors() self.store_action_state("duplicate_line") - def handle_input(self, event): - """Handle input.""" - if event.type == "mouse": - return False - key = event.key_code - name = event.key_name - # Try match a key to a method and call it - - key_bindings = self.get_key_bindings() - operation = None - if key in key_bindings.keys(): - operation = key_bindings[key] - elif name in key_bindings.keys(): - operation = key_bindings[name] - if operation: - self.run_operation(operation) - # Try to type the key into the editor - else: - if isinstance(key, str): - self.type(key) - elif name and not name.startswith("KEY_"): - self.type(name) - return False - - def run_operation(self, operation): - """Run an editor core operation.""" - if operation in self.operations.keys(): - cancel = self.app.trigger_event_before(operation) - if cancel: - return False - result = self.operations[operation]() - self.app.trigger_event_after(operation) - return result - return False - class PromptEditor(Editor): """An input prompt based on the Editor.""" diff --git a/suplemon/main.py b/suplemon/main.py index f288515..67fa3da 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -58,7 +58,6 @@ def __init__(self, filenames=None, config_file=None): "help": self.help, "save_file": self.save_file, "run_command": self.query_command, - "find": self.find, "go_to": self.go_to, "open": self.open, "close_file": self.close_file, @@ -432,13 +431,6 @@ def go_to(self): if file_index != -1: self.switch_to_file(file_index) - def find(self): - """Find in file.""" - editor = self.get_editor() - what = self.ui.query("Find:", editor.last_find) - if what: - editor.find(what) - def find_file(self, s): """Return index of file matching string.""" i = 0 diff --git a/suplemon/ui.py b/suplemon/ui.py index 8971605..529a5f3 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -345,8 +345,9 @@ def show_bottom_status(self): str(len(editor.get_buffer())) ) - if self.app.config["app"]["debug"]: - data += " cs:"+str(editor.current_state)+" hist:"+str(len(editor.history)) # Undo / Redo debug + # Deprecate? + # if self.app.config["app"]["debug"]: + # data += " cs:"+str(editor.current_state)+" hist:"+str(len(editor.history)) # Undo / Redo debug # Add module statuses to the status bar for name in self.app.modules.modules.keys(): diff --git a/suplemon/viewer.py b/suplemon/viewer.py index 5db28d8..b5eec1b 100644 --- a/suplemon/viewer.py +++ b/suplemon/viewer.py @@ -9,6 +9,7 @@ import curses import logging +from . import helpers from .line import Line from .cursor import Cursor @@ -50,18 +51,38 @@ def __init__(self, app, window): self.x_scroll = 0 self.cursors = [Cursor()] + # Copy/paste buffer + self.buffer = [] + + # Runnable methods + self.operations = { + "arrow_right": self.arrow_right, # Arrow Right + "arrow_left": self.arrow_left, # Arrow Left + "arrow_up": self.arrow_up, # Arrow Up + "arrow_down": self.arrow_down, # Arrow Down + "jump_left": self.jump_left, # Ctrl + Left + "jump_right": self.jump_right, # Ctrl + Right + "jump_up": self.jump_up, # Ctrl + Up + "jump_down": self.jump_down, # Ctrl + Down + "page_up": self.page_up, # Page Up + "page_down": self.page_down, # Page Down + "home": self.home, # Home + "end": self.end, # End + "find": self.find_query # Ctrl + F + } + def init(self): pass - def size(self): - """Get editor size (x,y). (Deprecated, use get_size).""" - self.logger.warning("size() is deprecated, please use get_size()") - return self.get_size() + def get_buffer(self): + """Returns the current buffer. - def cursor(self): - """Return the main cursor. (Deprecated, use get_cursor)""" - self.logger.warning("cursor() is deprecated, please use get_cursor()") - return self.get_cursor() + Returns the local buffer or the global buffer depending on config. + """ + if self.config["use_global_buffer"]: + return self.app.global_buffer + else: + return self.buffer def get_size(self): """Get editor size (x,y).""" @@ -233,6 +254,31 @@ def toggle_highlight(self): """Toggle syntax highlighting.""" return False + ########################################################################### + # Curses + ########################################################################### + + def move_win(self, yx): + """Move the editor window to position yx.""" + # Must try & catch since mvwin might + # crash with incorrect coordinates + try: + self.window.mvwin(yx[0], yx[1]) + except: + self.logger.warning("Moving window failed!", exc_info=True) + + def refresh(self): + """Refresh the editor curses window.""" + self.window.refresh() + + def resize(self, yx=None): + """Resize the UI.""" + if not yx: + yx = self.window.getmaxyx() + self.window.resize(yx[0], yx[1]) + self.move_cursors() + self.refresh() + def render(self): """Render the editor curses window.""" if self.app.block_rendering: @@ -424,17 +470,9 @@ def render_cursors(self): continue self.window.chgat(y, cursor.x+self.line_offset()-self.x_scroll, 1, self.cursor_style) - def refresh(self): - """Refresh the editor curses window.""" - self.window.refresh() - - def resize(self, yx=None): - """Resize the UI.""" - if not yx: - yx = self.window.getmaxyx() - self.window.resize(yx[0], yx[1]) - self.move_cursors() - self.refresh() + ########################################################################### + # Scrolling + ########################################################################### def scroll_up(self): """Scroll view up if neccesary.""" @@ -460,19 +498,14 @@ def scroll_to_line(self, line_no): new_y = 0 self.y_scroll = new_y - def move_win(self, yx): - """Move the editor window to position yx.""" - # Must try & catch since mvwin might - # crash with incorrect coordinates - try: - self.window.mvwin(yx[0], yx[1]) - except: - self.logger.warning("Moving window failed!", exc_info=True) - def move_y_scroll(self, delta): """Add delta the y scroll axis scroll""" self.y_scroll += delta + ########################################################################### + # Cursors + ########################################################################### + def move_cursors(self, delta=None, noupdate=False): """Move all cursors with delta. To avoid refreshing the screen set noupdate to True.""" if self.app.block_rendering: @@ -569,6 +602,172 @@ def purge_line_cursors(self, line_no): self.remove_cursor(cursor) return True + ########################################################################### + # Input Handling + ########################################################################### + + def get_key_bindings(self): + """Get list of editor key bindings.""" + return self.app.get_key_bindings() + + def handle_input(self, event): + """Handle input.""" + if event.type == "mouse": + return False + key = event.key_code + name = event.key_name + # Try match a key to a method and call it + + key_bindings = self.get_key_bindings() + operation = None + if key in key_bindings.keys(): + operation = key_bindings[key] + elif name in key_bindings.keys(): + operation = key_bindings[name] + if operation: + self.run_operation(operation) + return True + return False + + def run_operation(self, operation): + """Run an editor core operation.""" + if operation in self.operations.keys(): + cancel = self.app.trigger_event_before(operation) + if cancel: + return False + result = self.operations[operation]() + self.app.trigger_event_after(operation) + return result + return False + + ########################################################################### + # Operations + ########################################################################### + + def arrow_right(self): + """Move cursors right.""" + for cursor in self.cursors: + line = self.lines[cursor.y] + if cursor.y != len(self.lines)-1 and (cursor.x >= len(line) or len(line) == 0): + cursor.move_down() + cursor.set_x(0) + elif cursor.x < len(self.lines[cursor.y]) and len(line) > 0: + cursor.move_right() + self.move_cursors() + self.scroll_down() + + def arrow_left(self): + """Move cursors left.""" + for cursor in self.cursors: + if cursor.y != 0 and cursor.x == 0: + cursor.move_up() + cursor.set_x(len(self.lines[cursor.y])+1) + self.move_cursors((-1, 0)) + self.scroll_up() + + def arrow_up(self): + """Move cursors up.""" + self.move_cursors((0, -1)) + self.scroll_up() + + def arrow_down(self): + """Move cursors down.""" + self.move_cursors((0, 1)) + self.scroll_down() + + def home(self): + """Move to start of line or text on that line.""" + for cursor in self.cursors: + wspace = helpers.whitespace(self.lines[cursor.y]) + if cursor.x == wspace: + cursor.set_x(0) + else: + cursor.set_x(wspace) + self.move_cursors() + + def end(self): + """Move to end of line.""" + for cursor in self.cursors: + cursor.set_x(len(self.lines[cursor.y])) + self.move_cursors() + + def page_up(self): + """Move half a page up.""" + amount = int(self.get_size()[1]/2) * -1 + self.move_cursors((0, amount)) + self.scroll_up() + + def page_down(self): + """Move half a page down.""" + amount = int(self.get_size()[1]/2) + self.move_cursors((0, amount)) + self.scroll_down() + + def jump_left(self): + """Jump one 'word' to the left.""" + chars = self.config["punctuation"] + for cursor in self.cursors: + line = self.lines[cursor.y] + if cursor.x == 0: + if cursor.y > 0: + # Jump to end of previous line + cursor.set_x(len(self.lines[cursor.y-1])) + cursor.move_up() + continue + if cursor.x <= len(line): + cur_chr = line[cursor.x-1] + else: + cur_chr = line[cursor.x] + while cursor.x > 0: + next = cursor.x-2 + if next < 0: + next = 0 + if cur_chr == " ": + cursor.move_left() + if line[next] != " ": + break + else: + cursor.move_left() + if line[next] in chars: + break + self.move_cursors() + + def jump_right(self): + """Jump one 'word' to the right.""" + chars = self.config["punctuation"] + for cursor in self.cursors: + line = self.lines[cursor.y] + if cursor.x == len(line): + if cursor.y < len(self.lines): + # Jump to start of next line + cursor.set_x(0) + cursor.move_down() + continue + cur_chr = line[cursor.x] + while cursor.x < len(line): + next = cursor.x+1 + if next == len(line): + next -= 1 + if cur_chr == " ": + cursor.move_right() + if line[next] != " ": + break + else: + cursor.move_right() + if line[next] in chars: + break + self.move_cursors() + + def jump_up(self): + """Jump up 3 lines.""" + self.move_cursors((0, -3)) + self.scroll_up() + + def jump_down(self): + """Jump down 3 lines.""" + self.move_cursors((0, 3)) + self.scroll_down() + class Viewer(BaseViewer): def __init__(self, app, window): From 1b95da0647ee489b166bdd52446f02c907086791 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 2 Mar 2016 15:48:15 +0200 Subject: [PATCH 13/36] Fixed proper initialization of PromptEditor. --- suplemon/ui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/suplemon/ui.py b/suplemon/ui.py index 529a5f3..e5e79d3 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -419,6 +419,7 @@ def _query(self, text, initial=""): self.text_input = PromptEditor(self.app, self.status_win) self.text_input.set_config(self.app.config["editor"].copy()) self.text_input.set_input_source(self.get_input) + self.text_input.init() # Get input from the user out = self.text_input.get_input(text, initial) From 3858b748c47336abdce375242cd9f18d9a96fcc0 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 2 Mar 2016 16:17:55 +0200 Subject: [PATCH 14/36] Fixed saving files under new names. --- suplemon/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suplemon/main.py b/suplemon/main.py index 67fa3da..d43aa81 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -616,7 +616,7 @@ def save_file_as(self, file=False): if not name: return False dir = os.path.dirname(name) - if not os.path.exists(dir): + if dir and not os.path.exists(dir): if self.ui.query_bool("The path doesn't exist, do you want to create it?"): self.logger.debug("Creating missing folders in save path.") os.makedirs(dir) From a8ee0834e11c2a23e0bd3dde5cb14e6bc9e22648 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Wed, 2 Mar 2016 19:39:39 +0200 Subject: [PATCH 15/36] Added more scopes. --- suplemon/lexer.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/suplemon/lexer.py b/suplemon/lexer.py index 9bb3637..25b068c 100644 --- a/suplemon/lexer.py +++ b/suplemon/lexer.py @@ -35,20 +35,28 @@ def lex(self, code, lex): scope = "keyword" elif token == pygments.token.Comment: scope = "comment" - elif token in pygments.token.Literal.String: - scope = "string" - elif token in pygments.token.Literal.Number: - scope = "constant.numeric" + elif token == pygments.token.Comment.Single: + scope = "comment" elif token == pygments.token.Name.Function: scope = "entity.name.function" elif token == pygments.token.Name.Class: scope = "entity.name.class" elif token == pygments.token.Name.Tag: scope = "entity.name.tag" + elif token == pygments.token.Name.Attribute: + scope = "entity.other.attribute-name" + elif token == pygments.token.Name.Variable: + scope = "variable" elif token == pygments.token.Operator: scope = "keyword" elif token == pygments.token.Name.Builtin.Pseudo: scope = "constant.language" + elif token in pygments.token.Literal.String: + scope = "string" + elif token in pygments.token.Literal.Number: + scope = "constant.numeric" + elif token in pygments.token.Name: + scope = "entity.name" scopes.append((scope, word[1])) return scopes From 6bd3d19d1417a57a086dd6880b62f4973b51eaf8 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Thu, 3 Mar 2016 01:52:47 +0200 Subject: [PATCH 16/36] Fixed merging user keymap with defaults. --- suplemon/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suplemon/config.py b/suplemon/config.py index 27c5455..543c70e 100644 --- a/suplemon/config.py +++ b/suplemon/config.py @@ -67,7 +67,7 @@ def load_keys(self): if not keymap: self.logger.info("Failed to load keymap file '{0}'.".format(path)) return False - self.keymap[-1:] = keymap # Append the user key map + self.keymap += keymap # Append the user key map return True def load_defaults(self): From 12b336e99ad62c2eecbb15bafdf8d648041270a7 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Thu, 3 Mar 2016 01:54:44 +0200 Subject: [PATCH 17/36] Slightly optimized lexer. --- suplemon/lexer.py | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/suplemon/lexer.py b/suplemon/lexer.py index 25b068c..5922b48 100644 --- a/suplemon/lexer.py +++ b/suplemon/lexer.py @@ -1,12 +1,24 @@ # -*- encoding: utf-8 import pygments +import pygments.token import pygments.lexers class Lexer: def __init__(self, app): self.app = app + self.token_map = { + pygments.token.Comment: "comment", + pygments.token.Comment.Single: "comment", + pygments.token.Name.Function: "entity.name.function", + pygments.token.Name.Class: "entity.name.class", + pygments.token.Name.Tag: "entity.name.tag", + pygments.token.Name.Attribute: "entity.other.attribute-name", + pygments.token.Name.Variable: "variable", + pygments.token.Operator: "keyword", + pygments.token.Name.Builtin.Pseudo: "constant.language", + } def lex(self, code, lex): """Return tokenified code. @@ -31,32 +43,14 @@ def lex(self, code, lex): token = word[0] scope = "global" - if token in pygments.token.Keyword: - scope = "keyword" - elif token == pygments.token.Comment: - scope = "comment" - elif token == pygments.token.Comment.Single: - scope = "comment" - elif token == pygments.token.Name.Function: - scope = "entity.name.function" - elif token == pygments.token.Name.Class: - scope = "entity.name.class" - elif token == pygments.token.Name.Tag: - scope = "entity.name.tag" - elif token == pygments.token.Name.Attribute: - scope = "entity.other.attribute-name" - elif token == pygments.token.Name.Variable: - scope = "variable" - elif token == pygments.token.Operator: - scope = "keyword" - elif token == pygments.token.Name.Builtin.Pseudo: - scope = "constant.language" - elif token in pygments.token.Literal.String: - scope = "string" + if token in self.token_map.keys(): + scope = self.token_map[token] elif token in pygments.token.Literal.Number: scope = "constant.numeric" elif token in pygments.token.Name: scope = "entity.name" + elif token in pygments.token.Keyword: + scope = "keyword" scopes.append((scope, word[1])) return scopes From 9c9841b8b5ff8ca78dde217c39d538de179e4448 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Thu, 3 Mar 2016 02:04:15 +0200 Subject: [PATCH 18/36] Added performance notes. --- suplemon/main.py | 4 ++-- suplemon/viewer.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/suplemon/main.py b/suplemon/main.py index d43aa81..ed7e36d 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -190,14 +190,14 @@ def main_loop(self): if event: got_input = True - self.on_input(event) + self.on_input(event) # Up to 30% processing time self.block_rendering = False # TODO: why do I need resize here? # (View won't update after switching files, WTF) self.trigger_event_after("mainloop") - self.get_editor().resize() + self.get_editor().resize() # TODO: Optimize performance. Can make up 45% of processing time in the loop. self.ui.refresh() def get_status(self): diff --git a/suplemon/viewer.py b/suplemon/viewer.py index b5eec1b..4410ed2 100644 --- a/suplemon/viewer.py +++ b/suplemon/viewer.py @@ -346,6 +346,7 @@ def render_line_pygments(self, line, pos, x_offset, max_len): # and tokens should be cached in line instances. That way we can # support multi line comment syntax etc. It should also perform # better, since we only need to re-highlight lines when they change. + # TODO 2: Optimize lexer performance tokens = self.lexer.lex(line_data, self.pygments_syntax) first_token = True for token in tokens: @@ -407,6 +408,7 @@ def add_tab_indicators(self, data): return new_data def replace_whitespace(self, data): + # TODO: Optimize performance """Replace unsafe whitespace with alternative safe characters Replace unsafe whitespace with normal space or visible replacement. From f14138f0b4a4f70badb118e55d1a41ee5ebb5da4 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Thu, 3 Mar 2016 02:39:53 +0200 Subject: [PATCH 19/36] Fixed removing string token. --- suplemon/lexer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/suplemon/lexer.py b/suplemon/lexer.py index 5922b48..5098328 100644 --- a/suplemon/lexer.py +++ b/suplemon/lexer.py @@ -18,6 +18,7 @@ def __init__(self, app): pygments.token.Name.Variable: "variable", pygments.token.Operator: "keyword", pygments.token.Name.Builtin.Pseudo: "constant.language", + pygments.token.Literal.String: "string", } def lex(self, code, lex): From aca5a443e7d4d221a46ed9071aa4f46f03e8b686 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Thu, 3 Mar 2016 02:42:26 +0200 Subject: [PATCH 20/36] Dark status bars by default. --- suplemon/config/defaults.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suplemon/config/defaults.json b/suplemon/config/defaults.json index c347aeb..260501a 100644 --- a/suplemon/config/defaults.json +++ b/suplemon/config/defaults.json @@ -93,6 +93,6 @@ // Show the bottom status bar "show_bottom_bar": true, // Invert status bar colors (switch text and background colors) - "invert_status_bars": true + "invert_status_bars": false } } \ No newline at end of file From 4e56ac27f193cf5018aa0987eeb0da98d6f9d6a8 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Thu, 3 Mar 2016 02:43:28 +0200 Subject: [PATCH 21/36] Fixed possibility of invoking find in the find promt itself recursively. --- suplemon/editor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/suplemon/editor.py b/suplemon/editor.py index 5900f5c..9bb2749 100644 --- a/suplemon/editor.py +++ b/suplemon/editor.py @@ -661,6 +661,11 @@ def __init__(self, app, window): self.input_func = lambda: False self.caption = "" + def init(self): + Editor.init(self) + # Remove the find feature, otherwise it can be invoked recursively + del self.operations["find"] + def set_config(self, config): """Set the configuration for the editor.""" # Override showing line numbers From a974921b51a4ffc4e7482cc7415860498a09f2f0 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Thu, 3 Mar 2016 02:45:06 +0200 Subject: [PATCH 22/36] Some tweaks to rendering separation. --- suplemon/main.py | 6 +++--- suplemon/ui.py | 2 +- suplemon/viewer.py | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/suplemon/main.py b/suplemon/main.py index ed7e36d..0136607 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -194,10 +194,10 @@ def main_loop(self): self.block_rendering = False - # TODO: why do I need resize here? - # (View won't update after switching files, WTF) self.trigger_event_after("mainloop") - self.get_editor().resize() # TODO: Optimize performance. Can make up 45% of processing time in the loop. + # Rendering happens here + # TODO: Optimize performance. Can make up 45% of processing time in the loop. + self.get_editor().refresh() self.ui.refresh() def get_status(self): diff --git a/suplemon/ui.py b/suplemon/ui.py index e5e79d3..fe0ad3e 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -264,7 +264,7 @@ def resize(self, yx=None): self.screen.refresh() def check_resize(self): - """Check if terminal has resized.""" + """Check if terminal has resized and resize if needed.""" yx = self.screen.getmaxyx() if self.current_yx != yx: self.current_yx = yx diff --git a/suplemon/viewer.py b/suplemon/viewer.py index 4410ed2..325bdb8 100644 --- a/suplemon/viewer.py +++ b/suplemon/viewer.py @@ -269,6 +269,7 @@ def move_win(self, yx): def refresh(self): """Refresh the editor curses window.""" + self.render() self.window.refresh() def resize(self, yx=None): @@ -586,7 +587,7 @@ def purge_cursors(self): ref.append(cursor.tuple()) new.append(cursor) self.cursors = new - self.render() + self.render() # TODO: is this needed? def purge_line_cursors(self, line_no): """Remove all but first cursor on given line.""" From 0ba2b80c081b69e9062509739f4739d981e9ef51 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Thu, 3 Mar 2016 03:32:38 +0200 Subject: [PATCH 23/36] Optimizations. --- suplemon/editor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/suplemon/editor.py b/suplemon/editor.py index 9bb2749..b08f3d1 100644 --- a/suplemon/editor.py +++ b/suplemon/editor.py @@ -152,7 +152,6 @@ def restore_state(self, index=None): state = self.history[index] state.restore(self) self.current_state = index - self.refresh() def handle_input(self, event): done = Viewer.handle_input(self, event) @@ -718,8 +717,6 @@ def get_input(self, caption="", initial=""): self.set_data(initial) self.end() # Move to the end of the initial text - # TODO: Still can't figure out why resize is needed for succesful render() - self.resize() self.render() # Run the input loop until ready From 6a32ee0470b3f79459c58b074de0adc507b73d43 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Thu, 3 Mar 2016 04:12:27 +0200 Subject: [PATCH 24/36] Cleanup. --- suplemon/editor.py | 5 ++--- suplemon/main.py | 4 ++-- suplemon/ui.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/suplemon/editor.py b/suplemon/editor.py index b08f3d1..d9fa95e 100644 --- a/suplemon/editor.py +++ b/suplemon/editor.py @@ -717,15 +717,14 @@ def get_input(self, caption="", initial=""): self.set_data(initial) self.end() # Move to the end of the initial text - self.render() + self.refresh() # Run the input loop until ready while not self.ready: event = self.input_func(True) # blocking if event: self.handle_input(event) - self.resize() - self.render() + self.refresh() if self.canceled: return False return self.get_data() diff --git a/suplemon/main.py b/suplemon/main.py index 0136607..0858355 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -133,7 +133,7 @@ def run_wrapped(self, *args): # Load ui and files etc self.load() # Initial render - self.get_editor().resize() + self.get_editor().refresh() self.ui.refresh() # Start mainloop self.main_loop() @@ -190,7 +190,7 @@ def main_loop(self): if event: got_input = True - self.on_input(event) # Up to 30% processing time + self.on_input(event) # PERF: Up to 30% processing time self.block_rendering = False diff --git a/suplemon/ui.py b/suplemon/ui.py index fe0ad3e..163b373 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -97,6 +97,7 @@ def init(self): # Now import curses, otherwise ESCDELAY won't have any effect import curses import curses.textpad # noqa + self.logger.debug("Loaded curses {}".format(curses.version.decode())) def run(self, func): """Run the application main function via the curses wrapper for safety.""" @@ -261,7 +262,6 @@ def resize(self, yx=None): self.screen.erase() curses.resizeterm(yx[0], yx[1]) self.setup_windows(resize=True) - self.screen.refresh() def check_resize(self): """Check if terminal has resized and resize if needed.""" From 7664ca674841e75b4a83380a8a341c98966c28d9 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Thu, 3 Mar 2016 13:11:22 +0200 Subject: [PATCH 25/36] Quick Python 2.6 formatting fix. --- suplemon/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suplemon/ui.py b/suplemon/ui.py index 163b373..945d587 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -97,7 +97,7 @@ def init(self): # Now import curses, otherwise ESCDELAY won't have any effect import curses import curses.textpad # noqa - self.logger.debug("Loaded curses {}".format(curses.version.decode())) + self.logger.debug("Loaded curses {0}".format(curses.version.decode())) def run(self, func): """Run the application main function via the curses wrapper for safety.""" From 3a62ecd3a18678d28c0acaf65a85905d8355597d Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Fri, 4 Mar 2016 01:56:58 +0200 Subject: [PATCH 26/36] Moved find commands to Viewer --- suplemon/cursor.py | 3 ++ suplemon/editor.py | 96 ---------------------------------------- suplemon/viewer.py | 106 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 106 insertions(+), 99 deletions(-) diff --git a/suplemon/cursor.py b/suplemon/cursor.py index 8e78614..db9d52a 100644 --- a/suplemon/cursor.py +++ b/suplemon/cursor.py @@ -123,6 +123,9 @@ def __ne__(self, item): def __str__(self): return "Cursor({x},{y})".format(x=self.x, y=self.y) + def __repr__(self): + return self.__str__() + def tuple(self): """Return the cursor as a tuple. diff --git a/suplemon/editor.py b/suplemon/editor.py index d9fa95e..2ffb0fd 100644 --- a/suplemon/editor.py +++ b/suplemon/editor.py @@ -51,8 +51,6 @@ def __init__(self, app, window): """ Viewer.__init__(self, app, window) - # Last search used in 'find' - self.last_find = "" # History of editor states for undo/redo self.history = [State()] # Current state index of the editor @@ -86,8 +84,6 @@ def init(self): "copy": self.copy, # Ctrl + C "cut": self.cut, # Ctrl + X "duplicate_line": self.duplicate_line, # Ctrl + W - "find_next": self.find_next, # Ctrl + D - "find_all": self.find_all, # Ctrl + A } for key in operations.keys(): self.operations[key] = operations[key] @@ -548,98 +544,6 @@ def go_to_pos(self, line_no, col=0): self.scroll_to_line(cur.y) self.move_cursors() - def find_query(self): - """Find in file via user input.""" - what = self.app.ui.query("Find:", self.last_find) - if what: - self.find(what) - - def find(self, what, findall=False): - """Find what in data (from top to bottom). Adds a cursor when found.""" - # Sorry for this colossal function - if not what: - return - last_cursor = self.get_last_cursor() - y = last_cursor.y - - found = False - new_cursors = [] - # Loop through all lines starting from the last cursor - while y < len(self.lines): - line = self.lines[y] - x_offset = 0 # Which character to begin searching from - if y == last_cursor.y: - # On the current line begin from the last cursor x pos - x_offset = last_cursor.x - - # Find all occurances of search string - s = str(line[x_offset:]) # Data to search in - pattern = re.escape(what) # Default to non regex pattern - if self.config["regex_find"]: - try: # Try to search with the actual regex - indices = [match.start() for match in re.finditer(what, s)] - except: # Revert to normal search - indices = [match.start() for match in re.finditer(pattern, s)] - else: - indices = [match.start() for match in re.finditer(pattern, s)] - - # Loop through the indices and add cursors if they don't exist yet - for i in indices: - new = Cursor(i+x_offset, y) - if not self.cursor_exists(new): - found = True - new_cursors.append(new) - if not findall: - break - if new not in new_cursors: - new_cursors.append(new) - if found and not findall: - break - y += 1 - - if not new_cursors: - self.app.set_status("Can't find '{0}'".format(what)) - # self.last_find = "" - return - else: - # If we only have one cursor, and it's not - # where the first occurance is, just remove it - if len(self.cursors) == 1 and self.cursors[0].tuple() != new_cursors[0].tuple(): - self.cursors = [] - self.last_find = what # Only store string if it's really found - - # Add the new cursors - for cursor in new_cursors: - self.cursors.append(cursor) - - destination = self.get_last_cursor().y - self.scroll_to_line(destination) - self.store_action_state("find") # Store undo point - - def find_next(self): - """Find next occurance.""" - what = self.last_find - if what == "": - cursor = self.get_cursor() - search = "^([\w\-]+)" - line = self.lines[cursor.y][cursor.x:] - matches = re.match(search, line) - if matches: - what = matches.group(0) - else: - if line: - what = line[0] - # Escape the data if regex is enabled - if self.config["regex_find"]: - what = re.escape(what) - - self.last_find = what - self.find(what) - - def find_all(self): - """Find all occurances.""" - self.find(self.last_find, True) - def duplicate_line(self): """Copy current line and add it below as a new line.""" curs = sorted(self.cursors, key=lambda c: (c.y, c.x)) diff --git a/suplemon/viewer.py b/suplemon/viewer.py index 325bdb8..5b9b166 100644 --- a/suplemon/viewer.py +++ b/suplemon/viewer.py @@ -4,6 +4,7 @@ """ import os +import re import sys import imp import curses @@ -54,6 +55,9 @@ def __init__(self, app, window): # Copy/paste buffer self.buffer = [] + # Last search used in 'find' + self.last_find = "" + # Runnable methods self.operations = { "arrow_right": self.arrow_right, # Arrow Right @@ -68,7 +72,9 @@ def __init__(self, app, window): "page_down": self.page_down, # Page Down "home": self.home, # Home "end": self.end, # End - "find": self.find_query # Ctrl + F + "find": self.find_query, # Ctrl + F + "find_next": self.find_next, # Ctrl + D + "find_all": self.find_all, # Ctrl + A } def init(self): @@ -576,7 +582,7 @@ def remove_cursor(self, cursor): self.cursors.pop(index) return True - def purge_cursors(self): + def purge_cursors(self, render=True): """Remove duplicate cursors that have the same position.""" new = [] # This sucks: can't use "if .. in .." for different instances (?) @@ -587,7 +593,8 @@ def purge_cursors(self): ref.append(cursor.tuple()) new.append(cursor) self.cursors = new - self.render() # TODO: is this needed? + if render: + self.render() # TODO: is this needed? def purge_line_cursors(self, line_no): """Remove all but first cursor on given line.""" @@ -771,6 +778,99 @@ def jump_down(self): self.move_cursors((0, 3)) self.scroll_down() + def find_query(self): + """Find in file via user input.""" + what = self.app.ui.query("Find:", self.last_find) + if what: + self.find(what) + + def find(self, what, findall=False): + """Find what in data (from top to bottom). Adds a cursor when found.""" + # Sorry for this colossal function + if not what: + return + last_cursor = self.get_last_cursor() + y = last_cursor.y + + found = False + new_cursors = [] + # Loop through all lines starting from the last cursor + while y < len(self.lines): + line = self.lines[y] + x_offset = 0 # Which character to begin searching from + if y == last_cursor.y: + # On the current line begin from the last cursor x pos + x_offset = last_cursor.x + + # Find all occurances of search string + s = str(line[x_offset:]) # Data to search in + pattern = re.escape(what) # Default to non regex pattern + if self.config["regex_find"]: + try: # Try to search with the actual regex + indices = [match.start() for match in re.finditer(what, s)] + except: # Revert to normal search + indices = [match.start() for match in re.finditer(pattern, s)] + else: + # Use normal search + indices = [match.start() for match in re.finditer(pattern, s)] + + # Loop through the indices and add cursors if they don't exist yet + for i in indices: + new = Cursor(i+x_offset, y) + if not self.cursor_exists(new): + found = True + if new not in new_cursors: # Make sure we don't get duplicates + new_cursors.append(new) + if not findall: + break + if new not in new_cursors: + new_cursors.append(new) + if found and not findall: + break + y += 1 + + if not new_cursors: + self.app.set_status("Can't find '{0}'".format(what)) + return False + else: + # If we only have one cursor, and it's not + # where the first occurance is, just remove it + if len(self.cursors) == 1 and self.cursors[0].tuple() != new_cursors[0].tuple(): + self.cursors = [] + self.last_find = what # Only store string if it's really found + + # Add the new cursors + for cursor in new_cursors: + if not self.cursor_exists(cursor): + self.cursors.append(cursor) + + destination = self.get_last_cursor().y + self.scroll_to_line(destination) + self.store_action_state("find") # Store undo point + + def find_next(self): + """Find next occurance.""" + what = self.last_find + if what == "": + cursor = self.get_cursor() + search = "^([\w\-]+)" + line = self.lines[cursor.y][cursor.x:] + matches = re.match(search, line) + if matches: + what = matches.group(0) + else: + if line: + what = line[0] + # Escape the data if regex is enabled + if self.config["regex_find"]: + what = re.escape(what) + self.last_find = what + self.find(what) + + def find_all(self): + """Find all occurances.""" + self.find(self.last_find, True) + class Viewer(BaseViewer): def __init__(self, app, window): From 1f97386318eb33fb48029b2b6d9d45ddcb7cc23f Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Fri, 4 Mar 2016 02:20:07 +0200 Subject: [PATCH 27/36] Linting. --- suplemon/editor.py | 2 -- suplemon/main.py | 6 +++--- suplemon/viewer.py | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/suplemon/editor.py b/suplemon/editor.py index 2ffb0fd..5a98621 100644 --- a/suplemon/editor.py +++ b/suplemon/editor.py @@ -3,8 +3,6 @@ Editor class for extending viewer with text editing features. """ -import re - from . import helpers from .line import Line diff --git a/suplemon/main.py b/suplemon/main.py index 0858355..d5fd338 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -615,11 +615,11 @@ def save_file_as(self, file=False): name = self.ui.query("Save as:", f.name) if not name: return False - dir = os.path.dirname(name) - if dir and not os.path.exists(dir): + target_dir = os.path.dirname(name) + if target_dir and not os.path.exists(target_dir): if self.ui.query_bool("The path doesn't exist, do you want to create it?"): self.logger.debug("Creating missing folders in save path.") - os.makedirs(dir) + os.makedirs(target_dir) else: return False f.set_name(name) diff --git a/suplemon/viewer.py b/suplemon/viewer.py index 5b9b166..8be19ad 100644 --- a/suplemon/viewer.py +++ b/suplemon/viewer.py @@ -388,7 +388,7 @@ def render_line_pygments(self, line, pos, x_offset, max_len): def render_line_linelight(self, line, pos, x_offset, max_len): """Render line with naive line based highlighting.""" - x, y = pos + y = pos[1] line_data = line.get_data() line_data = self._prepare_line_for_rendering(line_data, max_len) curs_color = curses.color_pair(self.get_line_color(line)) @@ -396,7 +396,7 @@ def render_line_linelight(self, line, pos, x_offset, max_len): def render_line_normal(self, line, pos, x_offset, max_len): """Render line without any highlighting.""" - x, y = pos + y = pos[1] line_data = line.get_data() line_data = self._prepare_line_for_rendering(line_data, max_len) self.window.addstr(y, x_offset, line_data) From eb44235dfc6a927da337cc7ff6daeadea49ca5af Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 7 Mar 2016 19:53:00 +0200 Subject: [PATCH 28/36] Optimized purging cursors and removed obsolete noupdate argument from move_cursors --- suplemon/viewer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/suplemon/viewer.py b/suplemon/viewer.py index 8be19ad..7055ff6 100644 --- a/suplemon/viewer.py +++ b/suplemon/viewer.py @@ -275,6 +275,7 @@ def move_win(self, yx): def refresh(self): """Refresh the editor curses window.""" + self.move_cursors() self.render() self.window.refresh() @@ -515,7 +516,7 @@ def move_y_scroll(self, delta): # Cursors ########################################################################### - def move_cursors(self, delta=None, noupdate=False): + def move_cursors(self, delta=None): """Move all cursors with delta. To avoid refreshing the screen set noupdate to True.""" if self.app.block_rendering: noupdate = True @@ -550,8 +551,7 @@ def move_cursors(self, delta=None, noupdate=False): self.x_scroll -= abs(cur.x - self.x_scroll) # FIXME if cur.x - self.x_scroll+offset < offset: self.x_scroll -= 1 - if not noupdate: - self.purge_cursors() + self.purge_cursors() def move_x_cursors(self, line, col, delta): """Move all cursors starting at line and col with delta on the x axis.""" From 072379427ea89a832339eb311079270d6201131d Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Fri, 11 Mar 2016 17:50:47 +0200 Subject: [PATCH 29/36] Removed unneccessary try/except from get_line_color. --- suplemon/viewer.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/suplemon/viewer.py b/suplemon/viewer.py index 7055ff6..b48f27e 100644 --- a/suplemon/viewer.py +++ b/suplemon/viewer.py @@ -947,8 +947,7 @@ def get_line_color(self, raw_line): :rtype: int """ if self.syntax: - try: - return self.syntax.get_color(raw_line) - except: - return 0 + color = self.syntax.get_color(raw_line) + if color is not None: + return color return 0 From 75681ef956f763abce58383fe7f3c95ac19402b4 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Fri, 11 Mar 2016 18:06:26 +0200 Subject: [PATCH 30/36] Removed unneeded .keys() calls. --- suplemon/viewer.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/suplemon/viewer.py b/suplemon/viewer.py index b48f27e..80a27cd 100644 --- a/suplemon/viewer.py +++ b/suplemon/viewer.py @@ -423,7 +423,7 @@ def replace_whitespace(self, data): For example tab characters make cursors go out of sync with line contents. """ - for key in self.config["white_space_map"].keys(): + for key in self.config["white_space_map"]: char = " " if self.config["show_white_space"]: char = self.config["white_space_map"][key] @@ -630,9 +630,9 @@ def handle_input(self, event): key_bindings = self.get_key_bindings() operation = None - if key in key_bindings.keys(): + if key in key_bindings: operation = key_bindings[key] - elif name in key_bindings.keys(): + elif name in key_bindings: operation = key_bindings[name] if operation: self.run_operation(operation) @@ -641,7 +641,7 @@ def handle_input(self, event): def run_operation(self, operation): """Run an editor core operation.""" - if operation in self.operations.keys(): + if operation in self.operations: cancel = self.app.trigger_event_before(operation) if cancel: return False @@ -894,7 +894,7 @@ def setup_linelight(self): ext = self.file_extension # Check if a file extension is redefined # Maps e.g. 'scss' to 'css' - if ext in self.extension_map.keys(): + if ext in self.extension_map: ext = self.extension_map[ext] # Use it curr_path = os.path.dirname(os.path.realpath(__file__)) @@ -926,7 +926,7 @@ def setup_highlight(self): return False # Check if a file extension is redefined # Maps e.g. 'scss' to 'css' - if ext in self.extension_map.keys(): + if ext in self.extension_map: ext = self.extension_map[ext] # Use it try: self.pygments_syntax = pygments.lexers.get_lexer_by_name(ext) From 7a138945a1e51bbfc1ff3e934f0a85ac69397bf2 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Fri, 11 Mar 2016 18:24:34 +0200 Subject: [PATCH 31/36] Removed redundant rendering from move_cursors(). --- suplemon/viewer.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/suplemon/viewer.py b/suplemon/viewer.py index 80a27cd..4884ab0 100644 --- a/suplemon/viewer.py +++ b/suplemon/viewer.py @@ -518,9 +518,6 @@ def move_y_scroll(self, delta): def move_cursors(self, delta=None): """Move all cursors with delta. To avoid refreshing the screen set noupdate to True.""" - if self.app.block_rendering: - noupdate = True - for cursor in self.cursors: if delta: if delta[0] != 0 and cursor.x >= 0: @@ -582,7 +579,7 @@ def remove_cursor(self, cursor): self.cursors.pop(index) return True - def purge_cursors(self, render=True): + def purge_cursors(self): """Remove duplicate cursors that have the same position.""" new = [] # This sucks: can't use "if .. in .." for different instances (?) @@ -593,8 +590,6 @@ def purge_cursors(self, render=True): ref.append(cursor.tuple()) new.append(cursor) self.cursors = new - if render: - self.render() # TODO: is this needed? def purge_line_cursors(self, line_no): """Remove all but first cursor on given line.""" From 819fe6e5e630d8fcb21870498ea8ddac90d99f3f Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sat, 12 Mar 2016 00:03:14 +0200 Subject: [PATCH 32/36] Minor logging tweaks. --- suplemon/modules/linter.py | 2 +- suplemon/modules/system_clipboard.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/suplemon/modules/linter.py b/suplemon/modules/linter.py index 076b247..8fa189f 100644 --- a/suplemon/modules/linter.py +++ b/suplemon/modules/linter.py @@ -115,7 +115,7 @@ def get_output(self, cmd): process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=fnull) fnull.close() except (OSError, EnvironmentError): # can't use FileNotFoundError in Python 2 - self.logger.exception("Subprocess failed.") + self.logger.debug("Subprocess failed.") return False out, err = process.communicate() return out diff --git a/suplemon/modules/system_clipboard.py b/suplemon/modules/system_clipboard.py index ea7c911..752abd7 100644 --- a/suplemon/modules/system_clipboard.py +++ b/suplemon/modules/system_clipboard.py @@ -9,7 +9,7 @@ class SystemClipboard(Module): def init(self): self.init_logging(__name__) if not self.has_xsel_support(): - self.logger.warning("xsel not available. Can't use system clipboard.") + self.logger.warning("Can't use system clipboard. Install 'xsel' for system clipboard support.") return False self.bind_event_before("insert", self.insert) self.bind_event_after("copy", self.copy) From 3ebe6e36be9228e81b5dd34240883bcbae9ccf4f Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 13 Mar 2016 13:47:54 +0200 Subject: [PATCH 33/36] More sensible arrow_right implementation. --- suplemon/viewer.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/suplemon/viewer.py b/suplemon/viewer.py index 4884ab0..d192b2a 100644 --- a/suplemon/viewer.py +++ b/suplemon/viewer.py @@ -653,10 +653,14 @@ def arrow_right(self): """Move cursors right.""" for cursor in self.cursors: line = self.lines[cursor.y] - if cursor.y != len(self.lines)-1 and (cursor.x >= len(line) or len(line) == 0): - cursor.move_down() - cursor.set_x(0) - elif cursor.x < len(self.lines[cursor.y]) and len(line) > 0: + # If we are at the end of the line + if cursor.x >= len(line) or len(line) == 0: + # If there is another line, then move down + if cursor.y != len(self.lines)-1: + cursor.move_down() + cursor.set_x(0) + # Otherwise, move the cursor right + else: cursor.move_right() self.move_cursors() self.scroll_down() From 014634ec07464dfbb11b7e8a87325e0efb72ef16 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 13 Mar 2016 17:10:43 +0200 Subject: [PATCH 34/36] Renamed modules.py to module_loader.py. --- suplemon/main.py | 4 ++-- suplemon/{modules.py => module_loader.py} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename suplemon/{modules.py => module_loader.py} (100%) diff --git a/suplemon/main.py b/suplemon/main.py index d5fd338..df16204 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -9,7 +9,7 @@ import sys from . import ui -from . import modules +from . import module_loader from . import themes from . import helpers @@ -103,7 +103,7 @@ def init(self): self.ui.init() # Load extension modules - self.modules = modules.ModuleLoader(self) + self.modules = module_loader.ModuleLoader(self) self.modules.load() # Load themes diff --git a/suplemon/modules.py b/suplemon/module_loader.py similarity index 100% rename from suplemon/modules.py rename to suplemon/module_loader.py From 3a17e573dddf4d1bf8a2e3d452f82f6b919e88e1 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 13 Mar 2016 17:13:27 +0200 Subject: [PATCH 35/36] Better implementation for config commands. --- suplemon/config.py | 35 +++++++++++++++++++++++++++++++++++ suplemon/modules/config.py | 38 ++++++++++---------------------------- suplemon/modules/keymap.py | 38 ++++++++++---------------------------- 3 files changed, 55 insertions(+), 56 deletions(-) diff --git a/suplemon/config.py b/suplemon/config.py index 543c70e..e6ea4af 100644 --- a/suplemon/config.py +++ b/suplemon/config.py @@ -8,6 +8,7 @@ import logging from . import helpers +from . import suplemon_module class Config: @@ -167,3 +168,37 @@ def __str__(self): def __len__(self): """Return length of top level config variables.""" return len(self.config) + + +class ConfigModule(suplemon_module.Module): + """Helper for shortcut for openning config files.""" + def init(self): + self.conf_name = "defaults.json" + self.conf_default_path = os.path.join(self.app.path, "config", self.conf_name) + self.conf_user_path = self.app.config.path() + + def run(self, app, editor, args): + if args == "defaults": + # Open the default config in a new file only for viewing + self.open(app, self.conf_default_path, read_only=True) + else: + self.open(app, self.conf_user_path) + + def open(self, app, path, read_only=False): + if read_only: + f = open(path) + data = f.read() + f.close() + file = app.new_file() + file.set_name(self.conf_name) + file.set_data(data) + app.switch_to_file(app.last_file_index()) + else: + # Open the user config file for editing + f = app.file_is_open(path) + if f: + app.switch_to_file(app.get_file_index(f)) + else: + if not app.open_file(path): + app.new_file(path) + app.switch_to_file(app.last_file_index()) diff --git a/suplemon/modules/config.py b/suplemon/modules/config.py index 264ee91..f77ba3f 100644 --- a/suplemon/modules/config.py +++ b/suplemon/modules/config.py @@ -2,38 +2,20 @@ import os -from suplemon.suplemon_module import Module +from suplemon import config -class Config(Module): - """Shortcut to openning the current config file.""" +class SuplemonConfig(config.ConfigModule): + """Shortcut to openning the keymap config file.""" + def __init__(self, app): + config.ConfigModule.__init__(self, app) - def run(self, app, editor, args): - if args == "defaults": - # Open the default config in a new file only for viewing - path = os.path.join(app.path, "config", "defaults.json") - self.open_read_only(app, path) - else: - # Open the user config file for editing - path = app.config.path() - f = app.file_is_open(path) - if f: - app.switch_to_file(app.get_file_index(f)) - else: - if not app.open_file(path): - app.new_file(path) - app.switch_to_file(app.last_file_index()) - - def open_read_only(self, app, path): - f = open(path) - data = f.read() - f.close() - file = app.new_file() - file.set_name("defaults.json") - file.set_data(data) - app.switch_to_file(app.last_file_index()) + def init(self): + self.conf_name = "defaults.json" + self.conf_default_path = os.path.join(self.app.path, "config", self.conf_name) + self.conf_user_path = self.app.config.path() module = { - "class": Config, + "class": SuplemonConfig, "name": "config", } diff --git a/suplemon/modules/keymap.py b/suplemon/modules/keymap.py index 18732ae..4b6c0d4 100644 --- a/suplemon/modules/keymap.py +++ b/suplemon/modules/keymap.py @@ -2,38 +2,20 @@ import os -from suplemon.suplemon_module import Module +from suplemon import config -class Keymap(Module): - """Shortcut to openning the current config file.""" +class KeymapConfig(config.ConfigModule): + """Shortcut to openning the keymap config file.""" + def __init__(self, app): + config.ConfigModule.__init__(self, app) - def run(self, app, editor, args): - if args == "defaults": - # Open the default config in a new file only for viewing - path = os.path.join(app.path, "config", "keymap.json") - self.open_read_only(app, path) - else: - # Open the user config file for editing - path = app.config.keymap_path() - f = app.file_is_open(path) - if f: - app.switch_to_file(app.get_file_index(f)) - else: - if not app.open_file(path): - app.new_file(path) - app.switch_to_file(app.last_file_index()) - - def open_read_only(self, app, path): - f = open(path) - data = f.read() - f.close() - file = app.new_file() - file.set_name("keymap.json") - file.set_data(data) - app.switch_to_file(app.last_file_index()) + def init(self): + self.conf_name = "keymap.json" + self.conf_default_path = os.path.join(self.app.path, "config", self.conf_name) + self.conf_user_path = self.app.config.keymap_path() module = { - "class": Keymap, + "class": KeymapConfig, "name": "keymap", } From 1ef4cde3a2ffc3e73dec43838753be32835708ef Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 13 Mar 2016 17:42:16 +0200 Subject: [PATCH 36/36] More scopes. --- suplemon/lexer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/suplemon/lexer.py b/suplemon/lexer.py index 5098328..9151396 100644 --- a/suplemon/lexer.py +++ b/suplemon/lexer.py @@ -11,14 +11,16 @@ def __init__(self, app): self.token_map = { pygments.token.Comment: "comment", pygments.token.Comment.Single: "comment", + pygments.token.Operator: "keyword", pygments.token.Name.Function: "entity.name.function", pygments.token.Name.Class: "entity.name.class", pygments.token.Name.Tag: "entity.name.tag", pygments.token.Name.Attribute: "entity.other.attribute-name", pygments.token.Name.Variable: "variable", - pygments.token.Operator: "keyword", pygments.token.Name.Builtin.Pseudo: "constant.language", pygments.token.Literal.String: "string", + pygments.token.Literal.String.Doc: "string", + pygments.token.Punctuation: "punctuation", } def lex(self, code, lex):