Home Download Buy Blog Forum Support

Help with some plugin development questions

Help with some plugin development questions

Postby tanepiper on Mon Nov 14, 2011 8:35 pm

Hi there,

Code is here: https://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: Select all
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")


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: Select all
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")


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

Code: Select all
[{
  "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"
}]
Last edited by tanepiper on Mon Nov 14, 2011 9:09 pm, edited 1 time in total.
tanepiper
 
Posts: 68
Joined: Sun Nov 06, 2011 6:40 am

Re: Help with some plugin development questions

Postby facelessuser on Mon Nov 14, 2011 8:57 pm

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.
facelessuser
 
Posts: 1575
Joined: Tue Apr 05, 2011 7:38 pm

Re: Help with some plugin development questions

Postby tanepiper on Mon Nov 14, 2011 9:09 pm

It's available at https://github.com/tanepiper/SublimeText-Nodejs, I mentioned it in another post on the forum
tanepiper
 
Posts: 68
Joined: Sun Nov 06, 2011 6:40 am


Return to Plugin Development

Who is online

Users browsing this forum: No registered users and 7 guests