Wing Tips: Extending Wing with Python (Part One)

Jun 07, 2019


In this issue of Wing Tips we start to look at how to extend Wing's functionality by writing Python code. Extension scripts can be used to automate custom editing tasks, access Wing's source code analysis, control the debugger, and much more.

A Simple Example

Scripts written to extend Wing are regular Python files containing one or more function definitions at the top level of the file. Each of these functions creates a new IDE command that can be used just like Wing's built-in command set.

The following example creates a new command hello-world in the IDE:

import wingapi
def hello_world():
  wingapi.gApplication.ShowMessageDialog("Hello", "Hello world!")

To try this, drop it into a file named test.py in the scripts directory inside the Settings Directory listed in Wing's About box. Then select Reload All Scripts from the Edit menu so Wing scans for newly added scripts. Once that's done, you don't need to use Reload All Scripts again since Wing watches already-loaded script files and reloads them automatically when you save them to disk.

Now you can run your script with Command by Name from the Edit menu, by typing the command name hello-world into the mini-buffer that appears at the bottom of Wing's window and then pressing Enter:

/images/blog/scripting-1/command-by-name.png

You can also use the User Interface > Keyboard > Custom Key Bindings preference to bind the command hello-world to any key, or add the command to the tool bar with the User Interface > Toolbar > Custom Items preference.

Whatever method you use to invoke hello-world, you should see a dialog that looks something like this:

/images/blog/scripting-1/hello-world.png

Adding Menus

Commands created by scripts may also be added to new menus in the menu bar, by setting a function attribute as follows:

import wingapi
def hello_world():
  wingapi.gApplication.ShowMessageDialog("Hello", "Hello world!")
hello_world.contexts = [wingapi.kContextNewMenu("Scripts")]

After making this change in your copy of test.py, you should see the Scripts menu appear in the menu bar immediately after saving the file:

/images/blog/scripting-1/menu.png

Scripts can also add items to the editor context menu, project context menu, or the high-level configuration menu in the top right of Wing's window. We'll go over these in a future article in this series.

Enabling Auto-Completion and Documentation

Creating scripts is a lot easier if Wing offers auto-completion and documentation in the Source Assistant tool. To enable that, let's use a bit of script-based automation to take care of the necessary project setup.

Start by appending the following to the test.py script file that you created earlier, and then save to disk:

import wingapi
import os
def setup_script_completion():
  app = wingapi.gApplication
  proj = app.GetProject()
  pypath = proj.GetEnvironment().get('PYTHONPATH', None)
  dirname = os.path.join(app.GetWingHome(), 'src')
  if pypath is None:
    pypath = dirname
  elif not dirname in pypath.split(os.pathsep):
    pypath += os.pathsep + dirname
  app.GetProject().SetEnvironment(None, 'startup', {'PYTHONPATH': pypath})
setup_script_completion.contexts = [wingapi.kContextNewMenu("Scripts")]

Now you can configure any project for easier scripting by selecting Setup script completion from the Scripts menu. This adds the directory that contains Wing's scripting API to the Python Path in your project's Project Properties.

Once this is done, the auto-completer works on the contents of wingapi, documentation for the API appears in the Source Assistant as you type or navigate code, and Goto Definition will bring up the API's implementation source:

/images/blog/scripting-1/complete-sassist.gif

You may of course want to apply this configuration only to a project you create for working on your Wing extension scripts, to avoid altering the Python Path used in other projects.

A More Complex Example

Here's a more complex real world example that moves the caret to the start of the current block in Python code. You can try this by appending it to your copy of test.py, saving to disk, and then using``Goto Start of Block`` in the Scripts menu in any Python file:

import wingapi
from wingbase import textutils

def _indent_chars(line):
  indent_chars = 0
  for i, c in enumerate(line):
    if c in ' \t':
      indent_chars += 1
    else:
      break
  return indent_chars

def start_of_block():
  ed = wingapi.gApplication.GetActiveEditor()
  doc = ed.GetDocument()
  txt = doc.GetText()
  pos, end = ed.GetSelection()

  first_indent = None
  lineno = doc.GetLineNumberFromPosition(pos)
  while lineno >= 0:
    line = txt[doc.GetLineStart(lineno):doc.GetLineEnd(lineno)]
    indent = _indent_chars(line)
    if first_indent is None:
      if indent != 0:
        first_indent = indent
      lineno -= 1
    elif indent >= first_indent:
      lineno -= 1
    elif not line.strip():
      lineno -= 1
    else:
      break

  target = doc.GetLineStart(lineno)
  target += _indent_chars(line)

  ed.SetSelection(target, target)

def _start_of_block_available():
  ed = wingapi.gApplication.GetActiveEditor()
  if ed is None:
    return False
  mime = wingapi.gApplication.GetMimeType(ed.GetDocument().GetFilename())
  return mime == 'text/x-python'

start_of_block.available =_start_of_block_available
start_of_block.label = "Goto Start of Block"
start_of_block.contexts = [wingapi.kContextNewMenu('Scripts', 1)]

This example introduces a few other concepts:

(1) You can place utility functions at the top of a script file by prefixing their names with _ (underscore). This prevents Wing from creating a new command out of that function.

(2) Setting the function attribute available on a script controls when the command created by it is available in the user interface; when it isn't, menu items are greyed out.

(3) The label attribute replaces Wing's automatically generated label for the command, when it is shown in menus.

(4) A group number can be included in kContextNewMenu to group script-created commands into separate sections of menus.

This is just a small sampling of the capabilities of extension scripts in Wing. We'll go into more depth in the next few installments in this series. Or see Scripting and Extending Wing for detailed documentation.



That's it for now! In the next Wing Tip we'll look at how to use Wing to debug extension scripts, to make it much easier to develop more complex extensions.



Share this article: