4

I am trying to adapt a plugin for automated text replacement in a Sublime Text 3 Plugin. What I want it to do is paste in text from the clipboard and make some automatic text substitutions

import sublime
import sublime_plugin
import re

class PasteAndEscapeCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        # Position of cursor for all selections
        before_selections = [sel for sel in self.view.sel()]
        # Paste from clipboard
        self.view.run_command('paste')
        # Postion of cursor for all selections after paste
        after_selections = [sel for sel in self.view.sel()]
        # Define a new region based on pre and post paste cursor positions
        new_selections = list()
        delta = 0
        for before, after in zip(before_selections, after_selections):
            new = sublime.Region(before.begin() + delta, after.end()) 
            delta = after.end() - before.end()
            new_selections.append(new)
        # Clear any existing selections
        self.view.sel().clear()
        # Select the saved region
        self.view.sel().add_all(new_selections)
        # Replace text accordingly
        for region in self.view.sel():
            # Get the text from the selected region
            text = self.view.substr(region)
            # Make the required edits on the text
            text = text.replace("\\","\\\\")
            text = text.replace("_","\\_")
            text = text.replace("*","\\*")
            # Paste the text back to the saved region
            self.view.replace(edit, region, text)
        # Clear selections and set cursor position
        self.view.sel().clear()
        self.view.sel().add_all(after_selections)

This works for the most part except I need to get the new region for the edited text. The cursor will be placed to the location of the end of the pasted text. However since I am making replacements which always make the text larger the final position will be inaccurate.

I know very little about Python for Sublime and like most others this is my first plugin.

How do I set the cursor position to account for the size changes in the text. I know I need to do something with the after_selections list as I am not sure how to create new regions as they were created from selections which are cleared in an earlier step.

I feel that I am getting close with

# Add the updated region to the selection
self.view.sel().subtract(region)
self.view.sel().add(sublime.Region(region.begin()+len(text)))

This, for some yet unknown to me reason, places the cursor at the beginning and end of the replaced text. A guess would be that I am removing the regions one by one but forgetting some "initial" region that also exists.

Note

I am pretty sure the double loop in the code in the question here is redundant. but that is outside the scope of the question.

Matt
  • 45,022
  • 8
  • 78
  • 119
  • I am guessing the python tag here might not apply as it might not be considered true python but you guys will know for sure. – Matt Jan 16 '18 at 16:28
  • A view's selection (`view.sel()`) is a list of regions, where each region is a cursor, which is why adding the second one makes a second one adds a second cursor. I'd try it with `view.sel().clear()` instead of the `subtract` and see what that gets you. – OdatNurd Jan 16 '18 at 19:08
  • @OdatNurd I can't add a clear in the loop as I would need to maintain the list of multiple selections, not just the last one. That is why I thought subtract would be a good idea. Subtract might not have been doing what I thought it was. I had assumed that the initial cursor was removed but it looked like it was there in addition to other `sel()`s. I still ended up doing a clear after I collected all the regions I wanted in the same way it would have been done for the paste. Likely I could combine the two as one loop but I dont do this enough to know for sure. – Matt Jan 16 '18 at 19:19

2 Answers2

2

I think your own answer to your question is a good one and probably the way I would go if I was to do something like this in this manner.

In particular, since the plugin is modifying the text on the fly and making it longer, the first way that immediately presents itself as a solution other than what your own answer is doing would be to track the length change of the text after the replacements so you can adjust the selections accordingly.

Since I can't really provide a better answer to your question than the one you already came up with, here's an alternative solution to this instead:

import sublime
import sublime_plugin

class PasteAndEscapeCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        org_text = sublime.get_clipboard()
        text = org_text.replace("\\","\\\\")
        text = text.replace("_","\\_")
        text = text.replace("*","\\*")

        sublime.set_clipboard(text)
        self.view.run_command("paste")

        sublime.set_clipboard(org_text)

This modifies the text on the clipboard to be quoted the way you want it to be quoted so that it can just use the built in paste command to perform the paste.

The last part puts the original clipboard text back on the clipboard, which for your purposes may or may not be needed.

OdatNurd
  • 21,371
  • 3
  • 50
  • 68
  • 1
    Thanks. I just saw that there are methods for interacting with the clipboard on the API pages and came back here to leave a comment. I don't need to paste at all if I do it right. Next all I have to do is figure out how to pass key pairs as args so that the plugin can be dynamic! [It's my first day](https://www.youtube.com/watch?v=9ZlOhSt_qW0) – Matt Jan 16 '18 at 19:54
  • I'd say you're doing pretty well so far. :) – OdatNurd Jan 16 '18 at 19:58
1

So, one approach for this would be to make new regions as the replaced text is created using their respective lengths as starting positions. Then once the loop is complete clear all existing selections and set the new one we created in the replacement loop.

# Replace text accordingly
new_replacedselections = list()
for region in self.view.sel():
    # Get the text from the selected region
    text = self.view.substr(region)
    # Make the required edits on the text
    text = text.replace("\\","\\\\") # Double up slashes
    text = text.replace("*","\\*")   # Escape * 
    text = text.replace("_","\\_")   # Escape _ 
    # Paste the text back to the saved region
    self.view.replace(edit, region, text)
    # Add the updated region to the collection
    new_replacedselections.append(sublime.Region(region.begin()+len(text)))

# Set the selection positions after the new insertions.
self.view.sel().clear()
self.view.sel().add_all(new_replacedselections)
Matt
  • 45,022
  • 8
  • 78
  • 119