Sublime Forum

Help with some plugin development questions

#1

Hi there,

Code is here: github.com/tanepiper/SublimeText-Nodejs

I’m working on a plugin where I have commands to run nodejs.

Here is an example of an command class:

[code]class NodeDrunCommand(NodeTextCommand):
def run(self, edit):
command = ‘node’, ‘debug’, self.view.file_name()]
open_url(‘http://127.0.0.1:5858’)
self.run_command(command, self.command_done)

def command_done(self, result):
self.scratch(result, title=“Node Output”, syntax=“Packages/JavaScript/JavaScript.tmLanguage”)[/code]

This code runs the debug version of node with the current file in context, and opens the users browser to the nodejs debugger.
What I’d like to do is connect the life cycle of the run_command available to the scratch window (a new file window, see below)
so I can steam stdout, stderr and also allow stdin (so for example you can run a node repl in a sublime window with a input area). When you close
the window, I want the process to close. Potentially I want to get extra data (just as process.memory, process.os, etc) and also put this in a content
area of table or tabulated content, and be able to update this information via a script(for example my script can be called ./nodestaticstic --all-process
and it will return an object I can parse with the data to update the view in python.

Another issue I’m having related to above is when I do a command, I have to create the scratch window on the end of the command, when I’d rather put it
to the beginning and updated in an evented way from stdout or stderr, and I need a way to know a stdin is being requested to show an input dialog.
This means on long running processes you don’t know what’s happening.

Most of the structure of the code is taken from the Git plugin (I found to be the most compatible with complexity) it has the following code:

[code]import os
import sublime
import sublime_plugin
import threading
import subprocess
import functools
import tempfile

when sublime loads a plugin it’s cd’d into the plugin directory. Thus

file is useless for my purposes. What I want is “Packages/Git”, but

allowing for the possibility that someone has renamed the file.

Fun discovery: Sublime on windows still requires posix path separators.

PLUGIN_DIRECTORY = os.getcwd().replace(os.path.normpath(os.path.join(os.getcwd(), ‘…’, ‘…’)) + os.path.sep, ‘’).replace(os.path.sep, ‘/’)

main_thread uses sublime.set_timeout to send things onto the main thread

most sublime.[something] calls need to be on the main thread

def main_thread(callback, *args, **kwargs):
sublime.set_timeout(functools.partial(callback, *args, **kwargs), 0)

open_url opens a url in the system browser

def open_url(url):
sublime.active_window().run_command(‘open_url’, {“url”: url})

view_contents returns the view region

def view_contents(view):
region = sublime.Region(0, view.size())
return view.substr(region)

plugin_file returns the path of the plugin file

def plugin_file(name):
return os.path.join(PLUGIN_DIRECTORY, name)

The unicode decode here is because sublime converts to unicode inside

insert in such a way that unknown characters will cause errors, which is

distinctly non-ideal… and there’s no way to tell what’s coming out of

git in output. So…

def _make_text_safeish(text, fallback_encoding):
try:
unitext = text.decode(‘utf-8’)
except UnicodeDecodeError:
unitext = text.decode(fallback_encoding)
return unitext

Class to start a new thread for a command

class CommandThread(threading.Thread):
def init(self, command, on_done, working_dir="", fallback_encoding=""):
threading.Thread.init(self)
self.command = command
self.on_done = on_done
self.working_dir = working_dir
self.fallback_encoding = fallback_encoding

When a thread is run

def run(self):
try:
# Per http://bugs.python.org/issue8557 shell=True is required to
# get $PATH on Windows. Yay portable code.
shell = os.name == ‘nt’
if self.working_dir != “”:
os.chdir(self.working_dir)
proc = subprocess.Popen(self.command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
shell=shell, universal_newlines=True)
output = proc.communicate()[0]
# if sublime’s python gets bumped to 2.7 we can just do:
# output = subprocess.check_output(self.command)
main_thread(self.on_done, _make_text_safeish(output, self.fallback_encoding))
except subprocess.CalledProcessError, e:
main_thread(self.on_done, e.returncode)
except OSError, e:
if e.errno == 2:
main_thread(sublime.error_message, “Node binary could not be found in PATH\n\nConsider using the node_command setting for the Node plugin\n\nPATH is: %s” % os.environ’PATH’])
else:
raise e

class NodeCommand(sublime_plugin.TextCommand):
def run_command(self, command, callback=None, show_status=True, filter_empty_args=True, **kwargs):
if filter_empty_args:
command = [arg for arg in command if arg]
if ‘working_dir’ not in kwargs:
kwargs’working_dir’] = self.get_working_dir()

s = sublime.load_settings("Nodejs.sublime-settings")
if s.get('save_first') and self.active_view() and self.active_view().is_dirty():
  self.active_view().run_command('save')
if command[0] == 'node' and s.get('node_command'):
  command[0] = s.get('node_command')
if command[0] == 'npm' and s.get('npm_command'):
  command[0] = s.get('npm_command')
if not callback:
  callback = self.generic_done

thread = CommandThread(command, callback, **kwargs)
thread.start()

if show_status:
  message = kwargs.get('status_message', False) or ' '.join(command)
  sublime.status_message(message)

def generic_done(self, result):
if not result.strip():
return
self.panel(result)

def _output_to_view(self, output_file, output, clear=False, syntax=“Packages/Diff/Diff.tmLanguage”):
output_file.set_syntax_file(syntax)
edit = output_file.begin_edit()
if clear:
region = sublime.Region(0, self.output_view.size())
output_file.erase(edit, region)
output_file.insert(edit, 0, output)
output_file.end_edit(edit)

def scratch(self, output, title=False, **kwargs):
scratch_file = self.get_window().new_file()
if title:
scratch_file.set_name(title)
scratch_file.set_scratch(True)
self._output_to_view(scratch_file, output, **kwargs)
scratch_file.set_read_only(True)
return scratch_file

def panel(self, output, **kwargs):
if not hasattr(self, ‘output_view’):
self.output_view = self.get_window().get_output_panel(“git”)
self.output_view.set_read_only(False)
self._output_to_view(self.output_view, output, clear=True, **kwargs)
self.output_view.set_read_only(True)
self.get_window().run_command(“show_panel”, {“panel”: “output.git”})

def quick_panel(self, *args, **kwargs):
self.get_window().show_quick_panel(*args, **kwargs)

A base for all git commands that work with the entire repository

class NodeWindowCommand(NodeCommand, sublime_plugin.WindowCommand):
def active_view(self):
return self.window.active_view()

def _active_file_name(self):
view = self.active_view()
if view and view.file_name() and len(view.file_name()) > 0:
return view.file_name()

If there’s no active view or the active view is not a file on the

filesystem (e.g. a search results view), we can infer the folder

that the user intends Git commands to run against when there’s only

only one.

def is_enabled(self):
if self._active_file_name() or len(self.window.folders()) == 1:
return os.path.realpath(self.get_working_dir())

def get_file_name(self):
return ‘’

If there is a file in the active view use that file’s directory to

search for the Git root. Otherwise, use the only folder that is

open.

def get_working_dir(self):
file_name = self._active_file_name()
if file_name:
return os.path.dirname(file_name)
else:
return self.window.folders()[0]

def get_window(self):
return self.window

A base for all git commands that work with the file in the active view

class NodeTextCommand(NodeCommand, sublime_plugin.TextCommand):
def active_view(self):
return self.view

def is_enabled(self):
# First, is this actually a file on the file system?
if self.view.file_name() and len(self.view.file_name()) > 0:
return os.path.realpath(self.get_working_dir())

def get_file_name(self):
return os.path.basename(self.view.file_name())

def get_working_dir(self):
return os.path.dirname(self.view.file_name())

def get_window(self):
# Fun discovery: if you switch tabs while a command is working,
# self.view.window() is None. (Admittedly this is a consequence
# of my deciding to do async command processing… but, hey,
# got to live with that now.)
# I did try tracking the window used at the start of the command
# and using it instead of view.window() later, but that results
# panels on a non-visible window, which is especially useless in
# the case of the quick panel.
# So, this is not necessarily ideal, but it does work.
return self.view.window() or sublime.active_window()

Commands to run

class NodeRunCommand(NodeTextCommand):
def run(self, edit):
command = ‘node’, self.view.file_name()]
self.run_command(command, self.command_done)

def command_done(self, result):
self.scratch(result, title=“Node Output”, syntax=“Packages/JavaScript/JavaScript.tmLanguage”)

class NodeDrunCommand(NodeTextCommand):
def run(self, edit):
command = ‘node’, ‘debug’, self.view.file_name()]
open_url(‘http://127.0.0.1:5858’)
self.run_command(command, self.command_done)

def command_done(self, result):
self.scratch(result, title=“Node Output”, syntax=“Packages/JavaScript/JavaScript.tmLanguage”)

class NodeRunArgumentsCommand(NodeTextCommand):
def run(self, edit):
self.get_window().show_input_panel(“Arguments”, “”, self.on_input, None, None)

def on_input(self, message):
command = message.split()
command.insert(0, self.view.file_name());
command.insert(0, ‘node’);
self.run_command(command, self.command_done)

def command_done(self, result):
self.scratch(result, title=“Node Output”, syntax=“Packages/JavaScript/JavaScript.tmLanguage”)

class NodeDebugRunArgumentsCommand(NodeTextCommand):
def run(self, edit):
self.get_window().show_input_panel(“Arguments”, “”, self.on_input, None, None)

def on_input(self, message):
command = message.split()
command.insert(0, self.view.file_name());
command.insert(0, ‘debug’);
command.insert(0, ‘node’);
self.run_command(command, self.command_done)

def command_done(self, result):
self.scratch(result, title=“Node Output”, syntax=“Packages/JavaScript/JavaScript.tmLanguage”)

class NodeNpmCommand(NodeTextCommand):
def run(self, edit):
self.get_window().show_input_panel(“Arguments”, “”, self.on_input, None, None)

def on_input(self, message):
command = message.split()
command.insert(0, “npm”);
self.run_command(command, self.command_done)

def command_done(self, result):
self.scratch(result, title=“NPM Output”, syntax=“Packages/JavaScript/JavaScript.tmLanguage”)
[/code]

Here are the kind of commands I would like to do:

{ "caption": "Node: Run current file", "command": "node_run" }, { "caption": "Node: Run current file with arguments", "command": "node_run_arguments" }, { "caption": "Node: Run current file : debug", "command": "node_drun" }, { "caption": "Node: Run current file with arguments : debug", "command": "node_drun_arguments" }, { "caption": "Node: NPM Command", "command": "node_npm" }, { "caption":Node: NPM Install", "command":"node_npm_install" }]

0 Likes

End long running plugin
#2

Why don’t you put it up on github to make it simple for people to collaborate and help you get this working. My experience does not run deep enough with python to just take a quick glance at your code and give a suggestion (there may be some here who can), but if you make it simple to pull down you might get some tinkerers to come up with something. I am kind of fond of Node.js.

0 Likes

#3

It’s available at github.com/tanepiper/SublimeText-Nodejs, I mentioned it in another post on the forum

0 Likes