diff --git a/CHANGELOG.md b/CHANGELOG.md index b9914b7..6e307f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ Change Log ========== +## [v0.1.64](https://github.com/richrd/suplemon/tree/v0.1.64) (2017-12-17) compared to previous master branch. +[Full Changelog](https://github.com/richrd/suplemon/compare/v0.1.63...v0.1.64) + +**Implemented enhancements:** + +- Add bulk_delete and sort_lines commands. +- Lots of code style fixes and improvements. Credit @Gnewbee +- Add xclip support for system clipboard. Credit @LChris314 +- Added command docs to readme and help. + ## [v0.1.63](https://github.com/richrd/suplemon/tree/v0.1.63) (2017-10-05) compared to previous master branch. [Full Changelog](https://github.com/richrd/suplemon/compare/v0.1.62...v0.1.63) diff --git a/README.md b/README.md index f461fb1..92f6fe0 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ http://github.com/richrd/suplemon * Find, Find next and Find all (Ctrl + F, Ctrl + D, Ctrl + A) * Custom keyboard shortcuts (and easy-to-use defaults) * Mouse support - * Restores cursor positions in when reopenning files + * Restores cursor and scroll positions when reopenning files * Extensions (easy to write your own) * Lots more... @@ -81,7 +81,7 @@ No dependencies outside the Python Standard Library required. * Flake8 > For showing linting for Python files. - * xsel + * xsel or xclip > For system clipboard support on X Window (Linux). * pbcopy / pbpaste @@ -97,13 +97,6 @@ It is as easy as nano, and has much of the power of Sublime Text. It also suppor to allow all kinds of customizations. To get more help hit ```Ctrl + H``` in the editor. Suplemon is licensed under the MIT license. -## Goals - 1. [X] Create a command line text editor with built in multi cursor support. It's awesome! - 2. [X] Usability should be even better and easier than nano. It's on par with desktop editors. - 3. [X] Multi cursor should be comparable to Sublime Text. - 4. [X] Develop Suplemon with Suplemon!!! I've used Suplemon for a long time as my main - editor (replacing ST and nano) for all developement, Git commits and everything else. - ## Configuration ### Main Config @@ -215,6 +208,127 @@ To view the default keymap file run ```keymap default``` * Scroll Wheel Up / Down > Scroll up & down. +## Commands + +Suplemon has various add-ons that implement extra features. +The commands can be run with Ctrl + E and the prompt has autocomplete to make running them faster. +The available commands and their descriptions are: + + * autocomplete + + A simple autocompletion module. + + This adds autocomplete support for the tab key. It uses a word + list scanned from all open files for completions. By default it suggests + the shortest possible match. If there are no matches, the tab action is + run normally. + + * autodocstring + + Simple module for adding docstring placeholders. + + This module is intended to generate docstrings for Python functions. + It adds placeholders for descriptions, arguments and return data. + Function arguments are crudely parsed from the function definition + and return statements are scanned from the function body. + + * bulk_delete + + Bulk delete lines and characters. + Asks what direction to delete in by default. + + Add 'up' to delete lines above highest cursor. + Add 'down' to delete lines below lowest cursor. + Add 'left' to delete characters to the left of all cursors. + Add 'right' to delete characters to the right of all cursors. + + * comment + + Toggle line commenting based on current file syntax. + + * config + + Shortcut for openning the config files. + + * diff + + View a diff of the current file compared to it's on disk version. + + * eval + + Evaluate a python expression and show the result in the status bar. + + If no expression is provided the current line(s) are evaluated and + replaced with the evaluation result. + + * keymap + + Shortcut to openning the keymap config file. + + * linter + + Linter for suplemon. + + * lower + + Transform current lines to lower case. + + * lstrip + + Trim whitespace from beginning of current lines. + + * paste + + Toggle paste mode (helpful when pasting over SSH if auto indent is enabled) + + * reload + + Reload all add-on modules. + + * replace_all + + Replace all occurrences in all files of given text with given replacement. + + * reverse + + Reverse text on current line(s). + + * rstrip + + Trim whitespace from the end of lines. + + * save + + Save the current file. + + * save_all + + Save all currently open files. Asks for confirmation. + + * sort_lines + + Sort current lines. + + Sorts alphabetically by default. + Add 'length' to sort by length. + Add 'reverse' to reverse the sorting. + + * strip + + Trim whitespace from start and end of lines. + + * tabstospaces + + Convert tab characters to spaces in the entire file. + + * toggle_whitespace + + Toggle visually showing whitespace. + + * upper + + Transform current lines to upper case. + ## Support @@ -238,29 +352,6 @@ PRs are very welcome and appreciated. When making PRs make sure to set the target branch to `dev`. I only push to master when releasing new versions. -## Todo - * [ ] Design proper API for plugins/extensions/macros - * [ ] Documentation for v 1.0.0 - -## Wishlist (Stuff that would be nice, but not planning to do yet. *Maybe* for 2.0.0) - * [ ] Core - * [ ] Setting for enabling/disabling undo for cursor changes - * [ ] Selections - * [ ] List of recent files - * [X] Optionally Remember cursor positions in files (and restore when opened again) - * [ ] Read only viewer - * ~~And disable editing~~ Don't disable editing. Instead enable save as. - * [ ] Extensions: - * [ ] Peer to peer colaborative editing. Could be implemented as an extension. - * [ ] Auto backup. Activate on n changes or every n seconds - * [ ] File selector, kind of like what nano has - * [ ] This should be implemented as an extension - * [ ] Could be triggered with a key binding (and/or override open file) - * [ ] Need to refactor App class to support views instead of just files - * [ ] A view could be an editor or an extension ui - * [ ] Extensions should be able to control both status bars and key legend - - ## Rationale For many the command line is a different environment for text editing. Most coders are familiar with GUI text editors and for many vi and emacs diff --git a/suplemon/config.py b/suplemon/config.py index 4eabde3..c39be5b 100644 --- a/suplemon/config.py +++ b/suplemon/config.py @@ -7,7 +7,6 @@ import json import logging -from . import helpers from . import suplemon_module @@ -161,7 +160,7 @@ def remove_config_comments(self, data): cleaned = [] for line in lines: line = line.strip() - if helpers.starts(line, "//") or helpers.starts(line, "#"): + if line.startswith(("//", "#")): continue cleaned.append(line) return "\n".join(cleaned) diff --git a/suplemon/editor.py b/suplemon/editor.py index edbeb97..4a1d813 100644 --- a/suplemon/editor.py +++ b/suplemon/editor.py @@ -484,13 +484,12 @@ def copy(self): copy_buffer = [] # Get all lines with cursors on them line_nums = self.get_lines_with_cursors() - i = 0 - while i < len(line_nums): + + for i in range(len(line_nums)): # Get the line line = self.lines[line_nums[i]] # Put it in our temporary buffer copy_buffer.append(line.get_data()) - i += 1 self.set_buffer(copy_buffer) self.store_action_state("copy") @@ -502,8 +501,7 @@ def cut(self): line_nums = self.get_lines_with_cursors() # Sort from last to first (invert order) line_nums = line_nums[::-1] - i = 0 - while i < len(line_nums): # Iterate from last to first + for i in range(len(line_nums)): # Iterate from last to first # Make sure we don't completely remove the last line if len(self.lines) == 1: cut_buffer.append(self.lines[0]) @@ -517,7 +515,6 @@ def cut(self): cut_buffer.append(line) # Move all cursors below the current line up self.move_y_cursors(line_no, -1) - i += 1 self.move_cursors() # Make sure cursors are in valid places # Reverse the buffer to get correct order and store it self.set_buffer(cut_buffer[::-1]) diff --git a/suplemon/help.py b/suplemon/help.py index 2f4a1fb..ab69614 100644 --- a/suplemon/help.py +++ b/suplemon/help.py @@ -133,4 +133,120 @@ and then typing the command name. Commands are extensions and are stored in the modules folder in the Suplemon installation. + * autocomplete + + A simple autocompletion module. + + This adds autocomplete support for the tab key. It uses a word + list scanned from all open files for completions. By default it suggests + the shortest possible match. If there are no matches, the tab action is + run normally. + + * autodocstring + + Simple module for adding docstring placeholders. + + This module is intended to generate docstrings for Python functions. + It adds placeholders for descriptions, arguments and return data. + Function arguments are crudely parsed from the function definition + and return statements are scanned from the function body. + + * bulk_delete + + Bulk delete lines and characters. + Asks what direction to delete in by default. + + Add 'up' to delete lines above highest cursor. + Add 'down' to delete lines below lowest cursor. + Add 'left' to delete characters to the left of all cursors. + Add 'right' to delete characters to the right of all cursors. + + * comment + + Toggle line commenting based on current file syntax. + + * config + + Shortcut for openning the config files. + + * diff + + View a diff of the current file compared to it's on disk version. + + * eval + + Evaluate a python expression and show the result in the status bar. + + If no expression is provided the current line(s) are evaluated and + replaced with the evaluation result. + + * keymap + + Shortcut to openning the keymap config file. + + * linter + + Linter for suplemon. + + * lower + + Transform current lines to lower case. + + * lstrip + + Trim whitespace from beginning of current lines. + + * paste + + Toggle paste mode (helpful when pasting over SSH if auto indent is enabled) + + * reload + + Reload all add-on modules. + + * replace_all + + Replace all occurrences in all files of given text with given replacement. + + * reverse + + Reverse text on current line(s). + + * rstrip + + Trim whitespace from the end of lines. + + * save + + Save the current file. + + * save_all + + Save all currently open files. Asks for confirmation. + + * sort_lines + + Sort current lines. + + Sorts alphabetically by default. + Add 'length' to sort by length. + Add 'reverse' to reverse the sorting. + + * strip + + Trim whitespace from start and end of lines. + + * tabstospaces + + Convert tab characters to spaces in the entire file. + + * toggle_whitespace + + Toggle visually showing whitespace. + + * upper + + Transform current lines to upper case. + + """ diff --git a/suplemon/helpers.py b/suplemon/helpers.py index fdd26aa..409fdd3 100644 --- a/suplemon/helpers.py +++ b/suplemon/helpers.py @@ -19,27 +19,6 @@ def curr_time_sec(): return time.strftime("%H:%M:%S") -def starts(s, what): - """Check if a string begins with given string or any one in given list.""" - if isinstance(what, str): - what = [what] - for item in what: - if s.find(item) == 0: - return True - return False - - -def ends(s, what): - """Check if a string ends with given string or any one in given list.""" - s = s[::-1] - if isinstance(what, str): - what = [what] - for item in what: - if s.find(item[::-1]) == 0: - return True - return False - - def multisplit(data, delimiters): pattern = "|".join(map(re.escape, delimiters)) return re.split(pattern, data) diff --git a/suplemon/linelight/css.py b/suplemon/linelight/css.py index 386abde..e44f35d 100644 --- a/suplemon/linelight/css.py +++ b/suplemon/linelight/css.py @@ -1,4 +1,3 @@ -from suplemon import helpers from suplemon.linelight.color_map import color_map @@ -9,14 +8,14 @@ def get_comment(self): def get_color(self, raw_line): color = color_map["white"] line = raw_line.strip() - if helpers.starts(line, "@import"): + if line.startswith("@import"): color = color_map["blue"] - elif helpers.starts(line, "$"): + elif line.startswith("$"): color = color_map["green"] - elif helpers.starts(line, "/*") or helpers.ends(line, "*/"): + elif line.startswith("/*") or line.endswith("*/"): color = color_map["magenta"] - elif helpers.starts(line, "{") or helpers.ends(line, "}") or helpers.ends(line, "{"): + elif line.startswith("{") or line.endswith(("}", "{")): color = color_map["cyan"] - elif helpers.ends(line, ";"): + elif line.endswith(";"): color = color_map["yellow"] return color diff --git a/suplemon/linelight/html.py b/suplemon/linelight/html.py index 8c05167..92b49c7 100644 --- a/suplemon/linelight/html.py +++ b/suplemon/linelight/html.py @@ -1,4 +1,3 @@ -from suplemon import helpers from suplemon.linelight.color_map import color_map @@ -9,12 +8,12 @@ def get_comment(self): def get_color(self, raw_line): color = color_map["white"] line = raw_line.strip() - if helpers.starts(line, ["#", "//", "/*", "*/", ""]): + elif line.endswith(("*/", "-->")): color = color_map["magenta"] - elif helpers.starts(line, "<"): + elif line.startswith("<"): color = color_map["cyan"] - elif helpers.ends(line, ">"): + elif line.endswith(">"): color = color_map["cyan"] return color diff --git a/suplemon/linelight/js.py b/suplemon/linelight/js.py index fe93ec5..c98e653 100644 --- a/suplemon/linelight/js.py +++ b/suplemon/linelight/js.py @@ -1,4 +1,3 @@ -from suplemon import helpers from suplemon.linelight.color_map import color_map @@ -9,14 +8,14 @@ def get_comment(self): def get_color(self, raw_line): color = color_map["white"] line = raw_line.strip() - if helpers.starts(line, "function"): + if line.startswith("function"): color = color_map["cyan"] - elif helpers.starts(line, ["return"]): + elif line.startswith("return"): color = color_map["red"] - elif helpers.starts(line, "this."): + elif line.startswith("this."): color = color_map["cyan"] - elif helpers.starts(line, ["//", "/*", "*/", "*"]): + elif line.startswith(("//", "/*", "*/", "*")): color = color_map["magenta"] - elif helpers.starts(line, ["if", "else", "for ", "while ", "continue", "break"]): + elif line.startswith(("if", "else", "for ", "while ", "continue", "break")): color = color_map["yellow"] return color diff --git a/suplemon/linelight/json.py b/suplemon/linelight/json.py index 8a7c682..6cc0bf8 100644 --- a/suplemon/linelight/json.py +++ b/suplemon/linelight/json.py @@ -1,4 +1,3 @@ -from suplemon import helpers from suplemon.linelight.color_map import color_map @@ -9,8 +8,8 @@ def get_comment(self, line): def get_color(self, raw_line): color = color_map["white"] line = raw_line.strip() - if helpers.starts(line, ["{", "}"]): + if line.startswith(("{", "}")): color = color_map["yellow"] - elif helpers.starts(line, "\""): + elif line.startswith("\""): color = color_map["green"] return color diff --git a/suplemon/linelight/md.py b/suplemon/linelight/md.py index d10830a..d033f08 100644 --- a/suplemon/linelight/md.py +++ b/suplemon/linelight/md.py @@ -1,4 +1,3 @@ -from suplemon import helpers from suplemon.linelight.color_map import color_map @@ -9,12 +8,12 @@ def get_comment(self, line): def get_color(self, raw_line): color = color_map["white"] line = raw_line.strip() - if helpers.starts(line, ["*", "-"]): # List + if line.startswith(("*", "-")): # List color = color_map["cyan"] - elif helpers.starts(line, "#"): # Header + elif line.startswith("#"): # Header color = color_map["green"] - elif helpers.starts(line, ">"): # Item desription + elif line.startswith(">"): # Item description color = color_map["yellow"] - elif helpers.starts(raw_line, " "): # Code + elif raw_line.startswith(" "): # Code color = color_map["magenta"] return color diff --git a/suplemon/linelight/php.py b/suplemon/linelight/php.py index 8f8f043..0854e2f 100644 --- a/suplemon/linelight/php.py +++ b/suplemon/linelight/php.py @@ -1,4 +1,3 @@ -from suplemon import helpers from suplemon.linelight.color_map import color_map @@ -9,20 +8,20 @@ def get_comment(self): def get_color(self, raw_line): color = color_map["white"] line = raw_line.strip() - keywords = ["if", "else", "finally", "try", "catch", "foreach", - "while", "continue", "pass", "break"] - if helpers.starts(line, ["include", "require"]): + keywords = ("if", "else", "finally", "try", "catch", "foreach", + "while", "continue", "pass", "break") + if line.startswith(("include", "require")): color = color_map["blue"] - elif helpers.starts(line, ["class", "public", "private", "function"]): + elif line.startswith(("class", "public", "private", "function")): color = color_map["green"] - elif helpers.starts(line, "def"): + elif line.startswith("def"): color = color_map["cyan"] - elif helpers.starts(line, ["return"]): + elif line.startswith("return"): color = color_map["red"] - elif helpers.starts(line, "$"): + elif line.startswith("$"): color = color_map["cyan"] - elif helpers.starts(line, ["#", "//", "/*", "*/"]): + elif line.startswith(("#", "//", "/*", "*/")): color = color_map["magenta"] - elif helpers.starts(line, keywords): + elif line.startswith(keywords): color = color_map["yellow"] return color diff --git a/suplemon/linelight/py.py b/suplemon/linelight/py.py index c149ebe..6abfc35 100644 --- a/suplemon/linelight/py.py +++ b/suplemon/linelight/py.py @@ -1,4 +1,3 @@ -from suplemon import helpers from suplemon.linelight.color_map import color_map @@ -9,20 +8,20 @@ def get_comment(self): def get_color(self, raw_line): color = color_map["white"] line = raw_line.strip() - keywords = ["if", "elif", "else", "finally", "try", "except", - "for ", "while ", "continue", "pass", "break"] - if helpers.starts(line, ["import", "from"]): + keywords = ("if", "elif", "else", "finally", "try", "except", + "for ", "while ", "continue", "pass", "break") + if line.startswith(("import", "from")): color = color_map["blue"] - elif helpers.starts(line, "class"): + elif line.startswith("class"): color = color_map["green"] - elif helpers.starts(line, "def"): + elif line.startswith("def"): color = color_map["cyan"] - elif helpers.starts(line, ["return", "yield"]): + elif line.startswith(("return", "yield")): color = color_map["red"] - elif helpers.starts(line, "self."): + elif line.startswith("self."): color = color_map["cyan"] - elif helpers.starts(line, ["#", "//", "\"", "'", ":"]): + elif line.startswith(("#", "//", "\"", "'", ":")): color = color_map["magenta"] - elif helpers.starts(line, keywords): + elif line.startswith(keywords): color = color_map["yellow"] return color diff --git a/suplemon/main.py b/suplemon/main.py index 0d1c1d7..6d04136 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -30,9 +30,9 @@ def __init__(self, filenames=None, config_file=None): :param str filenames[*]: Path to a file to load """ self.version = __version__ - self.inited = 0 - self.running = 0 - self.debug = 1 + self.inited = False + self.running = False + self.debug = True self.block_rendering = False # Set default variables @@ -44,6 +44,11 @@ def __init__(self, filenames=None, config_file=None): self.global_buffer = [] self.event_bindings = {} + self.config = None + self.ui = None + self.modules = None + self.themes = None + # Maximum amount of inputs to process at once self.max_input = 100 @@ -110,14 +115,14 @@ def init(self): self.themes = themes.ThemeLoader(self) # Indicate that initialization is complete - self.inited = 1 + self.inited = True return True def exit(self): """Stop the main loop and exit.""" self.trigger_event_before("app_exit") - self.running = 0 + self.running = False def run(self): """Run the app via the ui wrapper.""" @@ -157,7 +162,7 @@ def load(self): "Python 3.3 or higher." .format(version=ver)) self.load_files() - self.running = 1 + self.running = True self.trigger_event_after("app_loaded") def on_input(self, event): @@ -175,14 +180,12 @@ def main_loop(self): got_input = False # Run through max 100 inputs (so the view is updated at least every 100 characters) - i = 0 - while i < self.max_input: + for i in range(self.max_input): event = self.ui.get_input(False) # non-blocking if not event: break # no more inputs to process at this time - i += 1 got_input = True self.on_input(event) @@ -530,14 +533,10 @@ def trigger_event_after(self, event): def toggle_fullscreen(self): """Toggle full screen editor.""" display = self.config["display"] - if display["show_top_bar"]: - display["show_top_bar"] = 0 - display["show_bottom_bar"] = 0 - display["show_legend"] = 0 - else: - display["show_top_bar"] = 1 - display["show_bottom_bar"] = 1 - display["show_legend"] = 1 + show_indicators = not display["show_top_bar"] + display["show_top_bar"] = show_indicators + display["show_bottom_bar"] = show_indicators + display["show_legend"] = show_indicators # Virtual curses windows need to be resized self.ui.resize() diff --git a/suplemon/module_loader.py b/suplemon/module_loader.py index dfdc7c3..2441df7 100644 --- a/suplemon/module_loader.py +++ b/suplemon/module_loader.py @@ -21,25 +21,34 @@ def __init__(self, app=None): def load(self): """Find and load available modules.""" self.logger.debug("Loading modules...") + names = self.get_module_names() + for name in names: + module = self.load_single(name) + if module: + # Load and store the module instance + inst = self.load_instance(module) + if inst: + self.modules[module[0]] = inst + + def get_module_names(self): + """Get names of loadable modules.""" + names = [] dirlist = os.listdir(self.module_path) for item in dirlist: - # Skip 'hidden' dot files - if item[0] == ".": + # Skip 'hidden' dot files and files beginning with and underscore + if item.startswith((".", "_")): continue parts = item.split(".") if len(parts) < 2: + # Can't find file extension continue name = parts[0] ext = parts[-1] - - # only load .py modules that don't begin with an underscore - if ext == "py" and name[0] != "_": - module = self.load_single(name) - if module: - # Load and store the module instance - inst = self.load_instance(module) - if inst: - self.modules[module[0]] = inst + # only load .py modules + if ext != "py": + continue + names.append(name) + return names def load_instance(self, module): """Initialize a module.""" @@ -64,7 +73,24 @@ def load_single(self, name): mod.module["status"] = False return name, mod.module + def extract_docs(self): + """Get names and docs of runnable modules and print as markdown.""" + names = sorted(self.get_module_names()) + for name in names: + name, module = self.load_single(name) + # Skip modules that can't be run expicitly + if module["class"].run.__module__ == "suplemon.suplemon_module": + continue + # Skip undocumented modules + if not module["class"].__doc__: + continue + docstring = module["class"].__doc__ + docstring = "\n " + docstring.strip() + + doc = " * {0}\n{1}\n".format(name, docstring) + print(doc) + if __name__ == "__main__": ml = ModuleLoader() - ml.load() + ml.extract_docs() diff --git a/suplemon/modules/application_state.py b/suplemon/modules/application_state.py index db81054..8174657 100644 --- a/suplemon/modules/application_state.py +++ b/suplemon/modules/application_state.py @@ -7,6 +7,12 @@ class ApplicationState(Module): + """ + Stores the state of open files when exiting the editor and restores when files are reopened. + + Cursor positions and scroll position are stored and restored. + """ + def init(self): self.init_logging(__name__) self.bind_event_after("app_loaded", self.on_load) @@ -31,8 +37,8 @@ def get_file_state(self, file): """Get the state of a single file.""" editor = file.get_editor() state = { - "cursors": [cursor.tuple() for cursor in editor.get_cursors()], - "scroll_pos": editor.get_scroll_pos(), + "cursors": [cursor.tuple() for cursor in editor.cursors], + "scroll_pos": editor.scroll_pos, "hash": self.get_hash(editor), } return state @@ -47,7 +53,7 @@ def get_hash(self, editor): def set_file_state(self, file, state): """Set the state of a file.""" file.editor.set_cursors(state["cursors"]) - file.editor.set_scroll_pos(state["scroll_pos"]) + file.editor.scroll_pos = state["scroll_pos"] def store_states(self): """Store the states of opened files.""" diff --git a/suplemon/modules/autocomplete.py b/suplemon/modules/autocomplete.py index bfe131b..d930a7e 100644 --- a/suplemon/modules/autocomplete.py +++ b/suplemon/modules/autocomplete.py @@ -10,7 +10,7 @@ class AutoComplete(Module): """ A simple autocompletion module. - This module adds autocomplete support for the tab event. It uses a word + This adds autocomplete support for the tab key. It uses a word list scanned from all open files for completions. By default it suggests the shortest possible match. If there are no matches, the tab action is run normally. @@ -61,7 +61,7 @@ def get_match(self, word): # Build list of suitable matches candidates = [] for candidate in self.word_list: - if helpers.starts(candidate, word) and len(candidate) > len(word): + if candidate.startswith(word) and len(candidate) > len(word): candidates.append(candidate) # Find the shortest match # TODO: implement cycling through matches diff --git a/suplemon/modules/autodocstring.py b/suplemon/modules/autodocstring.py index dfff5ca..ecc91be 100644 --- a/suplemon/modules/autodocstring.py +++ b/suplemon/modules/autodocstring.py @@ -34,7 +34,7 @@ def run(self, app, editor, args): cursor = editor.get_cursor() line = editor.get_line(cursor.y) line_data = line.get_data() - if not helpers.starts(line_data.strip(), "def "): + if not line_data.strip().startswith("def "): app.set_status("Current line isn't a function definition.") return False @@ -126,15 +126,13 @@ def get_function_returns(self, editor, line_number): :param line_number: Line number of the function definition. :return: Boolean indicating wether the function something. """ - i = line_number+1 - while i < len(editor.lines): + for i in range(line_number+1, len(editor.lines)): line = editor.get_line(i) data = line.get_data().strip() - if helpers.starts(data, "def "): + if data.startswith("def "): break - if helpers.starts(data, "return "): + if data.startswith("return "): return True - i += 1 return False diff --git a/suplemon/modules/bulk_delete.py b/suplemon/modules/bulk_delete.py new file mode 100644 index 0000000..87cb9e3 --- /dev/null +++ b/suplemon/modules/bulk_delete.py @@ -0,0 +1,75 @@ +# -*- encoding: utf-8 + +from suplemon.suplemon_module import Module + + +class BulkDelete(Module): + """ + Bulk delete lines and characters. + Asks what direction to delete in by default. + + Add 'up' to delete lines above highest cursor. + Add 'down' to delete lines below lowest cursor. + Add 'left' to delete characters to the left of all cursors. + Add 'right' to delete characters to the right of all cursors. + """ + + def init(self): + self.directions = ["up", "down", "left", "right"] + + def handler(self, prompt, event): + # Get arrow keys from prompt + if event.key_name in self.directions: + prompt.set_data(event.key_name) + prompt.on_ready() + return True # Disable normal key handling + + def run(self, app, editor, args): + direction = args.lower() + if not direction: + direction = app.ui.query_filtered("Press arrow key in direction to delete:", handler=self.handler) + + if direction not in self.directions: + app.set_status("Invalid direction.") + return False + + # Delete entire lines + if direction == "up": + pos = editor.get_first_cursor() + length = len(editor.lines) + editor.lines = editor.lines[pos.y:] + delta = length - len(editor.lines) + # If lines were removed, move the cursors up the same amount + if delta: + editor.move_cursors((0, -delta)) + + elif direction == "down": + pos = editor.get_last_cursor() + editor.lines = editor.lines[:pos.y+1] + + # Delete from start or end of lines + else: + # Select min/max function based on direction + func = min if direction == "left" else max + # Get all lines with cursors + line_indices = editor.get_lines_with_cursors() + for line_no in line_indices: + # Get all cursors for the line + line_cursors = editor.get_cursors_on_line(line_no) + # Get the leftmost of rightmost x coordinate + x = func(line_cursors, key=lambda c: c.x).x + + # Delete correct part of the line + line = editor.lines[line_no] + if direction == "left": + line.data = line.data[x:] + # Also move cursors appropriately when deleting left side + [c.move_left(x) for c in line_cursors] + else: + line.data = line.data[:x] + + +module = { + "class": BulkDelete, + "name": "bulk_delete", +} diff --git a/suplemon/modules/clock.py b/suplemon/modules/clock.py index 7fbed3d..473a069 100644 --- a/suplemon/modules/clock.py +++ b/suplemon/modules/clock.py @@ -7,6 +7,7 @@ class Clock(Module): """Shows a clock in the top status bar.""" + def get_status(self): s = time.strftime("%H:%M") if self.app.config["app"]["use_unicode_symbols"]: diff --git a/suplemon/modules/comment.py b/suplemon/modules/comment.py index 50eb04c..aeb1dba 100644 --- a/suplemon/modules/comment.py +++ b/suplemon/modules/comment.py @@ -5,7 +5,7 @@ class Comment(Module): - """Toggles line commenting based on current file syntax.""" + """Toggle line commenting based on current file syntax.""" def run(self, app, editor, args): """Comment the current line(s).""" @@ -24,13 +24,13 @@ def run(self, app, editor, args): target = str(line).strip() w = helpers.whitespace(line) # Amount of whitespace at line start # If the line starts with comment syntax - if helpers.starts(target, comment[0]): + if target.startswith(comment[0]): # Reconstruct the whitespace and add the line new_line = (" "*w) + line[w+len(comment[0]):] # If comment end syntax exists if comment[1]: # Try to remove it from the end of the line - if helpers.ends(new_line, comment[1]): + if new_line.endswith(comment[1]): new_line = new_line[:-1*len(comment[1])] # Store the modified line # editor.lines[lnum] = Line(new_line) diff --git a/suplemon/modules/config.py b/suplemon/modules/config.py index 0bda121..0f6fee7 100644 --- a/suplemon/modules/config.py +++ b/suplemon/modules/config.py @@ -6,7 +6,8 @@ class SuplemonConfig(config.ConfigModule): - """Shortcut to openning the keymap config file.""" + """Shortcut for openning the config files.""" + def init(self): self.config_name = "defaults.json" self.config_default_path = os.path.join(self.app.path, "config", self.config_name) diff --git a/suplemon/modules/date.py b/suplemon/modules/date.py index b98fcde..ecc2e4e 100644 --- a/suplemon/modules/date.py +++ b/suplemon/modules/date.py @@ -6,6 +6,8 @@ class Date(Module): + """Shows the current date without year in the top status bar.""" + def get_status(self): s = time.strftime("%d.%m.") if self.app.config["app"]["use_unicode_symbols"]: diff --git a/suplemon/modules/diff.py b/suplemon/modules/diff.py index 5add943..4e79c42 100644 --- a/suplemon/modules/diff.py +++ b/suplemon/modules/diff.py @@ -7,6 +7,7 @@ class Diff(Module): """View a diff of the current file compared to it's on disk version.""" + def run(self, app, editor, args): curr_file = app.get_file() curr_path = curr_file.get_path() diff --git a/suplemon/modules/eval.py b/suplemon/modules/eval.py index 0135bfd..8e3bd92 100644 --- a/suplemon/modules/eval.py +++ b/suplemon/modules/eval.py @@ -4,6 +4,13 @@ class Eval(Module): + """ + Evaluate a python expression and show the result in the status bar. + + If no expression is provided the current line(s) are evaluated and + replaced with the evaluation result. + """ + def run(self, app, editor, args): if not args: return self.evaluate_lines(editor) diff --git a/suplemon/modules/keymap.py b/suplemon/modules/keymap.py index df48ca6..a126d82 100644 --- a/suplemon/modules/keymap.py +++ b/suplemon/modules/keymap.py @@ -7,6 +7,7 @@ class KeymapConfig(config.ConfigModule): """Shortcut to openning the keymap config file.""" + def init(self): self.config_name = "keymap.json" self.config_default_path = os.path.join(self.app.path, "config", self.config_name) diff --git a/suplemon/modules/linter.py b/suplemon/modules/linter.py index 65ff15e..f6d7d52 100644 --- a/suplemon/modules/linter.py +++ b/suplemon/modules/linter.py @@ -9,6 +9,8 @@ class Linter(Module): + """Linter for suplemon.""" + def init(self): self.init_logging(__name__) @@ -80,8 +82,7 @@ def lint_file(self, file): return False editor = file.get_editor() - line_no = 0 - while line_no < len(editor.lines): + for line_no in range(len(editor.lines)): line = editor.lines[line_no] if line_no+1 in linting.keys(): line.linting = linting[line_no+1] @@ -89,7 +90,6 @@ def lint_file(self, file): else: line.linting = False line.reset_number_color() - line_no += 1 def get_msgs_on_line(self, editor, line_no): line = editor.lines[line_no] diff --git a/suplemon/modules/lower.py b/suplemon/modules/lower.py index a4e1824..0679dc3 100644 --- a/suplemon/modules/lower.py +++ b/suplemon/modules/lower.py @@ -4,6 +4,8 @@ class Lower(Module): + """Transform current lines to lower case.""" + def run(self, app, editor, args): line_nums = [] for cursor in editor.cursors: diff --git a/suplemon/modules/lstrip.py b/suplemon/modules/lstrip.py index 32b8497..f4cab49 100644 --- a/suplemon/modules/lstrip.py +++ b/suplemon/modules/lstrip.py @@ -4,6 +4,8 @@ class LStrip(Module): + """Trim whitespace from beginning of current lines.""" + def run(self, app, editor, args): # TODO: move cursors in sync with line contents line_nums = editor.get_lines_with_cursors() diff --git a/suplemon/modules/paste.py b/suplemon/modules/paste.py index 130e255..ecbf547 100644 --- a/suplemon/modules/paste.py +++ b/suplemon/modules/paste.py @@ -4,6 +4,8 @@ class Paste(Module): + """Toggle paste mode (helpful when pasting over SSH if auto indent is enabled)""" + def init(self): # Flag for paste mode self.active = False diff --git a/suplemon/modules/reload.py b/suplemon/modules/reload.py index f00971d..ee443ee 100644 --- a/suplemon/modules/reload.py +++ b/suplemon/modules/reload.py @@ -4,7 +4,8 @@ class Reload(Module): - """Reload all addon modules.""" + """Reload all add-on modules.""" + def run(self, app, editor, args): self.app.modules.load() diff --git a/suplemon/modules/replace_all.py b/suplemon/modules/replace_all.py index c5d2e3c..b093a53 100644 --- a/suplemon/modules/replace_all.py +++ b/suplemon/modules/replace_all.py @@ -4,6 +4,8 @@ class ReplaceAll(Module): + """Replace all occurrences in all files of given text with given replacement.""" + def run(self, app, editor, args): r_from = self.app.ui.query("Replace text:") if not r_from: diff --git a/suplemon/modules/reverse.py b/suplemon/modules/reverse.py index c131334..47937fe 100644 --- a/suplemon/modules/reverse.py +++ b/suplemon/modules/reverse.py @@ -4,6 +4,8 @@ class Reverse(Module): + """Reverse text on current line(s).""" + def run(self, app, editor, args): line_nums = [] for cursor in editor.cursors: diff --git a/suplemon/modules/rstrip.py b/suplemon/modules/rstrip.py index c41e98f..8eb6e00 100644 --- a/suplemon/modules/rstrip.py +++ b/suplemon/modules/rstrip.py @@ -4,7 +4,8 @@ class RStrip(Module): - """Strips whitespace from end of line.""" + """Trim whitespace from the end of lines.""" + def run(self, app, editor, args): line_nums = editor.get_lines_with_cursors() for n in line_nums: diff --git a/suplemon/modules/save.py b/suplemon/modules/save.py index 3bd76b5..24db054 100644 --- a/suplemon/modules/save.py +++ b/suplemon/modules/save.py @@ -4,6 +4,8 @@ class Save(Module): + """Save the current file.""" + def run(self, app, editor, args): return app.save_file() diff --git a/suplemon/modules/save_all.py b/suplemon/modules/save_all.py index 54b4fe8..d608c83 100644 --- a/suplemon/modules/save_all.py +++ b/suplemon/modules/save_all.py @@ -4,6 +4,8 @@ class SaveAll(Module): + """Save all currently open files. Asks for confirmation.""" + def run(self, app, editor, args): if not self.app.ui.query_bool("Save all files?", False): return False diff --git a/suplemon/modules/sort_lines.py b/suplemon/modules/sort_lines.py new file mode 100644 index 0000000..6994f2d --- /dev/null +++ b/suplemon/modules/sort_lines.py @@ -0,0 +1,41 @@ +# -*- encoding: utf-8 + +from suplemon.suplemon_module import Module + + +class SortLines(Module): + """ + Sort current lines. + + Sorts alphabetically by default. + Add 'length' to sort by length. + Add 'reverse' to reverse the sorting. + """ + + def sort_normal(self, line): + return line.data + + def sort_length(self, line): + return len(line.data) + + def run(self, app, editor, args): + args = args.lower() + + sorter = self.sort_normal + reverse = True if "reverse" in args else False + if "length" in args: + sorter = self.sort_length + + indices = editor.get_lines_with_cursors() + lines = [editor.get_line(i) for i in indices] + + sorted_lines = sorted(lines, key=sorter, reverse=reverse) + + for i, line in enumerate(sorted_lines): + editor.lines[indices[i]] = line + + +module = { + "class": SortLines, + "name": "sort_lines", +} diff --git a/suplemon/modules/strip.py b/suplemon/modules/strip.py index 75373a4..af16325 100644 --- a/suplemon/modules/strip.py +++ b/suplemon/modules/strip.py @@ -4,7 +4,8 @@ class Strip(Module): - """Strips whitespace from start and end of line.""" + """Trim whitespace from start and end of lines.""" + def run(self, app, editor, args): line_nums = editor.get_lines_with_cursors() for n in line_nums: diff --git a/suplemon/modules/system_clipboard.py b/suplemon/modules/system_clipboard.py index 165a630..5cd41cd 100644 --- a/suplemon/modules/system_clipboard.py +++ b/suplemon/modules/system_clipboard.py @@ -6,14 +6,19 @@ class SystemClipboard(Module): + """Integrates the system clipboard with suplemon.""" + def init(self): self.init_logging(__name__) if self.has_xsel_support(): self.clipboard_type = "xsel" elif self.has_pb_support(): self.clipboard_type = "pb" + elif self.has_xclip_support(): + self.clipboard_type = "xclip" else: - self.logger.warning("Can't use system clipboard. Install 'xsel' or 'pbcopy' for system clipboard support.") + self.logger.warning( + "Can't use system clipboard. Install 'xsel' or 'pbcopy' or 'xclip' for system clipboard support.") return False self.bind_event_before("insert", self.insert) self.bind_event_after("copy", self.copy) @@ -35,6 +40,8 @@ def get_clipboard(self): command = ["xsel", "-b"] elif self.clipboard_type == "pb": command = ["pbpaste", "-Prefer", "txt"] + elif self.clipboard_type == "xclip": + command = ["xclip", "-selection", "clipboard", "-out"] else: return False data = subprocess.check_output(command, universal_newlines=True) @@ -48,6 +55,8 @@ def set_clipboard(self, data): command = ["xsel", "-i", "-b"] elif self.clipboard_type == "pb": command = ["pbcopy"] + elif self.clipboard_type == "xclip": + command = ["xclip", "-selection", "clipboard", "-in"] else: return False p = subprocess.Popen(command, stdin=subprocess.PIPE) @@ -64,6 +73,10 @@ def has_xsel_support(self): output = self.get_output(["xsel", "--version"]) return output + def has_xclip_support(self): + output = self.get_output(["which", "xclip"]) # xclip -version outputs to stderr + return output + def get_output(self, cmd): try: process = subprocess.Popen(cmd, stdout=subprocess.PIPE) diff --git a/suplemon/modules/tabstospaces.py b/suplemon/modules/tabstospaces.py index 68d5229..a775f0c 100644 --- a/suplemon/modules/tabstospaces.py +++ b/suplemon/modules/tabstospaces.py @@ -4,12 +4,12 @@ class TabsToSpaces(Module): + """Convert tab characters to spaces in the entire file.""" + def run(self, app, editor, args): - i = 0 - for line in editor.lines: + for i, line in enumerate(editor.lines): new = line.data.replace("\t", " "*editor.config["tab_width"]) editor.lines[i].set_data(new) - i += 1 module = { diff --git a/suplemon/modules/toggle_whitespace.py b/suplemon/modules/toggle_whitespace.py index fc985f4..30b0173 100644 --- a/suplemon/modules/toggle_whitespace.py +++ b/suplemon/modules/toggle_whitespace.py @@ -4,6 +4,8 @@ class ToggleWhitespace(Module): + """Toggle visually showing whitespace.""" + def run(self, app, editor, args): # Toggle the boolean new_value = not self.app.config["editor"]["show_white_space"] diff --git a/suplemon/modules/upper.py b/suplemon/modules/upper.py index b79f4ab..9706699 100644 --- a/suplemon/modules/upper.py +++ b/suplemon/modules/upper.py @@ -4,6 +4,8 @@ class Upper(Module): + """Transform current lines to upper case.""" + def run(self, app, editor, args): line_nums = [] for cursor in editor.cursors: diff --git a/suplemon/prompt.py b/suplemon/prompt.py index d7a3b5e..d9c9731 100644 --- a/suplemon/prompt.py +++ b/suplemon/prompt.py @@ -12,8 +12,8 @@ class Prompt(Editor): """An input prompt based on the Editor.""" def __init__(self, app, window): Editor.__init__(self, app, window) - self.ready = 0 - self.canceled = 0 + self.ready = False + self.canceled = False self.input_func = lambda: False self.caption = "" @@ -34,14 +34,14 @@ def set_input_source(self, input_func): def on_ready(self): """Accepts the current input.""" - self.ready = 1 + self.ready = True return def on_cancel(self): """Cancels the input prompt.""" self.set_data("") - self.ready = 1 - self.canceled = 1 + self.ready = True + self.canceled = True return def line_offset(self): @@ -129,6 +129,30 @@ def get_input(self, caption="", initial=False): return False +class PromptFiltered(Prompt): + """An input prompt that allows intercepting and filtering input events.""" + + def __init__(self, app, window, handler=None): + Prompt.__init__(self, app, window) + self.prompt_handler = handler + + def handle_input(self, event): + """Handle special bindings for the prompt.""" + # The cancel and accept keys are kept for concistency + if event.key_name in ["ctrl+c", "escape"]: + self.on_cancel() + return False + if event.key_name == "enter": + self.on_ready() + return False + + if self.prompt_handler and self.prompt_handler(self, event): + # If the prompt handler returns True the default action is skipped + return True + + return Editor.handle_input(self, event) + + class PromptAutocmp(Prompt): """An input prompt with basic autocompletion.""" diff --git a/suplemon/ui.py b/suplemon/ui.py index b6e0737..5793985 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -8,7 +8,7 @@ import logging from wcwidth import wcswidth -from .prompt import Prompt, PromptBool, PromptFile, PromptAutocmp +from .prompt import Prompt, PromptBool, PromptFiltered, PromptFile, PromptAutocmp from .key_mappings import key_map # Curses can't be imported yet but we'll @@ -108,6 +108,13 @@ def __init__(self, app): self.logger = logging.getLogger(__name__) self.warned_old_curses = 0 self.limited_colors = True + self.screen = None + self.current_yx = None + self.text_input = None + self.header_win = None + self.status_win = None + self.editor_win = None + self.legend_win = None def init(self): """Set ESC delay and then import curses.""" @@ -502,6 +509,12 @@ def query_bool(self, text, default=False): result = self._query(text, default, PromptBool) return result + def query_filtered(self, text, initial="", handler=None): + """Get an arbitrary string from the user with input filtering.""" + prompt_inst = PromptFiltered(self.app, self.status_win, handler=handler) + result = self._query(text, initial, inst=prompt_inst) + return result + def query_file(self, text, initial=""): """Get a file path from the user.""" result = self._query(text, initial, PromptFile) diff --git a/suplemon/viewer.py b/suplemon/viewer.py index 706608a..565f82d 100644 --- a/suplemon/viewer.py +++ b/suplemon/viewer.py @@ -83,6 +83,9 @@ def __init__(self, app, window): "find_all": self.find_all, # Ctrl + A } + self.pygments_syntax = None # Needs to be implemented in derived classes + self.lexer = None # Needs to be implemented in derived classes + def init(self): pass @@ -101,23 +104,19 @@ def get_size(self): y, x = self.window.getmaxyx() return (x, y) - def get_scroll_pos(self): - return (self.y_scroll, self.x_scroll) - - def get_y_scroll(self): - return self.y_scroll + @property + def scroll_pos(self): + return self.y_scroll, self.x_scroll - def get_x_scroll(self): - return self.x_scroll + @scroll_pos.setter + def scroll_pos(self, pos): + self.y_scroll = pos[0] + self.x_scroll = pos[1] def get_cursor(self): """Return the main cursor.""" return self.cursors[0] - def get_cursors(self): - """Return list of all cursors.""" - return self.cursors - def get_first_cursor(self): """Get the first (primary) cursor.""" highest = None @@ -205,10 +204,6 @@ def set_config(self, config): self.config = config self.set_cursor_style(self.config["cursor_style"]) - def set_scroll_pos(self, pos): - self.y_scroll = pos[0] - self.x_scroll = pos[1] - def set_cursor_style(self, cursor_style): """Set cursor style. @@ -234,6 +229,12 @@ def set_single_cursor(self, cursor): """Discard all cursors and place a new one.""" self.cursors = [Cursor(cursor)] + def setup_linelight(self): + raise NotImplementedError("Needs to be implemented in derived classes") + + def setup_highlight(self): + raise NotImplementedError("Needs to be implemented in derived classes") + def set_file_extension(self, ext): """Set the file extension.""" ext = ext.lower() @@ -247,14 +248,6 @@ def add_cursor(self, cursor): """Add a new cursor. Accepts a x,y tuple or a Cursor instance.""" self.cursors.append(Cursor(cursor)) - def pad_lnum(self, n): - """Pad line number with zeroes.""" - # TODO: move to helpers - s = str(n) - while len(s) < self.line_offset()-1: - s = "0" + s - return s - def max_line_length(self): """Get maximum line length that fits in the editor.""" return self.get_size()[0]-self.line_offset()-1 @@ -312,11 +305,10 @@ def render(self): return self.window.erase() - i = 0 max_y = self.get_size()[1] max_len = self.max_line_length() # Iterate through visible lines - while i < max_y: + for i in range(max_y): x_offset = self.line_offset() lnum = i + self.y_scroll if lnum >= len(self.lines): # Make sure we have a line to show @@ -325,7 +317,8 @@ def render(self): line = self.lines[lnum] if self.config["show_line_nums"]: curs_color = curses.color_pair(line.number_color) - self.window.addstr(i, 0, self.pad_lnum(lnum+1)+" ", curs_color) + padded_num = str(lnum+1).zfill(self.line_offset()-1) + self.window.addstr(i, 0, padded_num+" ", curs_color) pos = (x_offset, i) try: @@ -333,7 +326,6 @@ def render(self): except: self.logger.error("Failed rendering line #{0} @{1} DATA:'{2}'!".format(lnum+1, pos, line), exc_info=True) - i += 1 self.render_cursors() def render_line_contents(self, line, pos, x_offset, max_len): @@ -409,6 +401,9 @@ def render_line_pygments(self, line, pos, x_offset, max_len): first_token = False x_offset += len(text) + def get_line_color(self, line): + raise NotImplementedError("Needs to be implemented in derived classes") + def render_line_linelight(self, line, pos, x_offset, max_len): """Render line with naive line based highlighting.""" y = pos[1] @@ -805,6 +800,9 @@ def find_query(self): if what: self.find(what) + def store_action_state(self, state): + raise NotImplementedError("Needs to be implemented in derived classes") + def find(self, what, findall=False): """Find what in data (from top to bottom). Adds a cursor when found.""" # Sorry for this colossal function