#######################################################################
""" wingapi.py -- Formal scripting API for Wing IDE

Copyright (c) 1999-2012, Archaeopteryx Software, Inc.  All rights reserved.

Written by John P. Ehresman and Stephan R.A. Deibel

"""
#########################################################################

import os
import sys
import string
import types
import inspect
import time

import edit.editor
import command.commandmgr

from guiutils import wgtk

import config
from wingutils import destroyable
from wingutils import location

import logging
logger = logging.getLogger('scripts')


#############################################################################
# Constants and utilities
#############################################################################

# Revision number for the API.  Increases only when there are potentially 
# non-forward-compatible changes, if ever.  If these occur, they will be
# documented in the Scripting chapter of the user manual.
kAPIVersion = 1

# These are used to set magic defaults for command arguments.  Using these
# constants as default will cause Wing to pass in the actual object when
# the command is invoked.  For example, use kEditor to get an instance
# of CAPIEditor for the editor selected by the user.

# The application singleton (CAPIApplication)
kArgApplication = []

# The current debugger (CAPIDebugger)
kArgDebugger = []

# The current project (CAPIProject)
kArgProject = []

# The user-selected source editor (CAPIEditor)
kArgEditor = []

# The user-selected document (CAPIDocument)
kArgDocument = []

# The user-selected file name (as string) -- not necessarily the same
# as the current editor or document since this can also be a selection
# on the project and elsewhere.
kArgFilename = []

# The user-selected source scope in [filename, lineno (0=first), symbol1, symbol2, ...] form
kArgSourceScope = []

# Numeric modifier given by user when executing the command
# This is only used for certain key bindings that allow typing
# a numeric modifier before commands.  Otherwise, the default
# passed to the constructor is used, or user is prompted
# if constructed w/o default.  Usage examples:
# arg1=kArgNumericModifier(-1)   -- Use -1 if value missing
# arg2=kArgNumericModifier()     -- Prompt user if value missing
kArgNumericModifier = command.commandmgr.kArgNumericModifier

# Data class used to define argument information for scripts:  Constructor wants:
#
#   doc        -- Documentation string describing the argument
#   type       -- Arg data type; wingutils.datatype.CTypeDef
#   formlet    -- Form model for arg collection gui; wingutils.formbuilder.CDataGui
#   label      -- (Optional) Label to display in arg collection gui
#
from command.commandmgr import CArgInfo

# These define places where commands can be made available.  Use these with
# the 'contexts' attribute on the command.
class kContextEditor:
  """Append to editor context menu. Groups are separated by dividers
  and items are alphabetical within the groups."""
  def __init__(self, group=0):
    self.name = "Editor"
    self.group = group
class kContextProject:
  """Append to project context menu. Groups are separated by dividers
  and items are alphabetical within the groups."""
  def __init__(self, group=0):
    self.name = "Project"
    self.group = group
class kContextNewMenu:
  """Adds new menu in menubar.  Groups are separated by dividers and items
  are alphabetical within the groups."""
  def __init__(self, name, group=0):
    self.name = name
    self.group = group

# Used internally to connect above special default values with real objects
def _CreateSpecialArgDefaultMap(singletons):
  """ Create map for accessing special arg defaults """
  retval = {}

  assert gApplication is not None
  app = gApplication
  retval[id(kArgApplication)] = app
  retval[id(kArgDebugger)] = app.GetDebugger
  retval[id(kArgProject)] = app.GetProject
  retval[id(kArgEditor)] = app.GetActiveEditor
  retval[id(kArgDocument)] = app.GetActiveDocument
  retval[id(kArgFilename)] = app.GetCurrentFiles
  retval[id(kArgSourceScope)] = app.GetCurrentSourceScopes
  # kArgNumericModifier is special cased internally
  return retval

# A note on filenames
# -------------------
#
# File names may either be the name of a local file on disk or a URI.
# Currently, only local file names and 'unknown:' URIs are supported.
# The latter are used for unsaved files and scratch buffers.  For
# example, 'unknown:untitled-1.py' and 'unknown:Scratch' both refer
# to an unsaved file.
#
# Although the API also accepts 'file:' URIs, these are not currently
# treated in a standards-compliant way and should be avoided.
#
# Other URIs may be supported in the future.

def IsUrl(filename):
  """Tests whether given filename is actually a url -- use this on
  filenames obtained from the API to determine how to treat them.
  When this returns False, the filename is a valid local file name."""
  import urlparse
  try:
    p = urlparse.urlparse(filename)
    return location.kURISchemes.has_key(p[0])
  except:
    return True

def _LocationToFilename(loc):
  """Convert location object into full path file name.  Untitled and
  scratch buffer file names are prefixed with 'unknown:'"""
  if loc is None:
    return None
  elif isinstance(loc, location.CLocalFileLocation):
    return loc.fName
  else:
    return loc.fUrl

def _FilenameToLocation(filename):
  """Convert full path file name to location object"""
  
  if filename is None:
    return None
  else:
    return location.CreateFromName(location.SanitizeUrl(filename))
  

###########################################################################
class _CAPIBase(destroyable.CDestroyable):
  """Base class for all API classes -- do not instantiate this directly"""

  # Override in derived classes with a map from signal name to a method
  # that will install a signal forwarder
  _fSignalForwarders = {}
  
  __fWrappers = {}
  @classmethod
  def _GetWrapper(cls, obj):
    """ Get wrapper for object.  Ensures that at most 1 wrapper exists
    per object. """
    
    key = (cls, obj)
    wrapper = cls.__fWrappers.get(key)
    if wrapper is None and gApplication is not None:
      def on_destroy(obj):
        cls.__fWrappers.pop(key, None)

      wrapper = cls(gApplication.fSingletons, obj)
      obj.connect('destroy', on_destroy)
      cls.__fWrappers[key] = wrapper
      
    return wrapper

  def __init__(self, singletons, obj, signals=()):
    destroyable.CDestroyable.__init__(self, signals)

    if 0:
      import singleton
      isinstance(singletons, singleton.CWingSingletons)
    self.fSingletons = singletons
    self.__fObj = obj
    self.__fObjSignalConnections = {}

    self.__fObj.connect('destroy', self.destroy)

  def __connect_forwarder(self, signal_name):
    if self.num_catchers(signal_name) != 0 or signal_name == 'destroy':
      return
  
    forwarder_info = self._fSignalForwarders.get(signal_name)
    if forwarder_info is not None:
      tag = forwarder_info(self)
    else:
      def default_forward(*args):
        self.emit(signal_name, *args)
      tag = self.__fObj.connect(signal_name, default_forward)

    self.__fObjSignalConnections[signal_name] = tag
    
  def connect(self, name, fct, *extra):
    
    self.__connect_forwarder(name)
    return destroyable.CDestroyable.connect(self, name, fct, *extra)
    
  def connect_while_alive(self, name, fct, alive_obj):
    
    self.__connect_forwarder(name)
    return destroyable.CDestroyable.connect_while_alive(self, name, fct, alive_obj)
    
  def disconnect(self, tag):
    
    destroyable.CDestroyable.disconnect(self, tag)

    if not self.destroyed():
      for signal_name, connection_info in self.__fObjSignalConnections.items():
        if self.num_catchers(signal_name) == 0:
          obj, id = connection_info
          obj.disconnect(id)
          del self.__fObjSignalConnections[signal_name]

  def Connect(self, signal, fct, alive_obj=None):
    """Connect handler function to given signal.  If alive_obj is given,
    the connection is automatically broken when that object is destroyed."""

    return self.fSingletons.fScriptMgr.ConnectToSignal(self, signal, fct, alive_obj)
  
  def Disconnect(self, id):
    """ Disconnect previously connected signal handler"""

    self.fSingletons.fScriptMgr.DisconnectFromSignal(self, id)
    

###########################################################################
# Top-level application API
###########################################################################

# This will be set to CAPIApplication instance before any command is called
gApplication = None

# Trick so Wing's auto-completer knows what to do
if 0:
  assert isinstance(gApplication, CAPIApplication)

class CAPIApplication(_CAPIBase):
  """ API to access the IDE functionality. """

  def __init__(self, singletons, app):
    """ Constructor.  Signals supported are:
    
      destroy                 -- The application is closing.  Args: (app, )
      editor-open             -- An editor was opened.  Args: (editor, )
      document-open           -- A document was opened:  Args: (doc, )
                                 (several editors may share a document)
      project-open            -- A project was opened.   Args: (filename, )
      project-close           -- A project was closed.   Args: (filename, )
      active-editor-changed   -- Active editor has changed.  Args: (editor, )
      active-window-changed   -- Active window has changed.  The window is None
                                 if Wing is no longer at front.  Args: (window_name, )
      perspective-changed     -- Current perspective has been changed.  Args: (persp_name, )
      
    """
    _CAPIBase.__init__(self, singletons, app,
                       ('editor-open', 'document-open',
                        'project-open', 'project-close', 
                        'active-editor-changed',
                        'active-window-changed',
                        'perspective-changed'))
      
  #------------------------------------------------------------------------
  # Top-level Settings and Environment
    
  def GetProductInfo(self):
    """ Returns the current Wing IDE version, build, product code,
    product name, and release date.
        
    Valid product codes and names are:
      
    0x00000001 'Personal'
    0x00000002 'Professional'
    0x00000004 'Enterprise'
 
    An example return value:
      
    (2.0.0, 1, 0x00000002, 'Professional', 'Aug 1, 2004')
    
    """
    return (config.kVersion, config.kBuild, config.kProductCode, config.kProduct, 
            config.kReleaseDate)
  
  def GetWingHome(self):
    """ Returns the current Wing IDE installation directory """
    return config.kWingHome
  
  def GetUserSettingsDir(self):
    """ Returns the current Wing IDE users settings directory """
    return config.kUserWingDir

  def GetStartingDirectory(self):
    """Get the most logical starting directory to use when browsing
    for files or directories (varies based on select on user interface)"""
    return self.fSingletons.fWingIDEApp.GetStartingDirectory()
  
  def FindPython(self):
    """ Find the default Python interpreter that Wing will use if none is
    specified with Python Executable in Project Properties. Wing tries looking
    for it as follows:
  
    On Linux:
  
    - Try 'python' in the current environment
    - Search PATH for python* (such as python2.7 or python3.2)
    - As a last resort, use the last known working Python if there was one
    
    On OS X:
    
    - Use /Library/Frameworks/Python.framework/Versions/Current/bin/python 
      if is exists and is valid
    - Try 'python' in the current environment
    - Search PATH for python* (such as python2.7 or python3.2)
    - As a last resort, use the last known working Python if there was one
    
    On Windows:
  
    - Try 'python' in the current environment
    - Look for the latest version in the registry using these keys:
      HKEY_CURRENT_USER\SOFTWARE\PYTHON\PYTHONCORE\#.#\INSTALLPATH
      HKEY_LOCAL_MACHINE\SOFTWARE\PYTHON\PYTHONCORE\#.#\INSTALLPATH
    - As a last resort, use the last known working Python if there was one
    
    Returns the full path to the interpreter.  The value is validated in that
    the interpreter is actually executed and sys.executable is returned.
    
    NOTE: This call will ignore versions of Python that Wing does not yet support.
    
    """
    
    return self.fSingletons.fInterpMgr.FindPython()    

  #------------------------------------------------------------------------
  # Command Execution
    
  def CommandAvailable(self, cmd, **args):
    """Check whether the given command is available for execution with
    the given keyword arguments.  The command can either be the name of a 
    command (see Wing's command reference) or the command itself."""
    return self.fSingletons.fCmdMgr.CommandAvailable(cmd, **args)
    
  def ExecuteCommand(self, cmd, **args):
    """Execute the given command with given keyword arguments.  The command 
    can either be the name of a command (see Wing's command reference) or
    the command itself.  To execute an external command (or command line),
    see the ExecuteCommandLine(), AsyncExecuteCommandLine*(), and
    *OSCommand() calls."""
    self.fSingletons.fCmdMgr.Execute(cmd, **args)
    
  def InstallTimeout(self, timeout, fct):
    """ Install a function to be called in given number of milliseconds.
    The function is called repeatedly at the given interval until it 
    returns 0 or None.  Note that the timeout will also be removed if
    its script module is reloaded, in order to avoid calling old byte
    code.  For this reason, modules must reinstall timeouts during 
    initialization.  Returns a timeout_id that can be sent to RemoveTimeout
    to remove the timeout prematurely."""

    return self.fSingletons.fScriptMgr.InstallTimeout(timeout, fct)

  def RemoveTimeout(self, timeout_id):
    """Remove a timeout previously installed with InstallTimeout. Note that in
    many cases the timeout function uninstalls itself by returning 0 or
    None."""
    
    self.fSingletons.fScriptMgr.RemoveTimeout(timeout_id)
    
  #------------------------------------------------------------------------
  # Access to Key Objects
  
  def GetActiveWindow(self):
    """ Get the internal name of the currently active window. This is None if 
    no window in Wing has the focus. """
    win = self.fSingletons.fWinMgr.GetActiveWindow()
    if win is None:
      return None
    return win.GetName()

  def GetActiveEditor(self):
    """ Get the currently active editor (CAPIEditor). This is None if no
    editor has the focus. """
    editor = self.fSingletons.fGuiMgr.GetActiveDocument()
    if editor is None or not isinstance(editor, edit.editor.CSourceEditor):
      return None
    wrap = CAPIEditor._GetWrapper(editor)
    isinstance(wrap, CAPIEditor)
    return wrap

  def GetActiveDocument(self):
    """ Get the currently active document (CAPIDocument). This is None if no
    document has the focus. """
    editor = self.fSingletons.fGuiMgr.GetActiveDocument()
    if editor is None or not isinstance(editor, edit.editor.CSourceEditor):
      return None
    wrap = CAPIDocument._GetWrapper(editor.fCache)
    isinstance(wrap, CAPIDocument)
    return wrap

  def GetCurrentFiles(self):
    """ Get the currently active file(s), as selected by user somewhere on
    the gui. May be in current editor, on project manager, in source
    browser, etc. Returns a list of full path filenames for the file(s) or 
    None if none are selected.  The file name is prefixed with 'unknown:'
    for untitled and scratch buffers."""
    scopes = self.fSingletons.fGuiMgr.GetCurrentSourceScopes()
    if scopes is None:
      return None
    else:
      return [_LocationToFilename(s[0]) for s in scopes]

  def GetCurrentSourceScopes(self):
    """ Get the current source scope(s), as selected by user somewhere on the
    gui. May be in current editor, on project manager, in source browser, etc,
    as determined by keyboard/mouse focus. Returns None if nothing selected or
    a list of lists each containing a filename followed by line number
    (0=first in file), followed by zero or more source symbol names indicating
    the nested scope to which user has selected. E.g. [["/x/y/z.py", 120,
    "Class1", "Method1"],] if only Class1.Method1() in z.py is selected.  Filenames
    are prefixed with 'unknown:' for untitled and scratch buffers."""
    loc_scopes = self.fSingletons.fGuiMgr.GetCurrentSourceScopes()
    if loc_scopes is None:
      return None
    else:
      return [[_LocationToFilename(t[0]), t[1] - 1] + t[2:] for t in loc_scopes]
  
  def GetAllFiles(self, visible_only=False, sticky_only=False):
    """ Get the full path names of all currently open files (whether or
    not a document object and editor has been created for them).  Filenames
    are prefixed with 'unknown:' for untitled and scratch buffers.
    Optionally filter the result to omit non-visible files or those
    that are non-sticky (transient files that are auto-closed when
    no longer visible in the GUI)."""
    locs = self.fSingletons.fGuiMgr.GetDocuments(visible_only=visible_only,
                                                 sticky_only=sticky_only)
    return [_LocationToFilename(loc) for loc in locs]

  def GetOpenDocuments(self):
    """ Get all currently open documents -- this includes only those documents
    for which a document object has already been created (documents are not
    created until they need to be shown)."""
    retval = []
    for cache in self.fSingletons.fCacheMgr.GetCaches():
      if cache.GetViewerCount(True):
        retval.append(CAPIDocument._GetWrapper(cache))
    return retval
  
  def CreateWindow(self, name):
    """Create a new window with given internal name. The window is initially blank;
    use OpenEditor() with the window_name arg to fill it. """
    self.fSingletons.fGuiMgr.CreateDocumentWindow(name)

  def CloseWindow(self, name, allow_cancel=1):
    """ Close the window with given internal name, and all the editors in it.
    When allow_cancel is 0, the window is closed without prompting user to
    save any changes made there. """
    self.fSingletons.fGuiMgr.CloseDocumentWindow(name, allow_cancel)

  def OpenEditor(self, filename, window_name=None, raise_window=False,
                 sticky=True):
    """ Open the given file into an editor.  If the window's internal
    name is given, the editor opens into that window; otherwise
    the most recently visited window is used.  If window name is 
    not the name of an existing window, a new window is created 
    with that name.  Set sticky to False to auto-close the filename
    when all editors for it are hidden.  Note that the file may
    report a different name if symbolic links exist and an equivalent
    file was already open.  Returns the CAPIEditor or None if failed."""
    
    win = None
    if window_name != None:
      win = self.fSingletons.fGuiMgr.FindDocumentWindow(window_name)
      if win is None:
        win = self.fSingletons.fGuiMgr.CreateDocumentWindow(window_name)

    loc = _FilenameToLocation(filename)
    editor = self.fSingletons.fGuiMgr.DisplayDocument(loc, blank_if_not_found=1,
                                                      win=win, raise_window=raise_window,
                                                      sticky=sticky)
    if editor is None:
      return None
    else:
      wrap = CAPIEditor._GetWrapper(editor)
      isinstance(wrap, CAPIEditor)
      return wrap

  def ScratchEditor(self, title='Scratch', mime_type='text/plain',
                    raise_window=False, raise_view=True, 
                    sticky=True, window_name=None):
    """Create a scratch editor with given title and mime type.
    The document can be edited but will never be marked as changed
    and requiring a save to disk.  However, it can be saved with
    Save As if desired.  If title contains %d, a sequence number will
    be inserted automatically."""

    win = None
    if window_name != None:
      win = self.fSingletons.fGuiMgr.FindDocumentWindow(window_name)
      if win is None:
        win = self.fSingletons.fGuiMgr.CreateDocumentWindow(window_name)

    editor = self.fSingletons.fGuiMgr.ScratchDocument(title, mime_type, 1,
                                                      raise_window, raise_view,
                                                      sticky, win)
    if editor is None:
      return None
    else:
      wrap = CAPIEditor._GetWrapper(editor)
      isinstance(wrap, CAPIEditor)
      return wrap

  def GetAnalysis(self, filename):
    """Get a CAPIStaticAnalysis object for the given Python file"""

    ana = self.fSingletons.fCacheMgr.GetAnalysis(_FilenameToLocation(filename))
    import pysource.analysis
    if isinstance(ana, pysource.analysis.CPySourceAnalysis):
      return CAPIStaticAnalysis(self.fSingletons, ana)
    else:
      return None
  
  def OpenURL(self, url):
    """Open the given URL with an external viewer"""
    self.fSingletons.fWingIDEApp.ExecuteURL(url)
    
  def GetProject(self):
    """ Get the current project """
    proj = self.fSingletons.fProjMgr.fProject
    if proj is None:
      return None
    wrap = CAPIProject._GetWrapper(proj)
    isinstance(wrap, CAPIProject)
    return wrap
  
  def NewProject(self, completed_cb, failure_cb=None):
    """ Create a new project.  The given callbacks are invoked when the
    process is complete (the failure_cb if the user cancels closing
    the current project)"""
    self.fSingletons.fProjMgr.NewProject(completed_cb, failure_cb)
  
  def GetDebugger(self):
    """ Get the debugger API object """
    wrap = CAPIDebugger._GetWrapper(self.fSingletons.fDebugMgr)
    isinstance(wrap, CAPIDebugger)
    return wrap
  
  def SetClipboard(self, txt):
    """Set given text to the clipboard. The text should be a UTF-8 string or
    unicode object."""
    if isinstance(txt, unicode):
      txt = txt.encode('utf-8')
    clip = wgtk.clipboard_get()
    clip.set_text(txt)
    clip.store()
    
  def GetClipboard(self):
    """Get text on the clipboard, as a unicode string."""
    clip = wgtk.clipboard_get()
    txt = clip.wait_for_text()
    if txt is None:
      return ''
    else:
      return unicode(txt, 'utf-8', 'replace')
    
  def ShowTool(self, name, flash=True, grab_focus=True):
    """Show the given tool in the user interface. The name can be one of:
    'project', 'browser', 'batch-search', 'interactive-search',
    'source-assistant', 'debug-data', 'debug-stack', 'debug-io',
    'debug-exceptions', 'debug-breakpoints', 'debug-probe', 'debug-watch',
    'debug-modules', 'python-shell', 'about', 'messages', 'help', 'indent',
    'bookmarks', 'testing', 'open-files', os-command', 'snippets', 'diff',
    'uses', or 'refactoring'.  The tool title is flashed if flash is True
    and focus is moved to the tool if grab_focus is True."""
    
    self.fSingletons.fGuiMgr.ShowPanel(name, flash=flash, grab_focus=grab_focus)

  #------------------------------------------------------------------------
  # Application state
  
  def GetVisualState(self, errs=[], style='all'):
    """Get the current application's visual state. The style of state is one
    of 'all' to capture all of the application visual state,
    'tools-and-editors' to capture the set of visible tools, overall layout,
    and editors, and 'tools-only' to capture only the set of visible tools and
    overall layout. Returns an opaque dictionary with the state and adds any
    errors encountered to errs."""
    return self.fSingletons.fGuiMgr.GetVisualState(errs, constraint=style)
    
  def SetVisualState(self, state, errs=[]):
    """Restore previously saved application state. Adds any errors encountered
    to errs."""
    self.fSingletons.fGuiMgr.SetVisualState(state, errs)
    
  #------------------------------------------------------------------------
  # Access to preferences

  def GetPreference(self, pref):
    """Get value for given preference.  The pref argument should be a
    preferences specification defined in one of Wing's prefs source files
    or the fully qualified name of the preference (as given in the
    Preferences References in the Wing IDE manual)."""
    return self.fSingletons.fPrefMgr.GetValue(pref)
    
  def SetPreference(self, pref, value):
    """Set value for given preference.  The pref argument should be a
    preferences specification defined in one of Wing's prefs source files
    or the fully qualified name of the preference (as given in the
    Preferences References in the Wing IDE manual)."""
    self.fSingletons.fPrefMgr.SetValue(pref, value)

  def ConnectToPreference(self, pref, cb):
    """Connect to the given preference so that the callback is called
    whenever the value of the preference changes.  The callback will
    be uninstalled automatically if the caller's script is reloaded."""
    return self.fSingletons.fScriptMgr.ConnectToPreference(pref, cb)
    
  def DisconnectFromPreference(self, pref, id):
    """Disconnect to the given signal handler from preference.  Pass
    in the values returned from ConnectToPreference."""
    self.fSingletons.fPrefMgr.DisconnectFromValue(pref, id)
    
  def ShowPreference(self, prefname):
    """Show the given preference by name in the preference manager.
    Use the fully qualified name (as given in the Preferences References 
    in the Wing IDE manual)"""
    self.fSingletons.fGuiMgr.PrefGUI(prefname)
    
  #------------------------------------------------------------------------
  # Message dialog and message area
    
  def ShowMessageDialog(self, title, text, checks=[], buttons=[("OK", None)], 
                        modal=True):
    """Display modal message dialog to the user with given title and text. If
    checks is non-empty it contains a list of (label, default, callback)
    triples for extra check boxes to add below the message and above the
    buttons (the callback is called with the label and check state immediately
    when the checkbox is used). Set buttons to a list of (label, action) pairs
    to override the default of just an OK box. The button action can be None
    to just close the dialog or it can be a callable that returns True to
    prevent closing of the dialog or False to allow it to close when the
    button is pressed."""
    
    from guimgr import messages    
    from guiutils.dialogs import CButtonSpec

    button_spec = []
    for label, action in buttons:
      button_spec.append(CButtonSpec(label, action))
    
    dlg = messages.CMessageDialog(self.fSingletons, title, text, (),
                                  button_spec)
    if modal:
      dlg.RunAsModal(self.fSingletons.fWinMgr.GetActiveWindow())
    else:
      dlg.Run()
    
  def SetStatusMessage(self, text, timeout=5):
    """Display a transient status message in the integrated status area.
    The message persists for the given timeout or until another status
    message is shown."""

    panel = self.fSingletons.fGuiMgr._GetInteractionPanel()
    if panel is None:
      return
    panel.SetPriorityMsg(text, timeout)
    
  def ClearStatusMessage(self):
    """Clear the status message area to blank"""
    
    panel = self.fSingletons.fGuiMgr._GetInteractionPanel()
    if panel is None:
      return
    panel.RemovePriorityMsg()

  #------------------------------------------------------------------------
  # Sub-process control
    
  def ExecuteCommandLine(self, cmd, dirname, input, timeout, env=None, 
                         bufsize=100000, return_stderr=False):
    """Run the cmd synchronously in given directory and return its output.
    The command can either be a string command line or a list of arguments.
    Input is any text to send to the sub-process (or None). Timeout defines
    max seconds to wait. Unless an env is given, the command is run in the 
    environment configured in the project.
    
    The given buffer size is used for the I/O buffer to the sub-process: if <
    0, system default is used; if 0, I/O is unbuffered; if 1, line buffering
    is used if the OS supports it; if >1, buffer is set to that number of
    bytes.

    Returns err, output_txt where err is one of:
    
      0  -- Success
      1  -- Command could not be launched
      2  -- Command timed out

    When return_stderr is True, output_txt is a tuple (stdout, stderr).  
    Otherwise output_txt is stdout.
    
    Use AsyncExecuteCommandLine() to avoid locking up Wing while the command
    runs, or to access stderr or process exit status.
    
    Compatibility note:
    
    If env is not given, the environment configured in the project is used. In
    Wing 3.0.0alpha1 and earlier, the environment set at startup of Wing was
    used instead, incorrectly ignoring any modifications made in the project.
    
    """

    import proj.project
    
    from wingutils import spawn
    if isinstance(cmd, (str, unicode)):
      args = spawn.ParseCmdArgs(cmd)
    else:
      args = cmd
    if env is None:
      env = proj.project.GetEffectiveEnvironment()
    handler = spawn.CAsyncExecute(args[0], env, dirname, 10000.0, bufsize, *args[1:])  
    if handler.terminated != None:
      handler.Terminate()
      if return_stderr:
        return 1, ('', '')
      else:
        return 1, ''
    
    if input:
      handler.pipes.tochild.write(input)
      handler.pipes.tochild.flush()
      handler.pipes.tochild.close()
    
    max_time = time.time() + timeout
    finished = False
    while time.time() < max_time and not finished:
      finished = handler.Iterate()
      if not finished:
        time.sleep(0.1)

    if finished:
      errno = 0
    else:
      errno = 2
    stdout, stderr, err, exit_status = handler.Terminate(kill=True)
    if return_stderr:
      return (errno, (stdout, stderr))
    else:
      return (errno, stdout)
    
  def AsyncExecuteCommandLine(self, cmd, dirname, *args):
    """Get a handler to run the given command asyncronously in the given
    directory.  Pass additional command line arguments as extra parameters.
    Returns an object that can be polled for the results of the command
    execution.  Here is an example of one iteration:

    if handler.Iterate():
      stdout, stderr, err, exit_status = handler.Terminate()
      print "Done"
      print stdout
    else:
      print "Start time (relative to time.time()):", handler.time
      print "Iteration #", handler.count
      print "Output so far: %i characters", len(handler.stdout)
      
    This can be placed into a function installed with InstallTimeout()
    Be sure that Terminate() is always called and to return True until the
    handler has completed so the timeout is not uninstalled prematurely.
    
    The return values from Terminate() are as follows:
    
      stdout      -- The text received from child process stdout
      stderr      -- The text received from child process stderr
      err         -- Error code if execution failed (else None)
      exit_status -- Exit status of child process (or None if 
                     never launched or could not be determined)
    
    The environment specified in the project is used for the
    sub-process.  Use AsyncExecuteCommandLineE to specify a
    different environment.
    
    To send input to the sub-process, use handler.pipes.tochild.write() 
    optionally followed by handler.pipes.tochild.flush().  When I/O to
    the child is complete, call handler.pipes.tochild.close().
    
    Compatibility note:
    
    In Wing 3.0.0alpha1 and earlier, the environment set for the sub-
    process incorrectly ignored any Python Path specified in the project.
    If set, it is now added to the environment as PYTHONPATH.
    
    """

    import proj.project
    env = proj.project.GetEffectiveEnvironment()
    return self.AsyncExecuteCommandLineE(cmd, dirname, env, *args)

  def AsyncExecuteCommandLineE(self, cmd, dirname, env, *args):
    """Same as AsyncExecuteCommandLine but accepts also the environment
    to send into the debug process."""
    
    from wingutils import spawn
    handler = spawn.CAsyncExecute(cmd, env, dirname, 10000.0, 100000, *args)
    return handler

  def AsyncExecuteCommandLineEB(self, cmd, dirname, env, bufsize, *args):
    """Same as AsyncExecuteCommandLineE but accepts also the I/O buffer size:
    if < 0, system default is used; if 0, I/O is unbuffered; if 1, line
    buffering is used if the OS supports it; if >1, buffer is set to that
    number of bytes.  To pass project environment to this call, use
    the GetEnvironment method on the project."""
    
    from wingutils import spawn
    handler = spawn.CAsyncExecute(cmd, env, dirname, 10000.0, bufsize, *args)
    return handler

  #------------------------------------------------------------------------
  # Sub-process control using the OS Commands tool
    
  def AddOSCommand(self, cmd, dirname, env, flags, *args):
    """Add the given command to the OS Commands tool. The command is the OS
    level command. Dirname can be None to indicate using the project-wide
    default. Env can be None to use the project defaults, or a dict to add
    values to the project default. The flags are a dict containing zero or
    more of the following:
    
    title:        The display title for the command  Default=cmd.
    io-encoding:  Encoding name for I/O (such as utf-8).  Default=None.
    key-binding:  Textual representation of key binding to assign
                  (such as "Ctrl-X Ctrl-Shift-T").  Default=None
    raise-panel:  True to raise the OS Commands tool when this command
                  is executed.  Default=True.
    auto-save:    True to auto-save files before executing the command.
                  Default=False.
    pseudo-tty:   True to use a Pseudo TTY for the command.  Default=False.
    line-mode:    True to set buffering to line mode.  Default=False.
    
    Returns the command ID.
    
    """

    if not config.kAvailOSCommands:
      raise NotImplementedError
    from edit import cap_oscommands
    import proj.attribs

    view = cap_oscommands.GetOSCommandsPanel()

    title = flags.get('title', cmd)
    if dirname is None:
      rundir=(proj.attribs.kProject, '')
    else:
      rundir=(proj.attribs.kCustom, dirname)
    if env is None:
      env = (proj.attribs.kProject, '')
    else:
      env = (proj.attribs.kMerge, env)
    from wingutils import spawn
    cmdline = spawn._win32_cmd_line_from_argv((cmd,) + args)
    scmd = cap_oscommands.CStoredCommand(cap_oscommands.kCommandCommandType,
                                         cmdline, title, rundir, env=env,
                                         pseudo_tty=flags.get('pseudo-tty', False),
                                         line_mode=flags.get('line-mode', False),
                                         io_encoding=flags.get('io-encoding', None),
                                         key_binding=flags.get('key-binding', None),
                                         raise_panel=flags.get('raise-panel', True),
                                         autosave=flags.get('auto-save', False))
    view.AddCommand(scmd, show=False)
    return scmd.id
    
  def RemoveOSCommand(self, cmd_id):
    """Remove OS Command previously defined with AddOSCommand.  The command is
    terminated first if it is currently running."""
    
    if not config.kAvailOSCommands:
      return
    from edit import cap_oscommands
    view = cap_oscommands.GetOSCommandsPanel()
    cmd = view._FindCmdByID(cmd_id)
    view.RemoveCommand(cmd)
    
  def ExecuteOSCommand(self, cmd_id, show=True):
    """Execute the given command in the OS Commands tool, using the ID returned
    from previous call to AddOSCommand.  Show the OS Commands tool if show is True
    or if the command was configured to always show the tool when executed."""
    
    if not config.kAvailOSCommands:
      return
    from edit import cap_oscommands
    
    view = cap_oscommands.GetOSCommandsPanel()
    if show:
      view = self.fSingletons.fGuiMgr.ShowPanel(config.kOSCommandPanel)
      
    view.Execute(cmd_id)
    
  def TerminateOSCommand(self, cmd_id):
    """Terminate OS Command if it is currently running."""
    
    if not config.kAvailOSCommands:
      return
    from edit import cap_oscommands
    view = cap_oscommands.GetOSCommandsPanel()
    view.Terminate(cmd_id)
    
  #------------------------------------------------------------------------
  # Scripting Framework Utilities
    
  def ReloadScript(self, module):
    """Reload the script file(s) associated with the given module or
    filename"""

    if self.destroyed():
      return
    self.fSingletons.fScriptMgr._ReloadScript(module)
    
  def EnablePlugin(self, plugin_id, enable):
    """Enable to disable a plugin. May be called from plugins that auto-enable
    in response to signals, to indicate whether the plugin should be active or
    not.  Note that the user can override the plugin-determined state to either
    set a plugin as always enabled or never enabled, either in preferences or
    in project properties.  Returns True if the plugin was enabled, False if not."""
    
    return self.fSingletons.fScriptMgr._EnablePlugin(plugin_id, enable)
  
  #------------------------------------------------------------------------
  # Internal support for signal forwarding
  
  def __EditorOpened(self):
    def forward(editor):
      self.emit('editor-open', CAPIEditor._GetWrapper(editor))
    id = self.fSingletons.fEditMgr.connect('annotate-editor', forward)
    return self.fSingletons.fEditMgr, id
  def __DocumentOpened(self):
    def forward(cache_mgr, cache):
      self.emit('document-open', CAPIDocument._GetWrapper(cache))
    id = self.fSingletons.fCacheMgr.connect('cache-buffer-created', forward)
    return self.fSingletons.fCacheMgr, id
  def __ProjOpened(self):
    def forward(mgr, proj):
      loc = proj.GetLocation()
      if loc is None:
        return
      self.emit('project-open', _LocationToFilename(loc))
    id = self.fSingletons.fProjMgr.connect('project-opened', forward)
    return self.fSingletons.fProjMgr, id
  def __ProjClosed(self):
    def forward(proj):
      loc = proj.GetLocation()
      if loc is None:
        return
      self.emit('project-close', _LocationToFilename(loc))
    id = self.fSingletons.fProjMgr.connect('project-destroyed', forward)
    return self.fSingletons.fProjMgr, id
  def __ActiveEditorChanged(self):
    def forward():
      self.emit('active-editor-changed', self.GetActiveEditor())
    id = self.fSingletons.fGuiMgr.connect('active-document-changed', forward)
    return self.fSingletons.fGuiMgr, id
  def __ActiveWindowChanged(self):
    def forward(winmgr, win):
      self.emit('active-window-changed', self.GetActiveWindow())
    id = self.fSingletons.fWinMgr.connect('active-window-changed', forward)
    return self.fSingletons.fGuiMgr, id
  def __PerspectiveChanged(self):
    def forward():
      self.emit('perspective-changed', self.fSingletons.fPerspectiveMgr.GetPerspective())
    id = self.fSingletons.fPerspectiveMgr.connect('perspective-changed', forward)
    return self.fSingletons.fGuiMgr, id

  _fSignalForwarders = { 'editor-open': __EditorOpened,
                         'document-open': __DocumentOpened,
                         'project-open': __ProjOpened,
                         'project-close': __ProjClosed,
                         'active-editor-changed': __ActiveEditorChanged,
                         'active-window-changed': __ActiveWindowChanged,
                         'perspective-changed': __PerspectiveChanged,
                       }
    
###########################################################################
# Document API
###########################################################################

class CAPIDocument(_CAPIBase):
  """ API to access an open editor document.  A single document may be
  shared by multiple editors."""

  def __init__(self, singletons, cache):
    """ Constructor.  Signals supported are:
      
      destroy         -- The document is closing.  Args: (doc, )
      modified        -- Text has been modified.  Args:  
                         (insert=0/1, pos, length, text, lines_added)
      presave         -- The document is about to be saved to disk.
                         Args=(filename, encoding) where filename and encoding 
                         are None if the document-set location and
                         encoding will be used.  The callback may
                         make changes to the buffer if desired, though
                         this is best avoided if filename is not None.
      save-point      -- Text has entered or left save point (state where
                         it matches the copy that was read from or written
                         to disk).  Args: save_point=0/1
      filename-changed -- The filename for this document has changed.
                         Args: old_name, new_name
                         
    """
    _CAPIBase.__init__(self, singletons, cache,
                       ('modified', 'presave', 'save-point',
                        'filename-changed'))
    self.fCache = cache
    
  def GetMimeType(self):
    """ Get mime type for this document """
    return self.fSingletons.fFileAttribMgr.GetProbableMimeType(self.fCache.fLocation)
    
  def GetFilename(self):
    """ Get the filename for the source of the editor document """
    return _LocationToFilename(self.fCache.fLocation)
  
  def GetText(self):
    """ Get the whole editor text """
    return unicode(self.GetCharRange(0, self.GetLength()), 'utf_8')
    
  def SetText(self, txt):
    """ Set the whole editor text """
    if isinstance(txt, unicode):
      txt = txt.encode('utf_8')
    self.fCache.fDoc.delete_chars(0, self.GetLength())
    self.fCache.fDoc.insert_string(0, txt)
    
  def DeleteChars(self, start, end):
    """ Delete characters in given range -- note that the character 
    at end pos will be deleted. """
    self.fCache.fDoc.delete_chars(start, end - start + 1)
    
  def InsertChars(self, pos, txt):
    """ Insert characters at given position """
    if not isinstance(txt, unicode):
      txt = unicode(txt)
    txt = txt.encode('utf_8')
    self.fCache.fDoc.insert_string(pos, txt)
    
  def GetLength(self):
    """ Get the total length in characters of the editor """
    return self.fCache.fDoc.length()
  
  def GetLineCount(self):
    """ Get the total number of lines in the editor """
    return self.fCache.fDoc.lines_total()

  def GetCharRange(self, start, end):
    """ Get the text in given range """
    if start == end:
      return ''
    else:
      end = min(self.GetLength(), end)
      return self.fCache.fDoc.get_char_range(start, end - start)

  def GetLineNumberFromPosition(self, pos):
    """ Get line number (0=first) at given character position in editor """
    return self.fCache.fDoc.line_from_position(pos)
  
  def GetLineStart(self, lineno):
    """ Get character position for start of given line number (0=first)"""
    return self.fCache.fDoc.line_start(lineno)

  def GetLineEnd(self, lineno):
    """ Get character position for end of given line number (0=first)"""
    return self.fCache.fDoc.line_end(lineno)
  
  def BeginUndoAction(self):
    """ Set start of undable action group.  It is critical to call
    EndUndoAction at the end of the action or the user will experience
    undos that span many more edits than intended.  Use try/finally
    to guarantee this:

    doc.BeginUndoAction()
    try:
      # edits here
    finally:
      doc.EndUndoAction()

    """
    self.fCache.fDoc.begin_undo_action()
  
  def EndUndoAction(self):
    """ Set end of undoable action group """
    self.fCache.fDoc.end_undo_action()

  def CanUndo(self):
    """Check whether undo is available"""
    return self.fCache.fDoc.can_undo()
  
  def CanRedo(self):
    """Check whether redo is available"""
    return self.fCache.fDoc.can_redo()
  
  def Undo(self):
    """Undo one action"""
    self.fCache.fDoc.undo()
  
  def Redo(self):
    """Check whether redo is available"""
    self.fCache.fDoc.redo()

  def Save(self, filename=None):
    """Save the document to disk.  If a file name is given, a copy is saved
    there without altering the document's primary file."""
    if filename:
      filename = location.SanitizeUrl(filename)
      loc = location.CLocalFileLocation(filename)
      self.fCache.WriteToLocation(loc)
    else:
      self.fCache.WriteToLocation()
    
  def IsSavePoint(self):
    """Check whether buffer matches file on disk"""
    return self.fCache.fDoc.is_save_point()
  
  def GetAsFileObject(self):
    """ Get the editor contents in a file-like object """
    from cache import textcache
    return textcache.ScintDocAsFile(self.fCache.fDoc)
  
  def GetEditors(self):
    """Get all existing editors for this document"""
    eds = self.fSingletons.fEditMgr.GetEditors(self.fCache.fLocation)
    retval = []
    for ed in eds:
      retval.append(CAPIEditor._GetWrapper(ed))
    return retval

  #------------------------------------------------------------------------
  # Internal support for signal forwarding

  def __Modified(self):
    def text_mod(cache, insert, pos, length, text, lines_added):
      self.emit('modified', insert, pos, length, text, lines_added)
    id = self.fCache.connect('text-modified', text_mod)
    return self.fCache, id
 
  def __SavePoint(self):
    def save_point(c, val):
      self.emit('save-point', val)
    id = self.fCache.connect('save-point', save_point)
    return self.fCache, id

  def __FilenameChanged(self):
    def loc_change(cache, old_loc, new_loc):
      old_name = _LocationToFilename(old_loc)
      new_name = _LocationToFilename(new_loc)
      self.emit('filename-changed', old_name, new_name)
    id = self.fCache.connect('location-changed', loc_change)
    return self.fCache, id
    
  def __Presave(self):
    def presave(cache, loc, encoding):
      filename = _LocationToFilename(loc)
      self.emit('presave', filename, encoding)
    id = self.fCache.connect('presave', presave)
    return self.fCache, id

  _fSignalForwarders = {'modified': __Modified,
                        'save-point': __SavePoint,
                        'filename-changed': __FilenameChanged,
                        'presave': __Presave,
                      }
  
###########################################################################
# Editor view API
###########################################################################    
    
class CAPIEditor(_CAPIBase):
  """API to access an editor view"""
  
  def __init__(self, singletons, editor):
    """ Constructor.  Signals supported:
      
      destroy                 -- The editor has been destroyed.  Args = (editor, )
      selection-changed       -- Current selection has changed.  Args = (start, end)
      selection-lines-changed -- Starting and/or ending line for the selection
                                 has changed.  Args = (first_line, last_line)
                                 (0=first line in file)
      scrolled                -- View has scrolled.  Args = ( top_line )
                                 (0=first line in file)
      visible-lines-changed   -- The range of visible lines has changed.
                                 Args = ( top_line, bottom_line) 
      readonly-edit-attempt   -- An edit was attempted and rejected on a readonly
                                 file.  Args = None
      data-entry-stopped      -- Data entry mode has stopped.  Args = None
      
    """
    
    _CAPIBase.__init__(self, singletons, editor, [
      'selection-changed', 'scrolled', 'visible-lines-changed',
      'selection-lines-changed', 'readonly-edit-attempt',
      'data-entry-stopped'
    ])
    self.fEditor = editor

  def IsReadOnly(self):
    """Check whether the editor is readonly"""
    return self.fEditor.fReadOnly
  
  def SetReadOnly(self, readonly):
    """Set whether or not the editor is read-only"""
    self.fEditor.SetReadOnly(readonly)
    
  def GetDocument(self):
    """ Get the document object being shown in this view """
    wrap = CAPIDocument._GetWrapper(self.fEditor.fCache)
    isinstance(wrap, CAPIDocument)
    return wrap
  
  def GetSelection(self):
    """ Get start, end for selection on editor view """
    return self.fEditor.GetSelection()
    
  def SetSelection(self, start, end, expand=1):
    """ Set selection on editor view, optionally expanding any folded parts to
    show the selection.  Does not alter scroll position. """
    self.fEditor.SetSelection(start, end, expand)

  def GetClickLocation(self):
    """Get the offset in text for the last mouse click on the editor"""
    
    return self.fEditor._FindPoint()[1]
  
  def GetFirstVisibleLine(self):
    """ Get number of the first visible line (0=first in file) on screen in
    this editor view """
    return self.fEditor.GetFirstVisibleLine()
  
  def GetNumberOfVisibleLines(self):
    """Get the number of visible lines on screen for this editor view"""
    return self.fEditor._fScint.lines_on_screen()
    
  def GetSourceScope(self):
    """ Get the current source scope, based on position of selection or
    insertion caret. Returns None if nothing selected or a list containing a
    filename followed by line number (0=first in file), followed by zero or
    more source symbol names indicating the nested scope to which user has
    selected. E.g. ["/x/y/z.py", 120, "Class1", "Method1"].  Filenames are
    prefixed with 'unknown:' for untitled or scratch buffers."""
    t = self.fEditor.GetSourceScopes()[0]
    return [_LocationToFilename(t[0]), t[1] - 1] + t[2:]
  
  def ScrollToLine(self, lineno, select=0, pos='slop', store_history=1):
    """ Optionally select and scroll so that given line (0=first) is visible
    in the display. Set pos='slop' to just ensure visibility without a
    specific position, pos='center' to always center on display, or pos='top'
    to always position at top of display. Only pos='center' or pos='top' are
    safe if the editor window has not yet been realized. Selects the whole
    line if select == 1, places caret at start of line if select == 2, makes
    no selection changes if select == 0, or selects given character range if
    select is a tuple of (start, end). Setting lineno to -1 when a (start,
    end) selection is given will use the line number at start of the given
    selection.  Set store_history=0 to avoid remembering the current editor
    position in the visit history."""
    if lineno >= 0:
      lineno += 1
    self.fEditor.ScrollToLine(lineno, select, pos, store_history)
  
  def GetVisualState(self):
    return self.fEditor.GetVisualState([])
  
  def SetVisualState(self, state):
    return self.fEditor.SetVisualState(state, [])
  
  def GrabFocus(self):
    """Set keyboard focus on this editor"""
    self.fEditor.BecomeActive()
    
  def GetTabSize(self):
    """Get the effective tab size for this editor -- this is determined 
    according to the contents of the file and preferences."""
    
    return self.fEditor.GetTabSize()

  def GetIndentSize(self):
    """Get the indent size for this editor -- this is determined according
    to the contents of the file and preferences."""

    return self.fEditor._fIndentSize
  
  def GetIndentStyle(self):
    """Get the predominant indent style used in this file.  Returns one of:
      1 -- spaces only
      2 -- tabs only
      3 -- mixed tabs and spaces
    """
    
    return self.fEditor.GetIndentStyle()

  def SetIndentStyle(self, style):
    """Set the indent style to use in this editor.  This should only be
    used on an empty file or to force indent style regardless of existing
    file content (not a good idea with Python files)."""
    
    self.fEditor.fAdvIndentCap._ForceIndentStyle(style)

  def GetEol(self):
    """Get one end-of-line that matches the content of this editor"""
    return self.fEditor._GetEol()
  
  def CommandAvailable(self, cmd_name, **args):
    """Check whether given named command is available for execution with the
    given args.  Use this to execute commands on an editor even if it's not
    the current editor.  The available commands for an editor are documented
    in the Active Editor Commands section of the Command Reference in the
    Wing IDE manual."""
    
    cmd_map = self.fEditor.GetCommandMap()
    cmd = cmd_map._GetCommand(cmd_name)
    if cmd is None:
      return None
    else:
      return self.fSingletons.fCmdMgr.CommandAvailable(cmd, **args)
    
  def ExecuteCommand(self, cmd_name, **args):
    """Execute a named command on this editor with the given args. Use this to
    execute commands on an editor even if it's not the current editor. The
    available commands for an editor are documented in the Active Editor
    Commands section of the Command Reference in the Wing IDE manual."""
    
    cmd_map = self.fEditor.GetCommandMap()
    cmd = cmd_map._GetCommand(cmd_name)
    if cmd is None:
      return
    self.fSingletons.fCmdMgr.Execute(cmd, **args)
    
  def FoldingAvailable(self):
    """Check whether folding is available and enabled on this editor."""
    
    cap = self.fEditor.fFoldingCap
    if cap is None or not cap.fEnabled:
      return False
    else:
      return True
    
  def FoldUnfold(self, fold_check_cb):
    """Folds or unfolds all the fold points as determined by the given call
    back, which returns 1 to expand, 0 to collapse, and -1 to leave a fold
    point untouched.  If the callback is instead a boolean or other non-callable,
    all folds are either expanded or collapsed.  Returns a list of lineno's
    that were folded and a list of linenos that were expanded."""

    if not self.FoldingAvailable():
      return set(), set()

    cap = self.fEditor.fFoldingCap
    return cap.FoldUnfold(fold_check_cb)

  class _CAPIFieldMetaData:
    """Stores meta data for fields used with StartDataEntry and
    PasteSnippet.  This is minimal at present, but may be expanded
    in the future."""
  
    def __init__(self, auto_enter_from=-1):
      """Args:
        auto_enter_from -- Specifies the field index from which data for
          this field should be auto-entered, rather than allowing the
          user to type into the field.  This is used for fields that appear
          several times.
      """      
      self.fAutoEnterFrom = auto_enter_from
      
  def PasteSnippet(self, txt, fields, auto_terminate=False, meta_data={}):
    """Paste a text snippet into the editor and place the editor into inline
    data entry mode (see StartDataEntry below). The txt is the text to paste
    and fields provides the (start, end) offsets within that text for fields
    the user can enter or alter. Set auto_terminate to True to end data entry
    mode when the last field is reached."""
    
    self.fEditor.PasteTemplate(txt, fields, auto_terminate, meta_data)
    
  # Deprecated:  Will be removed in Wing IDE 6.0
  PasteTemplate = PasteSnippet
  
  def StartDataEntry(self, fields, active_range=(0,-1), goto_first=True,
                     auto_terminate=False, meta_data={}):
    """Start inline data entry mode so user can use the tab and back tab keys
    to move between data "fields" for data entry inline in the editor. The
    fields is a list of (start, end) positions where the fields are located,
    in tab traversal order. The mode is exited when the user presses Esc or
    when the caret moves outside of the given active range (or the adjusted
    range, taking edits into account). The default range (0, -1) indicates the
    entire document. When goto_first is set, the first field in the tab
    sequence will become the current selection. Set goto_first to select first
    field immediately. Set auto_terminate to stop data entry mode when the
    last field is reached."""
    
    self.fEditor.StartDataEntry(fields, active_range, goto_first, auto_terminate, meta_data)
    
  def StopDataEntry(self):
    """Exit inline data entry mode"""
    
    self.fEditor.StopDataEntry()
    
  #------------------------------------------------------------------------
  # Internal support for signal forwarding

  def __SelectionChanged(self):
    def forward(editor, start, end):
      self.emit('selection-changed', start, end)
    id = self.fEditor.connect('selection-changed', forward)
    return self.fEditor, id
  
  def __Scrolled(self):
    def forward(editor, top_line):
      self.emit('scrolled', top_line)
    id = self.fEditor.connect('scrolled', forward)
    return self.fEditor, id
  
  def __VisibleLinesChanged(self):
    def forward(editor, top_line, bottom_line):
      self.emit('visible-lines-changed', top_line, bottom_line)
    id = self.fEditor.connect('visible-lines-changed', forward)
    return self.fEditor, id
  
  def __SelectionLinesChanged(self):
    def sel_line_change(editor, first_line, last_line):
      self.emit('selection-lines-changed', first_line, last_line)
    id = self.fEditor.connect('selection-lines-changed', sel_line_change)
    return self.fEditor, id

  def __ReadonlyEditAttempt(self):
    def ro_edit():
      self.emit('readonly-edit-attempt')
    self.fEditor.connect('readonly-edit-attempt', ro_edit)
    return self.fEditor, id
    
  def __DataEntryStopped(self):
    def data_entry_stopped():
      self.emit('data-entry-stopped')
    self.fEditor.connect('data-entry-stopped', ro_edit)
    return self.fEditor, id
    
  _fSignalForwarders = {'scrolled': __Scrolled,
                        'visible-lines-changed': __VisibleLinesChanged,
                        'selection-changed': __SelectionChanged,
                        'selection-lines-changed': __SelectionLinesChanged,
                        'readonly-edit-attempt': __ReadonlyEditAttempt,
                        'data-entry-stopped': __DataEntryStopped,
                      }
  

###########################################################################
# Project API
###########################################################################

class CAPIProject(_CAPIBase):
  """ API to access the project """
  
  def __init__(self, singletons, project):
    """ Constructor.  Signals supported:
      
      destroy             -- The project is closing.  Args: (proj, )
      files-added         -- Files have been added.  Args = ( filename_list, )
      files-removed       -- Files have been removed.  Args = ( filename_list, )
      
    """
    _CAPIBase.__init__(self, singletons, project, ('files-added', 'files-removed'))
    self.fProject = project

  #-----------------------------------------------------------------------
  # Access to project contents
  
  def GetFilename(self):
    """Gets the filename where the project is stored.  Returns the *.wpr file's
    name.  If the project is a shared project, a file *.wpu in the same 
    directory will also exist and contain user-specific project state.  The
    name will be prefixed with 'unknown:' if the project is untitled."""

    loc = self.fProject.GetLocation()
    return _LocationToFilename(loc)
    
  def GetSelectedFile(self):
    """ Returns the filename of the currently selected file on this project, or
    None if there is no selection """

    # Just one project open at a time for now
    assert self.fProject is self.fSingletons.fProjMgr.fProject

    panel_defn = self.fSingletons.fPanelMgr.GetPanelDefn(config.kProjectManagerPanel)
    if panel_defn is None or len(panel_defn.fInstanceList) == 0:
      return None

    proj_view = panel_defn.fInstanceList[-1].fView
    if proj_view is None:
      return None
    
    scopes = proj_view.GetSourceScopes()
    if len(scopes) == 0:
      return None

    return _LocationToFilename(scopes[0][0])
    
  def GetAllFiles(self):
    """ Returns a list of all the full path filenames in this project. """
    locs = self.fProject.GetFiles()
    return [_LocationToFilename(loc) for loc in locs]
    
  def GetAllDirectories(self, top_only = False):
    """ Get list of full path names for all directories in this project. If
    top_only is true, only dirs that aren't contained within other dirs with
    files are returned. """
    locs = self.fProject._GetDirs(top_only)
    return [_LocationToFilename(loc) for loc in locs]
    
  def AddFiles(self, files):
    """ Add the files with given filenames to the project """
    self.fProject.AddFiles([_FilenameToLocation(x) for x in files])
    
  def RemoveFiles(self, files):
    """ Remove the given filenames from the project """
    self.fProject.RemoveFiles([_FilenameToLocation(x) for x in files])
    
  def AddDirectory(self, dirname, recursive=True, filter='*', include_hidden=False,
                   watch_disk=True, excludes=()):
    """Add the given directory to the project, optionally recursively adding also
    all children.  The filter specifies which files to display.  Set include_hidden
    to True to show also hidden files like .svn, *~, and so forth.  Set watch_disk
    to watch the disk for changes and update the Project view accordingly.  Set
    excludes to a list of relative path names from dirname for files to explicitely
    exclude from the display.  This call will replace any previously added settings
    for the same directory."""
    from proj import project
    excludes = tuple([_FilenameToLocation(e) for e in excludes])
    direntry = project.CProjectDirectory(_FilenameToLocation(dirname),
                                         recursive, filter, include_hidden,
                                         watch_disk, excludes)
    self.fProject.AddDirectory(direntry)
    
  def RemoveDirectory(self, dirname):
    """Remove the given directory, and sub-directories if it was added recursively,
    from the project."""
    self.fProject.RemoveDirectory(_FilenameToLocation(dirname))
    
  #-----------------------------------------------------------------------
  # Access to project properties
  
  def GetEnvironment(self, filename=None, set_pypath=True, overrides_only=False):
    """Get the runtime environment for the given debug file in the context
    of this project.  This is determined by overriding the inherited
    environment with any values set in Project and File Properties.  If
    the given filename is None, only the project-wide settings are used.
    If a Python Path is set in the project and set_pypath is True, it is 
    added to the environment as PYTHONPATH (overwriting any PYTHONPATH
    in the inherited environment).
    
    When overrides_only is True, this call only returns the env that is
    configured for the given file (or project-wide) and not inherited
    environment values.  This result can be used to subsequently alter
    the overrides set with SetEnvironment().
    
    Compatibility note:
    
    In Wing 3.0.0alpha1 and earlier, this function ignored any Python
    Path set in the project.  It is now added to the environment as
    PYTHONPATH by default.  To avoid this, pass set_pypath=False."""

    import proj.project

    loc = _FilenameToLocation(filename)
    
    # Returning only user changes to inherited base env
    if overrides_only:
      which, env_list = self.fSingletons.fFileAttribMgr[proj.attribs.kEnvVars, loc]
      if which == 'default' or which == 'project':
        env = {}
      else:
        env = {}
        for item in env_list:
          if item.find('=') > 0:
            key, value = item.split('=')
            env[key] = value
      if set_pypath:
        which, pypath = self.fSingletons.fFileAttribMgr[proj.attribs.kPyPath, loc]
        if which == 'custom' and pypath:
          env['PYTHONPATH'] = pypath
      return env

    # Returning whole env, including any changes in project or file properties
    env = proj.project.GetEffectiveEnvironment(loc, self.fProject)
    if set_pypath:
      pypath = proj.project.GetEffectiveValue(proj.attribs.kPyPath, loc)
      if pypath:
        env['PYTHONPATH'] = pypath
    return env
  
  def ExpandEnvVars(self, txt, filename=None):
    """Expand $(envname) and ${envname} style environment variables within the given text
    in the context of the environment returned by GetEnvironment(set_pypath=False)."""
    
    import proj.project

    loc = _FilenameToLocation(filename)
    return proj.project.ExpandEnvVars(txt, loc, self.fProject)
  
  def SetEnvironment(self, filename, base, env={}):
    """Set the runtime environment to use for the given debug file or
    for the project as a whole if filename is None.  The argument base
    indicates which environment to use as the base environment:
    
      'startup' -- Uses the startup environment
      'project' -- Uses the project environment (not a valid value when
                   filename is None)

    In either case, the given env is applied to the selected base environment
    by removing any keys with empty values and adding/updating any keys with
    non-empty values.
    
    If PYTHONPATH is included in the environment, it is stored in (or cleared
    from) the Python Path property for the file or project and not the
    Environment property."""
    
    import proj.attribs
    
    if base == 'project' and filename is None:
      raise RuntimeError("Invalid base env 'project' for project")
    
    loc = _FilenameToLocation(filename)
    if base == 'startup':
      if len(env) == 0:
        which = 'default'
      else:
        which = 'custom'
    else:
      if len(env) == 0:
        which = 'project'
      else:
        which = 'merge'

    def _makestr(t):
      if isinstance(t, unicode):
        t = t.encode(config.kFileSystemEncoding, 'replace')
      elif not isinstance(key, str):
        t = str(t)
      return t
        
    env_cvt = []
    pypath = None
    for key, value in env.items():
      key = _makestr(key)
      value = _makestr(value)
      if key == 'PYTHONPATH':
        pypath = value
      else:
        env_cvt.append("%s=%s" % (key, value))
      
    self.fSingletons.fFileAttribMgr.SetValue(proj.attribs.kEnvVars, loc, (which, env_cvt))
    
    if pypath is not None:
      if pypath == '':
        self.fSingletons.fFileAttribMgr.SetValue(proj.attribs.kPyPath, loc, ('project', ''))
      else:
        self.fSingletons.fFileAttribMgr.SetValue(proj.attribs.kPyPath, loc, ('custom', pypath))
      
  def GetPythonExecutable(self, filename):
    """Get the Python executable set for given file. Returns None if using the
    default found Python (which can be determined with CAPIApplication.FindPython()"""

    import proj.attribs
    
    loc = _FilenameToLocation(filename)
    which, pyexec = self.fSingletons.fFileAttribMgr.GetValue(proj.attribs.kPyExec, loc)
    
    if which != 'custom':
      pyexec = None
      
    return pyexec
    
  def SetPythonExecutable(self, filename, executable):
    """Set the Python executable to use when debugging the given file.  The
    filename may be None to set the project-wide value.  The executable may
    be None to use the default Python."""

    import proj.attribs
    
    if executable is None:
      if filename is None:
        which = 'default'
      else:
        which = 'project'
      executable = ''
    else:
      which = 'custom'
      
    loc = _FilenameToLocation(filename)
    self.fSingletons.fFileAttribMgr.SetValue(proj.attribs.kPyExec, loc, (which, executable))
    
  def GetRunArguments(self, filename):
    """Get the run arguments for debugging the given file, or '' if none.
    The filename should not be None."""
    
    import debug.client.attribs
    
    loc = _FilenameToLocation(filename)
    return self.fSingletons.fFileAttribMgr.GetValue(debug.client.attribs.kRunArgs, loc)

  def SetRunArguments(self, filename, args, add_recent=True):
    """Set the run arguments (as a string) for debugging the given file. Use
    None for no arguments."""
    
    import debug.client.attribs
    import proj.properties

    if args is None:
      args = ''
      
    loc = _FilenameToLocation(filename)
    self.fSingletons.fFileAttribMgr.SetValue(debug.client.attribs.kRunArgs, loc, args)
    if args != '' and add_recent:
      proj.properties.UpdateRecentRunArgs(gApplication.fSingletons.fFileAttribMgr,
                                          loc, args)

  def GetInitialDirectory(self, filename):
    """Get the initial directory for debugging the given file. Filename may be
    None to set the project-wide setting. The dirname may be None to use the
    startup directory."""
    
    import proj.attribs
    
    loc = _FilenameToLocation(filename)
    which, dirname = self.fSingletons.fFileAttribMgr.GetValue(proj.attribs.kInitialDir, loc)
    
    if which != 'custom':
      dirname = None
      
    return dirname
    
  def SetInitialDirectory(self, filename, dirname):
    """Set the initial directory to use when debugging the given file.  
    Filename may be None to set the project-wide setting.  The dirname
    may be None to use the startup directory."""
    
    import proj.attribs
    
    if dirname is None:
      if filename is None:
        which = 'default'
      else:
        which = 'project'
      dirname = ''
    else:
      which = 'custom'
      
    loc = _FilenameToLocation(filename)
    self.fSingletons.fFileAttribMgr.SetValue(proj.attribs.kInitialDir, loc, (which, dirname))
    
  def GetMainDebugFile(self):
    """Get the full path for the main debug file for this project, or None if
    there is none"""

    import proj.attribs
    loc = self.fSingletons.fFileAttribMgr[proj.attribs.kMainDebugFile]
    return _LocationToFilename(loc)
  
  def SetMainDebugFile(self, filename):
    """Set the main debug file for this project.  Pass in a full path or None to
    unset the main debug file."""
    
    loc = _FilenameToLocation(filename)
    import proj.attribs
    self.fSingletons.fFileAttribMgr[proj.attribs.kMainDebugFile] = loc
  
  #-----------------------------------------------------------------------
  # Access to attributes:  Use these to store information in a project file
    
  def GetAttribute(self, attrib_name, filename=None):
    """Get value for given named attribute. The attribute name is uniquified
    internally so avoid conflicts between scripts. If this is not desired, so
    that other scripts can access the attribute, prefix the attrib_name with
    '.'. If filename is not None, the attribute is a per-file attribute.
    Otherwise, it's a project-wide attribute. May raise KeyError if the
    attribute is not defined."""

    import proj.attribs
    
    if filename is not None:
      loc = _FilenameToLocation(filename)
      d = self.fSingletons.fFileAttribMgr.GetValue(proj.attribs.kScriptAttributes, loc)
    else:
      d = self.fSingletons.fFileAttribMgr.GetValue(proj.attribs.kScriptAttributes)
      
    attrib_name = self.__GetUniqueAttribName(attrib_name)
    return d[attrib_name]
    
  def SetAttribute(self, attrib_name, value, filename=None):
    """Set value for given attribute.  If filename is not None, the attribute
    is a per-file attribute.  Otherwise, it's a project-wide attribute."""
    
    import proj.attribs
    
    if filename is not None:
      loc = _FilenameToLocation(filename)
      d = self.fSingletons.fFileAttribMgr.GetValue(proj.attribs.kScriptAttributes, loc)
    else:
      d = self.fSingletons.fFileAttribMgr.GetValue(proj.attribs.kScriptAttributes)
      
    attrib_name = self.__GetUniqueAttribName(attrib_name)
    d[attrib_name] = value
  
    if filename is not None:
      loc = _FilenameToLocation(filename)
      self.fSingletons.fFileAttribMgr.SetValue(proj.attribs.kScriptAttributes, loc, d)
    else:
      self.fSingletons.fFileAttribMgr.SetValue(proj.attribs.kScriptAttributes, d)
    
  def __GetUniqueAttribName(self, attrib_name):
    """Utility to get unique attribute name"""
    
    if attrib_name.startswith('.'):
      return attrib_name
    
    f = sys._getframe()
    api_fn = f.f_code.co_filename
    while f is not None and f.f_code.co_filename == api_fn:
      f = f.f_back
    if f is None:
      return attrib_name
    
    script_name = os.path.basename(f.f_code.co_filename)
    if script_name.endswith('.pyc') or script_name.endswith('.pyo'):
      script_name = script_name[:-4]
    elif script_name.endswith('.py'):
      script_name = script_name[:-3]
      
    attrib_name = script_name + '.' + attrib_name
    return attrib_name
    
  #------------------------------------------------------------------------
  # Internal support for signal forwarding

  def __FilesAdded(self):
    def files_added(proj, locs):
      filenames = [_LocationToFilename(l) for l in locs]
      self.emit('files-added', filenames)
    id = self.fProject.connect('files-added', files_added)
    return self.fProject, id
  
  def __FilesRemoved(self):
    def files_removed(proj, locs):
      filenames = [_LocationToFilename(l) for l in locs]
      self.emit('files-removed', filenames)
    id = self.fProject.connect('files-removed', files_removed)
    return self.fProject, id
    
  _fSignalForwarders = {
    'files-added': __FilesAdded,
    'files-removed': __FilesRemoved,
  }

###########################################################################
# Debugger API
###########################################################################

class CAPIDebugger(_CAPIBase):
  """ API to access the debugger """
  
  def __init__(self, singletons, debugger):
    """ Constructor.  Signals supported:

      new-runstate              -- A new runstate has been created. Args = (runstate,)
      current-runstate-changed  -- A new runstate has been selected as the current
                                   runstate.  Args = (runstate,)

    """
    
    _CAPIBase.__init__(self, singletons, debugger, ('new-runstate', 
                                                    'current-runstate-changed'))
    self.fDebugger = debugger
    
  def GetRunStates(self):
    """Get the list of run states in the debugger."""

    if len(self.fDebugger.fRunStates) == 0:
      return []
    wrs = [CAPIDebugRunState._GetWrapper(rs) for rs in self.fDebugger.fRunStates]
    return wrs
  
  def GetCurrentRunState(self):
    """Get the currently active run state"""

    rs = self.fDebugger.CurrentRunState()
    if rs is None:
      return None
    wrap = CAPIDebugRunState._GetWrapper(rs)
    isinstance(wrap, CAPIDebugRunState)
    return wrap
  
  #------------------------------------------------------------------------
  # Internal support for signal forwarding

  def __NewRunstate(self):
    def new_runstate(rs):
      if rs is not None:
        wrs = CAPIDebugRunState._GetWrapper(rs)
      else:
        wrs = None
      self.emit('new-runstate', wrs)
    id = self.fDebugger.connect('new-runstate', new_runstate)
    return self.fDebugger, id
    
  def __RunstateChanged(self):
    def runstate_changed(rs):
      if rs is not None:
        wrs = CAPIDebugRunState._GetWrapper(rs)
      else:
        wrs = None
      self.emit('current-runstate-changed', wrs)
    id = self.fDebugger.connect('current-runstate-changed', runstate_changed)
    return self.fDebugger, id
  
  _fSignalForwarders = {'new-runstate': __NewRunstate,
                        'runstate-changed': __RunstateChanged,
                      }

import debug.client.constants

class CAPIDebugRunState(_CAPIBase):
  """ API to access an individual run state in the debugger.  A run state is
  associated with a single debug program.  It is created before any debug 
  process is started, takes care of starting and controlling individual
  debug sessions, and outlives individual debug processes."""
  
  def __init__(self, singletons, runstate):
    """ Constructor.  Signals supported:
      
      debug-started         -- Debug session has been started.  Args = None
      debug-terminated      -- Debug session ended.  Args = None
      exception             -- Debug process has encountered an exception.  Args = None
      paused                -- Debug process has paused or reached a breakpoint.  Args = None
      running               -- Debug process has started running again.  Args = None

    """
    _CAPIBase.__init__(self, singletons, runstate,
                       ('debug-started', 'debug-terminated', 'exception', 'paused', 'running',))
    self.fRunState = runstate
    self.fBpMgr = self.fSingletons.fDebugMgr.fBpMgr
    
  def Run(self, filename, stop_on_first=0):
    """ Run using given file as entry point, optionally stopping on the first
    line of code """
    from debug.client import runstate
    loc = _FilenameToLocation(filename)
    launcher = runstate.CDebugProcessLauncher(self.fRunState)
    launcher.SetFirstStop(stop_on_first)
    launcher.SetShowDialog(False)
    launcher.SetLoc(loc)
    launcher.Launch()

  def Step(self, over=1, out=0):
    """ Step in the code, either over or into the current line or out
    of the current scope (out=1 overrides any value for over) """
    if out:
      self.fRunState.StepOut()
    elif over:
      self.fRunState.StepOver()
    else:
      self.fRunState.StepInto()

  def RunToCursor(self):
    """ Run until the current cursor location is reached, or to next
    breakpoint or end of program """
    self.fRunState.RunToCursor()
  
  def Continue(self):
    """Contine running the debug process to next breakpoint, exception, or
    termination."""
    self.fRunState.Continue()
    
  def GetProcessID(self):
    """Get the process ID of the debug process for this run state.  Returns
    None if there is no active process anymore."""
    if self.fRunState._fActiveInfo.process is None:
      return None
    else:
      return self.fRunState._fActiveInfo.process.GetPid()
    
  def GetThreads(self):
    """Get list of (threads ids, fct/method name, running) for this run state.
    This is None if the process is not paused at a breakpoint or exception.
    The currently selected thread can be determined by called
    GetStackFrame()."""
    if self.fRunState._fRunMode not in (debug.client.constants.kException,
                                        debug.client.constants.kStopped):
      return None
    return self.fRunState._fServer.GetThreads()
  
  def GetStackFrame(self):
    """Get the currently selected thread id (None if no thread is paused or
    stopped at a breakpoint or exception) and stack frame index (0=outermost
    frame, or None if the thread is still running) """
    if self.fRunState._fRunMode not in (debug.client.constants.kException,
                                        debug.client.constants.kStopped):
      return None, None
    return self.fRunState._fServer.GetCurrentIndex()
  
  def SetStackFrame(self, thread_id, idx):
    """Set the currently selected thread (None to use current thread) and
    stack frame index (0=outermost frame, or None to use currently selected
    frame in given thread). Returns the actual thread id and index set.
    A returned thread id of None indicates no thread is stopped and a
    frame index of None indicates that the selected thread has not stopped."""
    if self.fRunState._fRunMode not in (debug.client.constants.kStopped,
                                        debug.client.constants.kException):
      return None, None
    return self.fRunState._fServer.FrameSet(thread_id, idx, force=True)
  
  def GetStack(self):
    """Get the stack for the currently selected debug thread as a list of
    frames each of which is a tuple containing (filename, lineno, line_text,
    scope, local_varnames). Line numbers are zero based (0=first line in
    file). Returns None instead if the currently selected thread is not
    paused, at a breakpoint, or at an exception. The currently selected thread
    is changed by called SetStackFrame()."""
    if self.fRunState._fRunMode not in (debug.client.constants.kStopped,
                                        debug.client.constants.kException):
      return None
    stack = self.fRunState._fServer.GetCurrentStack()
    if stack is None:
      return None
    return [s[0] for s in stack]
      
  def SetBreak(self, filename, lineno, temp=0, cond=None, enable=1, ignore=0):
    """ Set breakpoint at given line of the given file (0=first line). If a
    breakpoint already exists here, it is replaced. Set temp to create a
    one-time breakpoint, cond to a conditional string for a conditional
    breakpoint, enable=0 to disable, and ignore to the number of hits on the
    breakpoint that should be ignored. Returns (lineno, err) where lineno is
    the actual line the breakpoint was placed at and err is either None or an
    error string. """
    loc = _FilenameToLocation(filename)
    return self.fBpMgr.BreakInsert(loc, lineno, temp, cond,
                                   enable, ignore, suppress_errors=1)

  def ClearBreak(self, filename, lineno):
    """ Clear the breakpoint at given lineno (0=first in file) """
    loc = _FilenameToLocation(filename)
    self.fBpMgr.BreakClear(loc, lineno)
  
  def ClearAllBreaks(self):
    """ Clear all breakpoints """
    self.fBpMgr.BreakClearAll()
  
  def Kill(self):
    """ Stop debugging """
    self.fRunState.Kill()
  
  #------------------------------------------------------------------------
  # Internal support for signal forwarding

  def __DebugStarted(self):
    def debug_started(debugging):
      if debugging:
        self.emit('debug-started')
    id = self.fRunState.connect('debugging-changed', debug_started)
    return self.fRunState, id

  def __DebugTerminated(self):
    def debug_terminated(debugging):
      if not debugging:
        self.emit('debug-terminated')
    id = self.fRunState.connect('debugging-changed', debug_terminated)
    return self.fRunState, id

  def __DebugException(self):
    def debug_exception(mode):
      if mode == debug.client.constants.kException:
        self.emit('exception')
    id = self.fRunState.connect('run-mode-changed', debug_exception)
    return self.fRunState, id

  def __DebugPaused(self):
    def debug_paused(mode):
      if mode == debug.client.constants.kStopped:
        self.emit('paused')
    id = self.fRunState.connect('run-mode-changed', debug_paused)
    return self.fRunState, id

  def __DebugRunning(self):
    def debug_running(mode):
      if mode == debug.client.constants.kRunning:
        self.emit('running')
    id = self.fRunState.connect('run-mode-changed', debug_running)
    return self.fRunState, id
    
  def __StackFrameChanged(self):
    def frame_changed(thread_id, idx):
      self.emit('stack-frame-changed')
    id = self.fRunState.connect('frame-changed', frame_changed)
    return self.fRunState, id
    
  _fSignalForwarders = {'debug-started': __DebugStarted,
                        'debug-terminated': __DebugTerminated,
                        'exception': __DebugException,
                        'paused': __DebugPaused,
                        'running': __DebugRunning,
                        'stack-frame-changed': __StackFrameChanged,
                      }
  
###########################################################################
# Search API
###########################################################################

class CAPISearch(_CAPIBase):
  """API for searching files and directories.  Arguments are:
  
  txt                  -- The text to search for
  match_case           -- True for case-sensitive search (default=True)
  whole_words          -- True to match only whole words (default=False)
  omit_binary          -- True to omit files that appear to be binary files
                          (default=True)
  search_styles        -- One of 'text' for plain text search, 'wildcard' for 
                          wild card matches (unix glob style matching), 
                          and 'regex' for regular expression matching
                          (default='text')
  include_linenos      -- True to include line numbers in the results (when
                          set to 0, line numbers are not computed, which makes 
                          for faster searching) (default=False)
  use_buffer_content   -- True to use the content of edited buffers instead of
                          the disk file when unsaved edits exist (default=True)
  regex_flags          -- For regex searches: the regex flags from the re module
                          (default=0)
  
  After constructor is called, use *one* of the following to start
  searching:
    
  SearchDirectory()
  SearchFiles()
  
  Additionally, SearchFile() may be used to re-search a file, e.g.
  to respond to changes in the file.
  
  Results and search status are reported via the following signals:

  start         -- A new search was started
  end           -- Search complete or aborted
  match         -- One or more matches seen.  Args:  filename, list of 
                   (lineno, linestart, line_text, matches) where linestart
                   is the position in the file where the line begins and 
                   matches is a list of (start, end) tuples.  Note that the 
                   match signal may occur more than once per line, delivering
                   each time one or more additional matches on that line.
                   Line numbers are zero unless include_lineos is True.
                   All positions are from the start of the file.
  dir           -- Scanning a new directory.  Args: filename
  scanning      -- Scanning a new file.  Args: filename
  file-done     -- Done scanning a file.  Args: filename.
  
  """
  
  def __init__(self, txt, match_case=True, whole_words=False,
               omit_binary=True, search_style='text', include_linenos=False,
               use_buffer_content=True, regex_flags=0):
    
    import singleton
    import search.common
    import wingutils.search
    
    _CAPIBase.__init__(self, singleton.GetSingletons(), self,
                       ('start', 'end', 'match', 'dir', 'scanning', 'file-done'))
    
    if use_buffer_content:
      opener_cb = search.common.GetStreamForLocation
    else:
      opener_cb = None

    self.fSearch = wingutils.search.CMultiFileSearcher(txt, match_case, whole_words, 
                                                       omit_binary, search_style, 
                                                       include_linenos, opener_cb,
                                                       0, None, regex_flags)
    
  #-----------------------------------------------------------------------
  def SearchDirectory(self, dirname, file_set, recursive):
    """ Start searching the given directory for all files that match the file
    set, optionally recursively. 
    
    The file_set can either be a name of a file set stored in the
    'main.file-sets' preference or (includes, excludes) where includes and
    excludes are lists of tuples (spec_type, text) and spec_type is one of
    'wildcard-filename', 'mime-type', or 'wildcard-directory'.
    
    For example, to search only Python files use 'Python Files' as the
    filespec. Or to search *.py files other than those within a directory
    named 'test', use the following:
    
      [(('wildcard-filename', '*.py'),), (('wildcard-directory', 'test'),)]
    
    If the file set is a string, an exception will be raised if it is 
    not a valid file set name."""

    if isinstance(file_set, str) or isinstance(file_set, unicode):
      import mainprefs
      import wingutils.fileset
      filesets = self.fSingletons.fPrefMgr[mainprefs.kFileSets]
      file_set = filesets[file_set]
  
    loc = _FilenameToLocation(dirname)
    self.fSearch.SearchDirectory(loc, file_set, recursive)

  #-----------------------------------------------------------------------
  def SearchFiles(self, files):
    """ Start searching all given files """

    file_locs = (_FilenameToLocation(f) for f in files)
    self.fSearch.SearchFiles(file_locs)
      
  #-----------------------------------------------------------------------
  def SearchFile(self, filename, start_pos=0):
    """ Search a single file for search matches, optionally starting at
    a given point."""

    loc = _FilenameToLocation(filename)
    self.fSearch.SearchFile(loc, start_pos)

  #----------------------------------------------------------------------
  def Stop(self):
    """ Terminate searching, if a search is active"""
    
    self.fSearch.Stop()
    
  #-----------------------------------------------------------------------
  def Pause(self):
    """ Pause the search process """

    self.fSearch.Pause()
      
  #-----------------------------------------------------------------------
  def Continue(self):
    """ Continue the search process """

    self.fSearch.Continue()
      
  #------------------------------------------------------------------------
  # Internal support for signal forwarding

  def __Start(self):
    def start():
      self.emit('start')
    id = self.fSearch.connect('start', start)
    return self, id
  
  def __End(self):
    def end():
      self.emit('end')
    id = self.fSearch.connect('end', end)
    return self, id
  
  def __Match(self):
    def match(loc, matches):
      m_tuples = [(m.lineno, m.start_offset, m.text, m.positions)
                  for m in matches]
      self.emit('match', _LocationToFilename(loc), m_tuples)
    id = self.fSearch.connect('match', match)
    return self.fSearch, id
    
  def __Dir(self):
    def dir(loc):
      self.emit('dir', _LocationToFilename(loc))
    id = self.fSearch.connect('dir', dir)
    return self.fSearch, id
  
  def __Scanning(self):
    def scanning(loc):
      self.emit('scanning', _LocationToFilename(loc))
    id = self.fSearch.connect('scanning', scanning)
    return self.fSearch, id
  
  def __FileDone(self):
    def file_done(loc):
      self.emit('file-done', _LocationToFilename(loc))
    id = self.fSearch.connect('file-done', file_done)
    return self.fSearch, id
  
  _fSignalForwarders = {
    'start': __Start,
    'end': __End,
    'match': __Match,
    'dir': __Dir,
    'scanning': __Scanning,
    'file-done': __FileDone,
  }


###########################################################################
# Source code analysis API
###########################################################################

class CAPISymbolInfo:
  """API to describe the inferred type for a particular source symbol.
  Type information is accessed with the following attributes:
  
  generalType -- General type of the symbol:  One of class, method, 
    function, instance, keyword, literal, or module
  typeName -- The full name of the type 
  fileName -- The file where the type is defined
  lineStart -- The first line of the type definition (0=first line in file)
  lineCount -- The number of lines taken up by the type definition
  pos -- The position of the type definition within the first line
  isCallable -- True when the symbol is a callable
  args -- A list of argument names, if callable
  docString -- The docstring for the type

  """
  
  def __init__(self, t):

    import pysource.inferencer

    if isinstance(t, pysource.inferencer.CClassValueInfo):
      self.generalType = 'class'
    elif isinstance(t, pysource.inferencer.CMethodValueInfo):
      self.generalType = 'method'
    elif isinstance(t, pysource.inferencer.CFunctionValueInfo):
      self.generalType = 'function'
    elif isinstance(t, pysource.inferencer.CInstanceValueInfo):
      self.generalType = 'instance'
    elif isinstance(t, pysource.inferencer.CKeywordValueInfo):
      self.generalType = 'keyword'
    elif isinstance(t, pysource.inferencer.CLiteralValueInfo):
      self.generalType = 'literal'
    elif isinstance(t, pysource.inferencer.CModuleValueInfo):
      self.generalType = 'module'
    else:
      raise NotImplemented, 'unexpected value type'
      
    self.typeName = t.fFullName
    
    loc, logical, self.lineStart, line_end, self.pos = t.GetPointOfDefinition()
    self.lineCount = line_end - self.lineStart
    self.fileName = _LocationToFilename(loc)
    
    self.args = t.GetArguments()
    self.docString = t.GetDocString()
    self.isCallable = t.IsCallable()

class CAPIStaticAnalysis(_CAPIBase):
  """API for inspecting the contents of a Python file, based on Wing's
  static analysis of that file"""
  
  #-----------------------------------------------------------------------
  def __init__(self, singletons, ana):
    _CAPIBase.__init__(self, singletons, ana)
    self.fAnalysis = ana
    
  #----------------------------------------------------------------------
  def GetScopeContents(self, scope, timeout=0.5):
    """Get a list of all the symbols defined in the given scope.  
    
    A scope of '' is the module-level scope. A nested scope is specified in
    the form MyClass.MyMethod.nested_def
    
    Set timeout to specify the maximum computation time in seconds.

    The return value is a dictionary mapping the symbol name to a sequence
    of strings describing the symbol:
    
      imported -- The symbol is imported from another module
      class -- The symbol is a class
      method -- The symbol is a method
      function -- The symbol is a function
      argument -- The symbol is a function or method argument
      module -- The symbol is a module
      package -- The symbol is a package
      
    Use GetSymbolInfo() to obtain additional information about a symbol,
    including its inferred type and point of definition.
      
    """
    
    _kAllowedDomainValues = frozenset([
      'imported', 'module', 'package', 'class', 'method', 'function',
      'argument',
      ])
    
    engine = self.fSingletons.fAnalysisMgr.fInferenceEngine
    ctx = engine.NewContext(time.time() + timeout)
    all_probs = engine._GetAllScopeProbables(self.fAnalysis.fLocation,
                                             scope, ctx=ctx)
    for symbol, (probs, domains) in all_probs.iteritems():
      all_probs[symbol] = tuple(d for d in domains
                                if d in _kAllowedDomainValues)
      
    return all_probs
       
  #-----------------------------------------------------------------------
  def FindScopeContainingLine(self, lineno):
    """ Find the scope containing the given line number.  Note that a
    class line or a def line is in its parent's scope."""

    return self.fAnalysis.FindScopeContainingLine(lineno)
  
  #----------------------------------------------------------------------
  def GetSymbolInfo(self, scope, symbol):
    """Get extended information for the given symbol within the named scope.
    A scope of '' and symbol of '' obtains type information for the module
    as a whole.  Returns a sequence of CAPISymbolInfo instances."""
      
    retval = []
    
    for info in self.fAnalysis.GetProbableTypes(symbol, scope):
      retval.append(CAPISymbolInfo(info))
    
    return retval
  