def act(context, actionObject, operation='entab'): def replacements(match): '''Utility function for replacing items''' return match.group(0).replace(search, replace) spaces = int(actionObject.userInput().stringValue()) if operation == 'entab': target = re.compile(r'^(\t* +\t*)+', re.MULTILINE) search = ' ' * spaces replace = '\t' else: target = re.compile(r'^( *\t+ *)+', re.MULTILINE) search = '\t' replace = ' ' * spaces insertions = tea.new_recipe() ranges = tea.get_ranges(context) if len(ranges) == 1 and ranges[0].length == 0: # No selection, use the document ranges[0] = tea.new_range(0, context.string().length()) for range in ranges: text = tea.get_selection(context, range) # Non-Unix line endings will bork things; convert them text = tea.unix_line_endings(text) text = re.sub(target, replacements, text) if tea.get_line_ending(context) != '\n': text = tea.clean_line_endings(context, text) insertions.addReplacementString_forRange_(text, range) insertions.setUndoActionName_(operation.title()) context.applyTextRecipe_(insertions) return True
def act(context, default=None, undo_name=None, **syntaxes): ''' Required action method default parameter is not a snippet, but should contain the $SELECTED_TEXT placeholder ''' # Get the selected ranges ranges = tea.get_ranges(context) if len(ranges) is 1: # Since we've only got one selection we can use a snippet range = ranges[0] insertion = tea.select_from_zones(context, range, default, **syntaxes) # Make sure the range is actually a selection if range.length > 0: text = tea.get_selection(context, range) snippet = '${1:' + insertion.replace('$SELECTED_TEXT', '${2:$SELECTED_TEXT}') + '}$0' else: # Not a selection, just wrap the cursor text = '' snippet = insertion.replace('$SELECTED_TEXT', '$1') + '$0' snippet = tea.construct_snippet(text, snippet) return tea.insert_snippet_over_range(context, snippet, range, undo_name) # Since we're here, it must not have been a single selection insertions = tea.new_recipe() for range in ranges: insertion = tea.select_from_zones(context, range, default, **syntaxes) text = tea.get_selection(context, range) text = insertion.replace('$SELECTED_TEXT', text) insertions.addReplacementString_forRange_(text, range) if undo_name is not None: insertions.setUndoActionName_(undo_name) return context.applyTextRecipe_(insertions)
def act(context, default=None, prefix_selection=False, suffix_selection=False, undo_name=None, **syntaxes): ''' Required action method Inserts arbitrary text over all selections; specific text can be syntax-specific (same procedure as Wrap Selection In Link) If you set prefix_selection to true, the inserted text will precede any selected text; if suffix_selection is true it will follow any selected text; if both are true it will wrap the text ''' # Grab the ranges ranges = tea.get_ranges(context) # Set up our text recipe insertions = tea.new_recipe() for range in ranges: if prefix_selection or suffix_selection: # Get the selected text text = tea.get_selection(context, range) if prefix_selection: text = '$INSERT' + text if suffix_selection: text += '$INSERT' # If empty selection, only insert one if text == '$INSERT$INSERT': text = '$INSERT' else: text = '$INSERT' # Check for zone-specific insertion insert = tea.select_from_zones(context, range, default, **syntaxes) text = text.replace('$INSERT', insert) text = text.replace('$TOUCH', '') # Insert the text, or replace the selected text if range.length is 0: insertions.addInsertedString_forIndex_(text, range.location) else: insertions.addReplacementString_forRange_(text, range) # Set undo name and run the recipe if undo_name != None: insertions.setUndoActionName_(undo_name) reset_cursor = False if len(ranges) is 1 and ranges[0].length is 0: # Thanks to addInsertedString's wonkiness, we have to reset the cursor reset_cursor = True # Espresso beeps if I return True or False; hence this weirdness return_val = context.applyTextRecipe_(insertions) if reset_cursor: new_range = tea.new_range(ranges[0].location + len(text), 0) tea.set_selected_range(context, new_range) return return_val
def act(context, input=None, alternate=None, trim='both', respect_indent=False, undo_name=None): ''' Required action method input dictates what should be trimmed: - None (default): falls back to alternate - selection: ignores lines if they exist, just trims selection - selected_lines: each line in the selection alternate dictates what to fall back on - None (default): will do nothing if input is blank - line: will trim the line the caret is on - all_lines: all lines in the document trim dictates what part of the text should be trimmed: - both (default) - start - end If respect_indent is True, indent characters (as defined in preferences) at the beginning of the line will be left untouched. ''' # Since input is always a selection of some kind, check if we have one ranges = tea.get_ranges(context) insertions = tea.new_recipe() if (len(ranges) == 1 and ranges[0].length == 0) or input is None: if alternate == 'line': text, range = tea.get_line(context, ranges[0]) text = tea.trim(context, text, False, trim, respect_indent) elif alternate == 'all_lines': range = tea.new_range(0, context.string().length()) text = tea.get_selection(context, range) text = tea.trim(context, text, True, trim, respect_indent) insertions.addReplacementString_forRange_(text, range) else: if input == 'selected_lines': parse_lines = True else: parse_lines = False for range in ranges: text = tea.get_selection(context, range) text = tea.trim(context, text, parse_lines, trim, respect_indent) insertions.addReplacementString_forRange_(text, range) if undo_name != None: insertions.setUndoActionName_(undo_name) return context.applyTextRecipe_(insertions)
def act(context, type="named", wrap="$HEX", undo_name=None): """ Required action method Type can be: named: named HTML entities, with high value numeric entities if no name numeric: numeric HTML entities hex: hexadecimal encoding; use the 'wrap' option for specific output Wrap will be used if type is 'hex' and will replace $HEX with the actual hex value. For example '\u$HEX' will be result in something like '\u0022' """ ranges = tea.get_ranges(context) if len(ranges) == 1 and ranges[0].length == 0: # We've got one empty range; make sure it's not at the # beginning of the document if ranges[0].location > 0: # Set the new target range to the character before the cursor ranges[0] = tea.new_range(ranges[0].location - 1, 1) else: return False # Since we're here we've got something to work with insertions = tea.new_recipe() for range in ranges: text = tea.get_selection(context, range) if type == "named": # Convert any characters we can into named HTML entities text = tea.named_entities(text) elif type == "numeric": # Convert any characters we can into numeric HTML entities text = tea.numeric_entities(text, type) elif type == "hex": # Convert characters to hex via numeric entities text = tea.numeric_entities(text) text = tea.entities_to_hex(text, wrap) insertions.addReplacementString_forRange_(text, range) if undo_name is not None: insertions.setUndoActionName_(undo_name) return context.applyTextRecipe_(insertions)
def didEndSheet_returnCode_contextInfo_(self, sheet, code, info): def replacements(match): '''Utility function for replacing items''' return match.group(0).replace(self.search, self.replace) if code == 1: # Leave sheet open with "processing" spinner self.spinner.startAnimation_(self) spaces = int(self.numSpaces.stringValue()) if self.action == 'entab': target = re.compile(r'^(\t* +\t*)+', re.MULTILINE) self.search = ' ' * spaces self.replace = '\t' else: target = re.compile(r'^( *\t+ *)+', re.MULTILINE) self.search = '\t' self.replace = ' ' * spaces insertions = tea.new_recipe() ranges = tea.get_ranges(self.context) if len(ranges) == 1 and ranges[0].length == 0: # No selection, use the document ranges[0] = tea.new_range(0, self.context.string().length()) for range in ranges: text = tea.get_selection(self.context, range) # Non-Unix line endings will bork things; convert them text = tea.unix_line_endings(text) text = re.sub(target, replacements, text) if tea.get_line_ending(self.context) != '\n': text = tea.clean_line_endings(self.context, text) insertions.addReplacementString_forRange_(text, range) insertions.setUndoActionName_(self.action.title()) self.context.applyTextRecipe_(insertions) self.spinner.stopAnimation_(self) sheet.orderOut_(self)
def act(context, direction=None, remove_duplicates=False, undo_name=None): """ Required action method This sorts the selected lines (or document, if no selection) either ascending, descending, or randomly. """ # Check if there is a selection, otherwise take all lines ranges = tea.get_ranges(context) if len(ranges) == 1 and ranges[0].length == 0: ranges = [tea.new_range(0, context.string().length())] # Setup the text recipe recipe = tea.new_recipe() for range in ranges: text = tea.get_selection(context, range) # A blank range means we have only one range and it's empty # so we can't do any sorting if text == "": return False # Split the text into lines, not maintaining the linebreaks lines = text.splitlines(False) # Remove duplicates if set if remove_duplicates: if direction is None: seen = {} result = [] for x in lines: if x in seen: continue seen[x] = 1 result.append(x) lines = result else: lines = list(set(lines)) # Sort lines ascending or descending if direction == "asc" or direction == "desc": lines.sort() if direction == "desc": lines.reverse() # If direction is random, shuffle lines if direction == "random": random.shuffle(lines) # Join lines to one string linebreak = tea.get_line_ending(context) sortedText = unicode.join(linebreak, lines) # Add final linebreak if selected text has one if text.endswith(linebreak): sortedText += linebreak # Insert the text recipe.addReplacementString_forRange_(sortedText, range) if undo_name is not None: recipe.setUndoActionName_(undo_name) # Apply the recipe return context.applyTextRecipe_(recipe)
def performActionWithContext_error_(self, context): ''' Gathers the necessary info, populates the environment, and runs the script ''' def execute(file, input): '''Utility function for running the script''' script = subprocess.Popen( [file], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) return script.communicate(str(input)) if self.script is None: tea.log('No script found') return False # Environment variables that won't change with repetition os.putenv('E_SUGARPATH', self.bundle_path) filepath = context.documentContext().fileURL() if filepath is not None: os.putenv('E_FILENAME', filepath.path().lastPathComponent()) if filepath.isFileURL(): os.putenv( 'E_DIRECTORY', filepath.path().stringByDeletingLastPathComponent() ) os.putenv('E_FILEPATH', filepath.path()) root = tea.get_root_zone(context) if root is False: root = '' os.putenv('E_ROOT_ZONE', root) # Set up the preferences prefs = tea.get_prefs(context) os.putenv('E_SOFT_TABS', str(prefs.insertsSpacesForTab())) os.putenv('E_TAB_SIZE', str(prefs.numberOfSpacesForTab())) os.putenv('E_LINE_ENDING', prefs.lineEndingString()) os.putenv('E_XHTML', tea.get_tag_closestring(context)) # Set up the user-defined shell variables defaults = NSUserDefaults.standardUserDefaults() for item in defaults.arrayForKey_('TEAShellVariables'): if 'variable' in item and item['variable'] != '': os.putenv(item['variable'], item['value']) # Initialize our common variables recipe = tea.new_recipe() ranges = tea.get_ranges(context) # Check the user script folder for overrides file = os.path.join(os.path.expanduser( '~/Library/Application Support/Espresso/TEA/Scripts/' ), self.script) if not os.path.exists(file): file = os.path.join(self.bundle_path, 'TEA', self.script) if not os.path.exists(file): # File doesn't exist in the bundle, either, so something is screwy return tea.say( context, 'Error: could not find script', 'TEA could not find the script associated with this action. '\ 'Please contact the Sugar developer, or make sure it is '\ 'installed here:\n\n'\ '~/Library/Application Support/Espresso/TEA/Scripts' ) # There's always at least one range; this thus supports multiple # discontinuous selections for range in ranges: # These environment variables may change with repetition, so reset os.putenv('E_SELECTED_TEXT', str(context.string().substringWithRange_(range)) ) word, wordrange = tea.get_word(context, range) os.putenv('E_CURRENT_WORD', str(word)) os.putenv('E_CURRENT_LINE', str(context.string().substringWithRange_( context.lineStorage().lineRangeForRange_(range) )) ) os.putenv( 'E_LINENUMBER', str(context.lineStorage().lineNumberForIndex_(range.location)) ) os.putenv('E_LINEINDEX', str( range.location - \ context.lineStorage().lineStartIndexForIndex_lineNumber_( range.location, None ) )) active = tea.get_active_zone(context, range) if active is False: active = '' os.putenv('E_ACTIVE_ZONE', str(active)) # Setup STDIN and track the source source = 'input' if self.input == 'selection': input = tea.get_selection(context, range) if input == '': if self.alt == 'document': input = context.string() elif self.alt == 'line': input, range = tea.get_line(context, range) # For this usage, we don't want to pass the final linebreak input = input[:-1] range = tea.new_range(range.location, range.length-1) elif self.alt == 'word': input, range = tea.get_word(context, range) elif self.alt == 'character': input, range = tea.get_character(context, range) source = 'alt' elif self.input == 'document': input = context.string() else: input = '' # Run the script try: output, error = execute(file, input) except: # Most likely cause of failure is lack of executable status try: os.chmod(file, 0755) output, error = execute(file, input) except: # Failed to execute completely, so exit with error return tea.say( context, 'Error: cannot execute script', 'Error: could not execute the script. Please contact '\ 'the Sugar author.' ) # Log errors if error: tea.log(str(error)) # Process the output output = output.decode('utf-8') if self.output == 'document' or \ (source == 'alt' and self.alt == 'document'): docrange = tea.new_range(0, context.string().length()) recipe.addReplacementString_forRange_(output, docrange) break elif self.output == 'text': recipe.addReplacementString_forRange_(output, range) elif self.output == 'snippet': recipe.addDeletedRange_(range) break # If no output, we don't need to go any further if self.output is None: return True # Made it here, so apply the recipe and return if self.undo is not None: recipe.setUndoActionName_(self.undo) recipe.prepare() if recipe.numberOfChanges() > 0: response = context.applyTextRecipe_(recipe) else: response = True if self.output == 'snippet': response = tea.insert_snippet(context, output) return response