Skip to content

Instantly share code, notes, and snippets.

@OdatNurd
Created March 2, 2018 06:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save OdatNurd/0a0fcc033bc2fd6c7946eb14a8d77fe7 to your computer and use it in GitHub Desktop.
Save OdatNurd/0a0fcc033bc2fd6c7946eb14a8d77fe7 to your computer and use it in GitHub Desktop.
The exec.py plugin from Sublime Text 2.0.2 modified to use incremental decoding
import sublime, sublime_plugin
import os, sys, codecs
import thread
import subprocess
import functools
import time
# This is the exec.py file taken from the Default package in Sublime Text 2.0.2
# which has been modified to use incremental decoding of build system output
# the way it's done in Sublime Text 3.
#
# The code is Copyright (c) Sublime HQ Pty Inc and provided here to assist those
# still using Sublime Text 2, which is no longer actively supported.
class ProcessListener(object):
def on_data(self, proc, data):
pass
def on_finished(self, proc):
pass
# Encapsulates subprocess.Popen, forwarding stdout to a supplied
# ProcessListener (on a separate thread)
class AsyncProcess(object):
def __init__(self, arg_list, env, listener,
# "path" is an option in build systems
path="",
# "shell" is an options in build systems
shell=False):
self.listener = listener
self.killed = False
self.start_time = time.time()
# Hide the console window on Windows
startupinfo = None
if os.name == "nt":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
# Set temporary PATH to locate executable in arg_list
if path:
old_path = os.environ["PATH"]
# The user decides in the build system whether he wants to append $PATH
# or tuck it at the front: "$PATH;C:\\new\\path", "C:\\new\\path;$PATH"
os.environ["PATH"] = os.path.expandvars(path).encode(sys.getfilesystemencoding())
proc_env = os.environ.copy()
proc_env.update(env)
for k, v in proc_env.iteritems():
proc_env[k] = os.path.expandvars(v).encode(sys.getfilesystemencoding())
self.proc = subprocess.Popen(arg_list, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, startupinfo=startupinfo, env=proc_env, shell=shell)
if path:
os.environ["PATH"] = old_path
if self.proc.stdout:
thread.start_new_thread(self.read_stdout, ())
if self.proc.stderr:
thread.start_new_thread(self.read_stderr, ())
def kill(self):
if not self.killed:
self.killed = True
self.proc.terminate()
self.listener = None
def poll(self):
return self.proc.poll() == None
def exit_code(self):
return self.proc.poll()
def read_stdout(self):
decoder_cls = codecs.getincrementaldecoder(self.listener.encoding)
decoder = decoder_cls('replace')
while True:
data = decoder.decode(os.read(self.proc.stdout.fileno(), 2**15))
if data != "":
if self.listener:
self.listener.on_data(self, data)
else:
self.proc.stdout.close()
if self.listener:
self.listener.on_finished(self)
break
def read_stderr(self):
decoder_cls = codecs.getincrementaldecoder(self.listener.encoding)
decoder = decoder_cls('replace')
while True:
data = decoder.decode(os.read(self.proc.stderr.fileno(), 2**15))
if data != "":
if self.listener:
self.listener.on_data(self, data)
else:
self.proc.stderr.close()
break
class ExecCommand(sublime_plugin.WindowCommand, ProcessListener):
def run(self, cmd = [], file_regex = "", line_regex = "", working_dir = "",
encoding = "utf-8", env = {}, quiet = False, kill = False,
# Catches "path" and "shell"
**kwargs):
if kill:
if self.proc:
self.proc.kill()
self.proc = None
self.append_data(None, "[Cancelled]")
return
if not hasattr(self, 'output_view'):
# Try not to call get_output_panel until the regexes are assigned
self.output_view = self.window.get_output_panel("exec")
# Default the to the current files directory if no working directory was given
if (working_dir == "" and self.window.active_view()
and self.window.active_view().file_name()):
working_dir = os.path.dirname(self.window.active_view().file_name())
self.output_view.settings().set("result_file_regex", file_regex)
self.output_view.settings().set("result_line_regex", line_regex)
self.output_view.settings().set("result_base_dir", working_dir)
# Call get_output_panel a second time after assigning the above
# settings, so that it'll be picked up as a result buffer
self.window.get_output_panel("exec")
self.encoding = encoding
self.quiet = quiet
self.proc = None
if not self.quiet:
print "Running " + " ".join(cmd)
sublime.status_message("Building")
show_panel_on_build = sublime.load_settings("Preferences.sublime-settings").get("show_panel_on_build", True)
if show_panel_on_build:
self.window.run_command("show_panel", {"panel": "output.exec"})
merged_env = env.copy()
if self.window.active_view():
user_env = self.window.active_view().settings().get('build_env')
if user_env:
merged_env.update(user_env)
# Change to the working dir, rather than spawning the process with it,
# so that emitted working dir relative path names make sense
if working_dir != "":
os.chdir(working_dir)
err_type = OSError
if os.name == "nt":
err_type = WindowsError
try:
# Forward kwargs to AsyncProcess
self.proc = AsyncProcess(cmd, merged_env, self, **kwargs)
except err_type as e:
self.append_data(None, str(e) + "\n")
self.append_data(None, "[cmd: " + str(cmd) + "]\n")
self.append_data(None, "[dir: " + str(os.getcwdu()) + "]\n")
if "PATH" in merged_env:
self.append_data(None, "[path: " + str(merged_env["PATH"]) + "]\n")
else:
self.append_data(None, "[path: " + str(os.environ["PATH"]) + "]\n")
if not self.quiet:
self.append_data(None, "[Finished]")
def is_enabled(self, kill = False):
if kill:
return hasattr(self, 'proc') and self.proc and self.proc.poll()
else:
return True
def append_data(self, proc, data):
if proc != self.proc:
# a second call to exec has been made before the first one
# finished, ignore it instead of intermingling the output.
if proc:
proc.kill()
return
str = data
# Normalize newlines, Sublime Text always uses a single \n separator
# in memory.
str = str.replace('\r\n', '\n').replace('\r', '\n')
selection_was_at_end = (len(self.output_view.sel()) == 1
and self.output_view.sel()[0]
== sublime.Region(self.output_view.size()))
self.output_view.set_read_only(False)
edit = self.output_view.begin_edit()
self.output_view.insert(edit, self.output_view.size(), str)
if selection_was_at_end:
self.output_view.show(self.output_view.size())
self.output_view.end_edit(edit)
self.output_view.set_read_only(True)
def finish(self, proc):
if not self.quiet:
elapsed = time.time() - proc.start_time
exit_code = proc.exit_code()
if exit_code == 0 or exit_code == None:
self.append_data(proc, ("[Finished in %.1fs]") % (elapsed))
else:
self.append_data(proc, ("[Finished in %.1fs with exit code %d]") % (elapsed, exit_code))
if proc != self.proc:
return
errs = self.output_view.find_all_results()
if len(errs) == 0:
sublime.status_message("Build finished")
else:
sublime.status_message(("Build finished with %d errors") % len(errs))
# Set the selection to the start, so that next_result will work as expected
edit = self.output_view.begin_edit()
self.output_view.sel().clear()
self.output_view.sel().add(sublime.Region(0))
self.output_view.end_edit(edit)
def on_data(self, proc, data):
sublime.set_timeout(functools.partial(self.append_data, proc, data), 0)
def on_finished(self, proc):
sublime.set_timeout(functools.partial(self.finish, proc), 0)
@OdatNurd
Copy link
Author

OdatNurd commented Mar 2, 2018

The first revision of the file is the default version that ships with Sublime Text 2.0.2, which decodes the data as it's received by the exec command for display in the command buffer . This seems to cause issues with decoding of multi byte UTF-8 data in certain circumstances, I presume due to potential unfortunate partially delivered data.

The version in the second revision is modified to use an incremental decoder, and decoding is done at the point where the data is read from the pipe instead of at the point where it's being received by the exec command.

This is how Sublime Text 3 performs this task (with a myriad of other enhancements).

This is two revisions on purpose so that you can see the diff to easily see the changes made.

@OdatNurd
Copy link
Author

OdatNurd commented Mar 2, 2018

To use this, back up the Default\exec.py file in the Packages folder (use Preferences > Browse Packages... from the menu to find that) and then replace the installed version with the version outlined here. To revert to the original version, put your backup version back in or retrieve the version from the Default.sublime-package in the Pristine Packages folder.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment