This could also work

Sometimes the eval is not that evil

Sublime is the text editor that is almost always running on my machine. A long time ago, it was the only editor I wrote code in. Nowadays, it's mostly some untitled files with messy notes, code snippets, half-written messages, or anything text-related.

I like to use it to transform text. For example, if I have a couple of lines and want to put every line between quotes or sort some lines alphabetically and concatenate the rows into one single comma-separated string, things like that. I even tend to copy text from my actual IDE, paste it into Sublime, transform it, and copy it back.

Of course, you could say that Vim (or Emacs) can do such things faster and more efficiently and be capable of many other things. But for me, the mouse-driven multi-cursor workflow fits better than Vim-magic. I know, I'm a weirdo.

Sometimes I run into problems that cannot be easily solved by Sublime. Situations where a quiet voice in the back of your head tells you that you should write a small script or at least open the REPL of your favorite scripting language.

Let's look at a simple example. You want to generate the list of numbers from 1 to 10, every number in its own line. Of course, the local Vim-magician would tell you that it's just a quick i 1 Esc q a Y p Ctrl+a q 8 @ a and you are done, but for me, this does not look like something that would come up in a real-world scenario.[1]

Luckily Sublime supports plugins, and even more lucky that they choose a proper scripting language for it. There is nothing worse than when you like a program, you want to create plugins for it, and it turns out that you must use Perl.[2]

Running the selection

My first idea was that I could create a command that runs the selected text as Python code and replaces it with the result:[3]

eval.py
import sublime_plugin

class EvalSelectionsCommand(sublime_plugin.TextCommand):
  def run(self, edit):
    for region in self.view.sel():
      if region.empty():
        continue

      code = self.view.substr(region)
      result = str(eval(code))
      self.view.replace(edit, region, result)

Interestingly enough, this was the first time I had to use eval() in Python. Soon enough, I also learned that we were all of us deceived, for another eval() was made... called exec(). Yes, my Precious, the first one just evaluates expressions. Only the second one can be used to execute any kind of Python code. Sneaky little hobbitses. But let's not go that far ahead. First, we need to somehow run this command.

eval.sublime-command
[
  { "caption": "Run selection", "command": "eval_selections" }
]

Now we can select, for example, the '\n'.join(map(str, range(1, 11))) text, hit a Ctrl+Shift+p, search for the command and we are done. Somehow, it felt less comfortable than I had hoped for, but it could be capable of many things compared to the simplicity of the code.

Transforming the selection

Now, let's look at a different approach. What if the selection does not contain the code, but the code is working on the selections?

eval.py
import sublime_plugin

class EvalSelectionsCommand(sublime_plugin.TextCommand):
  def run(self, edit, text):
    for idx, region in enumerate(self.view.sel()):
      data = self.view.substr(region)
      result = str(eval(text, {'d': data, 'i': idx}, {}))
      self.view.replace(edit, region, result)

  def input(self, args):
    return sublime_plugin.TextInputHandler()

The eval() got a little bit more complex. We pass a couple of variables we can use later in our expression. This way, we could have ten empty rows, an empty selection in every row, run the command and write i+1 as the expression. Or, if the selections are not empty, we could write f'{i+1}. {d}' to prefix them with numbers.

A later iteration of this idea. The d variable name was changed to _, with a preview based on the first selection.

More clever execution

We can now jump back to the exec(). If we have the following small code snippet selected and try to run it...

for i in range(10):
  print(i+1)

...our first implementation would throw a SyntaxError: invalid syntax exception. So we need to make it a little smarter:

eval.py
import sublime_plugin
import traceback

from io import StringIO
from contextlib import redirect_stdout

class EvalSelectionsCommand(sublime_plugin.TextCommand):
  def run(self, edit):
    for region in self.view.sel():
      if region.empty():
        continue

      code = self.view.substr(region)
      self.view.replace(edit, region, self.__run_code(code))

  def __run_code(self, code):
    try:
      return str(eval(code))
    except Exception as e:
      try:
        return self.__exec_code(code)
      except Exception as e:
        return ''.join(traceback.format_exception(type(e), e, e.__traceback__))

  def __exec_code(self, code):
    f = StringIO()
    with redirect_stdout(f):
      exec(code)
    return f.getvalue()

We will try to run it first with eval(), and if that fails, we use exec(). It's a little tricky. If we didn't redirect the standard output, it would end up in the console of Sublime, and we wouldn't get the desired result.

As an added bonus, we also have better error handling in this one, so we don't have to check the console if something is wrong with the code we want to run. The code will be replaced with the error, but we can quickly get it back with Ctrl+z, of course.

A wealth of possibilities

There are many ways we could improve this little plugin-wannabe. Just a couple of ideas:

  • the two mentioned approaches could be two different commands, different tools for different problems
  • we can add aliases, shortcuts, and helper functions for often used functionalities to eval() or exec so we can use them in our code snippet
  • output could be displayed in a separate window

But our broadcast time is coming to its end, so I have no other choice than leave the further investigation of the topic as an exercise for the reader.


Notes

1. There are more straightforward, shorter solutions, but this one felt the most Vim-like for me. More details here.

2. Just a childhood trauma. Don't really worry about it. A long-long time ago, I really liked the Irssi IRC client, but my brain couldn't handle Perl, so I wasn't able to write scripts for it.

3. Naturally, there are existing plugins for this, but where is the fun in that?

Ez a bejegyzés magyar nyelven is elérhető: Ez is egy megoldás

Have a comment?

Send an email to the blog at deadlime dot hu address.

Want to subscribe?

We have a good old fashioned RSS feed if you're into that.