import sublime, sublime_plugin def expand_to_line(view, region): """ As view.full_line, but doesn't expand to the next line if a full line is already selected """ if not (region.a == region.b) and view.substr(region.end() - 1) == '\n': return sublime.Region(view.line(region).begin(), region.end()) else: return view.full_line(region) def extract_line_blocks(view): blocks = [expand_to_line(view, s) for s in view.sel()] if len(blocks) == 0: return blocks # merge any adjacent blocks merged_blocks = [blocks[0]] for block in blocks[1:]: last_block = merged_blocks[-1] if block.begin() <= last_block.end(): merged_blocks[-1] = sublime.Region(last_block.begin(), block.end()) else: merged_blocks.append(block) return merged_blocks class SwapLineUpCommand(sublime_plugin.TextCommand): def run(self, edit): blocks = extract_line_blocks(self.view) # No selection if len(blocks) == 0: return # Already at BOF if blocks[0].begin() == 0: return # Add a trailing newline if required, the logic is simpler if every line # ends with a newline add_trailing_newline = (self.view.substr(self.view.size() - 1) != '\n') and blocks[-1].b == self.view.size() if add_trailing_newline: # The insert can cause the selection to move. This isn't wanted, so # reset the selection if it has moved to EOF sel = [r for r in self.view.sel()] self.view.insert(edit, self.view.size(), '\n') if self.view.sel()[-1].end() == self.view.size(): # Selection has moved, restore the previous selection self.view.sel().clear() for r in sel: self.view.sel().add(r) # Fix up any block that should now include this newline blocks[-1] = sublime.Region(blocks[-1].a, blocks[-1].b + 1) # Process in reverse order blocks.reverse() for b in blocks: prev_line = self.view.full_line(b.begin() - 1) self.view.insert(edit, b.end(), self.view.substr(prev_line)) self.view.erase(edit, prev_line) if add_trailing_newline: # Remove the added newline self.view.erase(edit, sublime.Region(self.view.size() - 1, self.view.size())) # Ensure the selection is visible self.view.show(self.view.sel(), False) class SwapLineDownCommand(sublime_plugin.TextCommand): def run(self, edit): blocks = extract_line_blocks(self.view) # No selection if len(blocks) == 0: return # Already at EOF if blocks[-1].end() == self.view.size(): return # Add a trailing newline if required, the logic is simpler if every line # ends with a newline add_trailing_newline = (self.view.substr(self.view.size() - 1) != '\n') if add_trailing_newline: # No block can be at EOF (checked above), so no need to fix up the # blocks self.view.insert(edit, self.view.size(), '\n') # Process in reverse order blocks.reverse() for b in blocks: next_line = self.view.full_line(b.end()) contents = self.view.substr(next_line) self.view.erase(edit, next_line) self.view.insert(edit, b.begin(), contents) if add_trailing_newline: # Remove the added newline self.view.erase(edit, sublime.Region(self.view.size() - 1, self.view.size())) # Ensure the selection is visible self.view.show(self.view.sel(), False)