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

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

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

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

import os
import sys
import inspect
import time

import edit.editor
import command.commandmgr

from guiutils import wgtk

import config
from wingbase import destroyable
from wingbase import location
from wingbase import textutils

from pysource import symbolinfo
from proj import project

import logging
from wingbase import searchutils
logger = logging.getLogger('scripts')


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

# Revision number for the API. Increases only when there are non-forward-compatible
# changes, such as removal of deprecated functionality.
kAPIVersion = 2

# Default value for arguments that should use a numeric modifier entered by the user when
# executing a command. This is only used for key bindings that allow typing a numeric
# modifier before commands (such as VI/VIM and Emacs). Otherwise, the default passed when
# instantiating the ``kArgNumericModifier`` instance is used, or the 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. This tells Wing how to
# collect non-default argument values from the user. The constructor arguments are::
#
#   doc      -- Documentation string describing the argument
#   type     -- Data type for the argument; sub-class of ``wingutils.datatype.CTypeDef``
#   formlet  -- Form model for arg collection; subclass of ``wingutils.formbuilder.CDataGui``
#   label    -- (Optional) Label to display in argument collection
#
from command.commandmgr import CArgInfo

# These define places in the user interface where commands can be made available. They are
# used 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

class kContextCommonMenu:
  """Adds new menu in the common actions menu"""
  
  def __init__(self, group=0):
    self.name = "Common"
    self.group = group
  
#@! utilities

#@ A Note on Filenames
#@ -------------------
#@
#@ File names in the API may either be the name of a local file on disk or a URL for
#@ untitled or remote files.

def IsUrl(filename):
  """Tests whether the given ``filename`` is a URL. Use this on filenames obtained from
  the API to determine how to treat them.
  
  When this returns ``False``, the ``filename`` is a local file name.
  
  Otherwise, the ``filename`` is a URL in the one of the following forms:
  
  * Untitled buffers use filenames starting with ``unknown:``  For example,
    ``unknown:untitled-1.py`` and ``unknown:Scratch`` both refer to an unsaved file.
  
  * Remote files and directories use filenames in the form ``ssh://hostname/path/to/item``
    where ``hostname`` is the ``Identifier`` in a `Remote Host <ref:proj/remote-hosts>`__.
  """
  
  if sys.version_info < (3, 0):
    import urlparse
  else:
    from urllib import parse as urlparse
    
  try:
    p = urlparse.urlparse(filename)
    return p[0] in location.kURISchemes
  except:
    return True

def _LocationToFilename(loc):
  """Used internally to convert location objects into full path file names or URLs.
  Untitled and scratch buffer file names are prefixed with 'unknown:' and remote
  files and directories are prefixed ``ssh:``"""
  
  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 names and URLs to internally used location objects"""
  
  if filename is None:
    return None
  else:
    return location.CreateFromName(location.SanitizeUrl(filename))
  
def _apicall(func):
  """Decorator used internally to track API usage"""
  
  import functools
  def wrapper(self, *args, **kw):
    import singleton
    s = singleton.GetSingletons()
    s.fUseMgr.Count('api.%s.%s' % (self.__class__.__name__, func.__name__))
    return func(self, *args, **kw)
  functools.update_wrapper(wrapper, func)
  return wrapper
  
#@! application

###########################################################################
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', wgtk.NoArgCallback(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 list(self.__fObjSignalConnections.items()):
        if self.num_catchers(signal_name) == 0:
          obj, id = connection_info
          obj.disconnect(id)
          del self.__fObjSignalConnections[signal_name]

  @_apicall
  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)
  
  @_apicall
  def Disconnect(self, id):
    """ Disconnect previously connected signal handler"""

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

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

# This will be set to the CAPIApplication instance before any script is loaded
gApplication = None

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

class CAPIApplication(_CAPIBase):
  """API for the top-level of IDE functionality. This should be accessed through
  ``wingapi.gApplication``.
  
  **Signals**
    
  A callback can be connected to the following signals using ``Connect(signal, cb)``:
  
    ``destroy``: The application is closing.  Calls ``cb(app:CAPIApplication)``
    
    ``editor-open``: An editor was opened.  Calls ``cb(editor: CAPIEditor)``
    
    ``document-open``: A document was opened.  Note that several editors may share
    a document.  Calls ``cb(doc:CAPIDocument)``
      
    ``project-open``: A project was opened.  Calls ``cb(filename:str)``
    
    ``project-close``: A project was closed.   Calls ``cb(filename:str)``
    
    ``active-editor-changed``: Active editor has changed.  Calls ``cb(editor:CAPIEditor)``
    
    ``active-window-changed``: Active window has changed.  The window is None
    if Wing is no longer at front.  Calls ``cb(window_name:str)``
      
    ``perspective-changed``: Current perspective has been changed.  Calls
    ``cb(perspective_name:str)``
    
    ``python-runtime-changed``: The effective Python version or installation
    being used by the currently open project has changed.  Calls ``cb()``.
  
  Use ``Disconnect(signal_id)`` to preemptively disconnect a signal handler, where
  ``signal_id`` is the signal ID previously returned from ``Connect``.

  """

  def __init__(self, singletons, app):
    """Constructor for this class"""
    
    _CAPIBase.__init__(self, singletons, app,
                       ('editor-open', 'document-open',
                        'project-open', 'project-close', 
                        'active-editor-changed',
                        'active-window-changed',
                        'perspective-changed',
                        'python-runtime-changed'))
      
  #------------------------------------------------------------------------
  #@ Top-level Settings and Environment
    
  @_apicall
  def GetProductInfo(self):
    """ Returns the current Wing version, update, product code, product name, and
    release date.
    
    If the full version of Wing is 1.2.3.4 then version will be '1.2.3' and update
    will be '4'.  If the current version is a pre-release then update may be prepended
    with one letter as follows::
    
      'a': alpha release
      'b': beta release
      'c': release candidate
        
    The valid product codes and names are::
      
      0x00000001 'Personal'
      0x00000002 'Professional'
      0x00000008 '101'
 
    Example return values:
    
    .. python::
      
      ('5.1.3', '2', 0x00000002, 'Professional', 'Mar 17, 2014')
      ('7.0.0', 'a1', 0x00000001, 'Personal', 'Aug 1, 2018')
    
    """
    
    if config.kProduct == 'Pro':
      prod = 'Professional'
    else:
      prod = config.kProduct
      
    return (config.kVersion, config.kBuild, config.kProductCode, prod, 
            config.kReleaseDate)
  
  @_apicall
  def GetWingHome(self):
    """Returns the Install Directory from which Wing is running."""
    
    return config.kWingHome
  
  @_apicall
  def GetUserSettingsDir(self):
    """Returns the active `User Settings <ref:install/user-settings-dir>`__ directory."""
    
    return config.kUserSettings

  @_apicall
  def GetStartingDirectory(self, force_local=True):
    """Get the most logical starting directory to use when browsing for files or
    directories. This varies based on the focus and selection on the user interface. When
    ``force_local`` is True, only a local starting directory is returned. Otherwise a
    starting directory on a remote host may be returned as a URL in the form
    ``ssh://hostname/path/to/dirname``, where ``hostname`` is the ``Identifier`` of a
    `Remote Host <ref:proj/remote-hosts>`__. Use ``IsUrl`` to distinguish
    urls from directory names."""
    
    return self.fSingletons.fWingIDEApp.GetStartingDirLoc(force_local=force_local)
  
  @_apicall
  def FindPython(self):
    r""" Find the default Python interpreter that Wing will use if none is specified with
    `Python Executable <script:view_project_properties#highlighted_attrib="proj.pyexec">`__
    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.7``)
    - As a last resort, use the last known working Python if there was one
    
    On macOS:
    
    - Use ``/Library/Frameworks/Python.framework/Versions/Current/bin/python`` 
      if is exists and is valid
    - Search as for Linux
    
    On Windows:
  
    - Try ``python`` in the current environment
    - Look for the latest version in the registry using the keys
      ``HKEY_CURRENT_USER\SOFTWARE\PYTHON\PYTHONCORE\#.#\INSTALLPATH`` and
      ``HKEY_LOCAL_MACHINE\SOFTWARE\PYTHON\PYTHONCORE\#.#\INSTALLPATH``
    - As a last resort, use the last known working Python if there was one
    
    **Return Value**: 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 support.
    
    """
    
    return self.fSingletons.fInterpMgr.FindLocalPython()    

  #------------------------------------------------------------------------
  #@ Command Execution
  #@ -----------------
  #@
  #@ These methods are used to execute IDE commands that are documented in the `Command
  #@ Reference <ref:commands/index>`__.
    
  @_apicall
  def CommandAvailable(self, cmd, **args):
    """Check whether a command is available for execution.
    
    The ``cmd`` can be the name of a command in Wing's `Command Reference
    <ref:commands/index>`__, or the name of a command added by an extension script.
    
    Any arguments are passed as keyword arguments using the documented argument names for
    the command being invoked. In most cases the value of optional arguments won't affect
    command availability, so they may usually be omitted."""
    
    return self.fSingletons.fCmdMgr.CommandAvailable(cmd, **args)
    
  @_apicall
  def ExecuteCommand(self, cmd, **args):
    """Execute a command with the given keyword arguments.
    
    The ``cmd`` can be the name of a command in Wing's `Command Reference
    <ref:commands/index>`__, or the name of a command added by an extension script.
    
    Any arguments are passed as keyword arguments using the documented argument names
    for the command being invoked.
    
    To execute an external command or command line, use ``ExecuteCommandLine``,
    ``AsyncExecuteCommandLine*``, or ``ExecuteOSCommand`` instead."""
    
    self.fSingletons.fCmdMgr.Execute(cmd, **args)
    
  #------------------------------------------------------------------------
  #@ Asynchronous Timeouts

  @_apicall
  def InstallTimeout(self, timeout, fct):
    """Install a function to be called as a timeout after a given number of milliseconds.
    The function is called repeatedly at the given interval until its return value
    evaluates to ``False`` or ``None``.
    
    Returns a ``timeout_id`` that may be sent to ``RemoveTimeout`` to remove the timeout
    prematurely.
    
    Note that the timeout will be removed if its script module is reloaded, in order to
    avoid calling old byte code. For this reason, script modules must reinstall timeouts
    during initialization."""

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

  @_apicall
  def RemoveTimeout(self, timeout_id):
    """Remove a timeout previously installed with ``InstallTimeout``."""
    
    self.fSingletons.fScriptMgr.RemoveTimeout(timeout_id)
    
  #------------------------------------------------------------------------
  #@ Access to Key Objects
  
  @_apicall
  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()

  @_apicall
  def GetActiveEditor(self):
    """Get the currently active ``CAPIEditor`` or ``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

  @_apicall
  def GetActiveDocument(self):
    """Get the ``CAPIDocument`` for the currently active editor, or 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 = CAPIDocument._GetWrapper(editor.fCache)
    isinstance(wrap, CAPIDocument)
    return wrap

  @_apicall
  def GetCurrentFiles(self):
    """Get a list of the the currently selected files. The list returned depends on the
    current focus and selection in the user interface. Files may be selected in the
    current editor, or in the ``Project``, ``Source Browser``, and other tools.
    
    Returns a list of full path filenames for the file or files, or ``None`` if none are
    selected.
    
    For untitled or scratch buffers, the file name is prefixed with ``unknown:``. For
    remote files, the file name will be a URL. Use ``IsUrl`` to distinguish between file
    names and URLs."""
    
    scopes = self.fSingletons.fGuiMgr.GetCurrentSourceScopes()
    if scopes is None:
      return None
    else:
      return [_LocationToFilename(s[0]) for s in scopes]

  @_apicall
  def GetCurrentSourceScopes(self):
    """Get the current source scopes, including file name, line number, and Python scope
    name. The value returned depends on the current focus and selection in the user
    interface. Source scopes may be selected in the current editor, or in the ``Project``,
    ``Source Browser``, or other tools.
    
    Returns ``None`` if nothing is selected or a list of scopes, each of which is a list
    that contains a filename, a line number (0=first), and zero or more source symbol
    names indicating the nested scope that the user has selected.
    
    For example, if ``Class1.Method1`` on line ``120`` of the file ``/x/y/z.py`` is selected,
    the return value would be:
    
    .. python::
    
      [["/x/y/z.py", 120, "Class1", "Method1"],]
      
    Line ``1`` is used without any source symbols to indicate the whole file is selected.
    The following would be returned if multiple items in the ``Project`` or ``Open Files``
    tools are selected:
    
    .. python::
    
      [["/x/y/a.py", 1], ["/a/b/z.py", 1]]

    For untitled or scratch buffers, the file name is prefixed with ``unknown:``. For
    remote files, the file name will be a URL. Use ``IsUrl`` to distinguish between file
    names and URLs."""
    
    loc_scopes = self.fSingletons.fGuiMgr.GetCurrentSourceScopes()
    if loc_scopes is None:
      return None

    retval = []
    for loc_scope in loc_scopes:
      sscope = [symbolinfo.SplitScopeNameFromSuffixes(n)[0] for n in loc_scope[2:]]
      retval.append([_LocationToFilename(loc_scope[0]), loc_scope[1] - 1]+sscope)
      
    return retval
  
  @_apicall
  def GetAnalysis(self, filename):
    """Get a ``CAPIStaticAnalysis`` object for the given Python file.  Returns None
    if the file is not a 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  

  @_apicall
  def GetAllFiles(self, visible_only=False, sticky_only=False):
    """Get a list of the full path names of all currently open files, whether or not a
    ``CAPIDocument`` object or editor has been created for them.
    
    Optionally filter the result to omit non-visible files or those that are opened in
    non-sticky transient mode. See `Transient Non-Sticky Editors <ref:edit/transient>`__
    for details.

    For untitled or scratch buffers, the file name is prefixed with ``unknown:``. For
    remote files, the file name will be a URL. Use ``IsUrl`` to distinguish between file
    names and URLs."""
    
    locs = self.fSingletons.fGuiMgr.GetDocuments(visible_only=visible_only,                                                 sticky_only=sticky_only)
    
    return [_LocationToFilename(loc) for loc in locs]

  @_apicall
  def GetOpenDocuments(self):
    """Get all currently open ``CAPIDocument`` objects. This includes only those documents
    that have already been shown in an editor, since documents are not created until they
    need to be shown.
    
    Note that this may also include documents active for searching or source analysis,
    for which no editor is open."""
    
    retval = []
    for cache in self.fSingletons.fCacheMgr.GetCaches():
      if cache.GetViewerCount(True):
        retval.append(CAPIDocument._GetWrapper(cache))
    return retval
  
  @_apicall
  def InspectFiles(self, filenames, result_cb, force=False):
    """Inspect all the given files, where filenames is a list of full path filenames
    or ``ssh://`` urls for remote files.  When all inspections are complete,
    result_cb(exc, result) is called where ``exc`` is True if inspection failed and
    otherwise ``result`` is a dictionary from each filename/url to a named tuple
    containing:
    
    modtime -- modification time or None if it does not exist
    readable -- True if disk permissions allow reading the file directory
    writable -- True if disk permisions allow writing to the file or directory
    bytecount -- For files, the size of the file
    files -- For directories, the names of any files in the directory
    dirs -- For directories, the name of any child directories

    Set force=True to force inspection without using cached values.
    
    """
  
    import collections
    FileInfo = collections.namedtuple('FileInfo', ['modtime', 'readable', 'writable',
                                                   'bytecount', 'files', 'dirs'])
    
    locs = [_FilenameToLocation(fn) for fn in filenames]

    def inspect_complete(exc):

      if exc:
        result_cb(True, {})
        return
      
      retval = {}
      for loc in locs:
        if loc.IsDirectory():
          files, dirs = loc.EnumerateFilesAndDirs()
          files = [l._GetBaseName() for l in files]
          dirs = [l._GetBaseName() for l in dirs]
        else:
          files = None
          dirs = None
        info = FileInfo(loc.GetLastModificationTime(), loc.IsReadable(), loc.IsWritable(),
                        loc.GetByteCount(), files, dirs)
        retval[_LocationToFilename(loc)] = info
        
      result_cb(False, retval)
    
    location._WaitForInspectMultiple(locs, inspect_complete, force=force)
    
  @_apicall
  def ReadFile(self, filename, result_cb, encoding=None):
    """Read from the given filename, which may either be a full path for a local file or
    an ``ssh://`` url for a remote file.
    
    Calls ``result_cb(err, content, encoding)`` with the result of the read where ``err``
    is an error if one occurred or None otherwise, ``content`` is the contents of the
    file, and ``encoding`` is the encoding that was used when reading the file.
    
    The ``encoding`` may optionally be set explicitly; otherwise it is inferred from the
    contents of the file, previously made user settings, and the default encoding for the
    host where the file resides.
    
    To read a binary file, set ``encoding='binary'``. In that case the ``content`` passed
    to ``result_cb`` is ``bytes`` and not ``str``.
    
    """
    
    loc = _FilenameToLocation(filename)
    location.ReadFromLocation(loc, result_cb, encoding)
  
  @_apicall
  def WriteFile(self, filename, data, result_cb, encoding='utf-8', ensure_dir=False):
    """Write to the given filename, which may either be a full path for a local file or an
    ``ssh://`` url for a remote file.
    
    Calls result_cb(err) with the result of the operation, where ``err`` is None on
    success or otherwise a string describing the error.
    
    The ``encoding`` may be set explicitly; otherwise it is inferred from the contents
    of the file, previously made user settings, and the default encoding for the
    host where the file resides.
    
    To write a binary file, set ``encoding='binary'`` and pass data as ``bytes`` and
    not ``str``.
    
    Set ``ensure_dir=True`` to case creation of the enclosing directories before
    write the file, if they do not yet exist.
    
    """
    
    loc = _FilenameToLocation(filename)
    location.WriteToLocation(loc, data, result_cb, encoding, ensure_dir)
    
  @_apicall
  def GetProject(self):
    """Get the currently open ``CAPIProject``. Returns ``None`` if no project is open."""
    
    proj = self.fSingletons.fProjMgr.fProject
    if proj is None:
      return None
    wrap = CAPIProject._GetWrapper(proj)
    isinstance(wrap, CAPIProject)
    return wrap
  
  @_apicall
  def NewProject(self, completed_cb, failure_cb=None):
    """Create a new project. ``complete_cb`` is called with no arguments when the new
    project has been created and opened and ``failure_cb`` is called with no arguments if
    the user cancels closing the current project."""
    
    self.fSingletons.fProjMgr.NewProject(completed_cb, failure_cb)
  
  @_apicall
  def GetDebugger(self):
    """Get the ``CAPIDebugger`` singleton, for access to debugger functionality."""
    
    wrap = CAPIDebugger._GetWrapper(self.fSingletons.fDebugMgr)
    isinstance(wrap, CAPIDebugger)
    return wrap
    
  @_apicall
  def ShowTool(self, name, flash=True, grab_focus=True):
    """Show the given tool in the user interface.  The most recently used instance of
    the tools is shown, or a new instance is created at its default location.
    
    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-console', 'debug-watch', debug-modules', 'python-shell', 'about', 'messages',
    'help', 'indent', bookmarks', 'testing', 'open-files', os-command', 'snippets', diff',
    'uses', 'refactoring', 'code-warnings'
    
    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)

  @_apicall
  def OpenURL(self, url):
    """Open the given URL with an external viewer."""
    
    self.fSingletons.fWingIDEApp.ExecuteURL(url)
    
  #-----------------------------------------------------------------------
  #@ Manage Windows
  
  @_apicall
  def CreateWindow(self, name):
    """Create a new window with given internal name. The window is initially blank. Use
    ``OpenEditor(``) with the given name to fill it."""
    
    self.fSingletons.fGuiMgr.CreateDocumentWindow(name)

  @_apicall
  def GetWindowEditorSplits(self, name):
    """Get the number of splits and current split for the window with the given internal name.
    Returns (num_splits, current_split) to indicate the total number of editor splits and
    current editor split in the window  (0=first or None if there is none)."""
    
    win = self.fSingletons.fGuiMgr.fWinMgr.GetNamedWindow(name)
    if win is None:
      return (0, None)
    
    from guimgr import guimanager
    multi = guimanager._GetMultiEditorForWindow(win)
    if multi is None:
      return (0, None)
    
    return (multi.NumSplits(), multi.GetSplit(multi.fActiveMultiEditor))
    
  @_apicall
  def CloseWindow(self, name, allow_cancel=True):
    """Close the window with given internal name, and all the editors in it. When
    ``allow_cancel`` is ``False``, the window is closed without prompting to save any
    changes made there. """
    
    self.fSingletons.fGuiMgr.CloseDocumentWindow(name, allow_cancel)

  #-----------------------------------------------------------------------
  #@ Manage Editors
  
  @_apicall
  def OpenEditor(self, filename, window_name=None, raise_window=False,
                 sticky=True, split_num=None):
    """Open the given file into an editor.
    
    If the ``window_name`` is given, the editor opens into the window with that internal
    name. 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.

    The window is not brought to the front unless ``raise_window`` is ``True``.
    
    Set ``sticky`` to ``False`` to cause Wing to auto-close the editor when hidden and
    more than the configured number of non-sticky editors is open. See `Transient
    Non-Sticky Editors <ref:edit/transient>`__ for details.
    
    Set ``split_num`` to the editor split in which to open the file (0=first).  When
    unspecified, the split is determined by the Editor > Split Reuse Policy preference.
    
    Returns the ``CAPIEditor`` or ``None`` if opening the file failed.
    
    Note that the file may open under a different name if symbolic links exist."""
    
    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, split_num=split_num)
    if editor is None:
      return None
    else:
      wrap = CAPIEditor._GetWrapper(editor)
      isinstance(wrap, CAPIEditor)
      return wrap

  @_apicall
  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 the given title and mime type. The document can be
    edited but will never be marked as changed or 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.
    
    ``mime_type`` sets the file type to use.  Use ``text/x-python`` for Python.
    
    The window is raised only if ``raise_window`` is True.
    
    The view is brought to front within the window only if ``raise_view`` is True.
    
    Set ``sticky`` to ``False`` to cause Wing to auto-close the editor when hidden and
    more than the configured number of non-sticky editors is open. See `Transient
    Non-Sticky Editors <ref:edit/transient>`__ for details.
    
    If the ``window_name`` is given, the editor opens into the window with that internal
    name. 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.
    
    Returns the ``CAPIEditor`` or ``None`` if the scratch buffer failed to create."""

    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

  @_apicall
  def GetMimeType(self, filename):
    """Get the mime type Wing is using for the given filename, based on the file name,
    contents, and `Files > File Types > Extra File Types
    <script:show_preferences_gui#prefname="main.extra-mime-types">`__ preference."""
    
    loc = location.CreateFromName(filename)
    return self.fSingletons.fFileAttribMgr.GetProbableMimeType(loc)

  #-----------------------------------------------------------------------
  #@ Clipboard
  
  @_apicall
  def SetClipboard(self, txt):
    """Store the given text to the clipboard. The text should be a utf-8 string or unicode
    object."""
    
    txt = textutils.AsUnicode(txt)
    clip = wgtk.GetClipboardAccess()
    clip.StoreUnicode(txt)
    
  @_apicall
  def GetClipboard(self):
    """Get the text currently on the clipboard, as a unicode string."""
    
    clip = wgtk.GetClipboardAccess()
    txt = clip.RetrieveUnicode()
    if txt is None:
      return ''
    else:
      return txt
    
  #------------------------------------------------------------------------
  #@ Application State
  
  @_apicall
  def GetVisualState(self, errs=[], style='all'):
    """Get the application's visual state.
    
    The ``style`` of the state may be one of:
    
      'all' to capture all of the application visual state
      
      'tools-and-editors' to capture which tools are visible, the overall layout of the
      windows, and which editors are open (but not details like scroll positions,
      selection, or current search string)
      
      'tools-only' to capture only which tools are visible and the overall layout of the
      windows (but not which editors are open).
    
    Returns an opaque dictionary with the state, for later use with ``SetVisualState``.
    
    Any errors encountered are added to ``errs`` as strings."""
    
    if style == 'all':
      constraint = None
    else:
      constraint = 'perspective_%s' % style.replace('-', '_')
    return self.fSingletons.fGuiMgr.GetVisualState(errs, constraint)
    
  @_apicall
  def SetVisualState(self, state, errs=[]):
    """Restore saved application state, as previously obtained from ``GetVisualState``.
    Any errors encountered are added to ``errs`` as strings."""
    
    self.fSingletons.fGuiMgr.SetVisualState(state, errs)
    
  #------------------------------------------------------------------------
  #@ Preferences

  @_apicall
  def GetPreference(self, pref):
    """Get the value of the given preference.
    
    The ``pref`` argument should be the fully qualified name of the preference, as given
    in the `Preferences Reference <ref:preferences/index>`__."""
    
    return self.fSingletons.fPrefMgr.GetValue(pref)
    
  @_apicall
  def SetPreference(self, pref, value):
    """Set value for the given preference.
    
    The ``pref`` argument should be the fully qualified name of the preference, as given
    in the `Preferences Reference <ref:preferences/index>`__.
    
    The ``value`` must conform to the documentation for the preference."""
    
    self.fSingletons.fPrefMgr.SetValue(pref, value)

  @_apicall
  def ConnectToPreference(self, pref, cb):
    """Connect to the given preference so that the given callback is called whenever the
    value of the preference changes.
    
    The ``pref`` argument should be the fully qualified name of the preference, as given
    in the `Preferences Reference <ref:preferences/index>`__.
    
    ``cb`` is called with no arguments. It may obtain the new value of the preference with
    ``GetPreference``.
    
    The callback will be uninstalled automatically if the caller's script is reloaded.
    
    Returns a signal id that can be used with later ``DisconnectFromPreference``."""
    
    return self.fSingletons.fScriptMgr.ConnectToPreference(pref, cb)
    
  @_apicall
  def DisconnectFromPreference(self, pref, id):
    """Disconnect a preference value callback.
    
    The ``pref`` argument should be the fully qualified name of the preference, as given
    in the `Preferences Reference <ref:preferences/index>`__.
    
    ``id`` is the signal id that was returned from ``ConnectToPreference``."""
    
    self.fSingletons.fPrefMgr.DisconnectFromValue(pref, id)
    
  @_apicall
  def ShowPreference(self, prefname):
    """Show the given preference by name in the preference manager dialog.
    
    ``prefname`` should be the fully qualified name of the preference, as given in the
    `Preferences Reference <ref:preferences/index>`__."""
    
    self.fSingletons.fGuiMgr.PrefGUI(prefname)
    
  #------------------------------------------------------------------------
  #@ Messages and Status
    
  @_apicall
  def ShowMessageDialog(self, title, text, checks=[], buttons=[("OK", None)], 
                        modal=True):
    """Display a message dialog to the user with the given ``title`` and ``text``.
    
    If ``checks`` is non-empty it contains a list of ``(label, default, callback)`` tuples
    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 a
    single ``OK`` button. The button action can be ``None`` to simply close the dialog or
    it can be a callable taking no arguments that returns ``True`` to prevent closing of
    the dialog or ``False`` to allow it to close when the button is pressed.
    
    The dialog is modal unless ``modal`` is set to ``False``."""
    
    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)
    dlg.AllowSheetWindow(False)
    if modal:
      dlg.RunAsModal(self.fSingletons.fWinMgr.GetActiveWindow())
    else:
      dlg.Run()
    
  @_apicall
  def SetStatusMessage(self, text, timeout=5):
    """Display a transient status message in the status area at the bottom of the IDE
    window. The message persists for the given ``timeout`` (in seconds) or until another
    status message is shown."""

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

  #------------------------------------------------------------------------
  #@ Sub-Process Control
    
  @_apicall
  def ExecuteCommandLine(self, cmd, dirname, input, timeout, env=None, 
                         bufsize=100000, return_stderr=False, encoding=...):
    """Run a command line synchronously until it completes.
    
    ``cmd`` can either be a command line (as a string) or a list containing the executable
    and any arguments.
    
    The command is run in the directory specified by ``dirname``.
    
    ``input`` is any text to send to the sub-process, or ``None`` to send nothing.
    
    ``timeout`` sets the maximum number of seconds to wait for the command to complete.
    
    Unless ``env`` is given, the command is run in the environment configured in the
    current project.  Otherwise, ``env`` should be in the same form as ``os.environ``.
    
    The ``bufsize`` is used for the I/O buffer to the sub-process, as follows: 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, the buffer is set to that number of bytes.

    When ``return_stderr`` is ``True``, both ``stderr`` and ``stdout`` are returned.
    Otherwise only ``stdout`` is returned.
    
    The ``encoding`` is the encoding used to encode / decode text sent / received
    from the child command. It defaults to the default OS Commands encoding; use
    encoding=None to disable encoding / decoding and return bytes instances with
    the command output.
    
    Returns ``(err, output_txt)`` where ``err`` is one of::
    
      0 -- Success
      1 -- Command could not be launched
      2 -- Command timed out

    And ``output_txt`` is either a string containing ``stdout`` or,  a tuple ``(stdout,
    stderr)`` (when ``return_stderr`` was set to ``True``).
  
    Use ``AsyncExecuteCommandLine`` to avoid locking up Wing while the command runs, or to
    access process exit status.
        
    """

    import proj.project
    if env is None:
      env = proj.project.GetEffectiveEnvironment()
    
    from wingbase import spawn
    errno, exit_status, stdout, stderr = spawn.check_output(cmd, dirname, input, timeout, env, bufsize,
                                                            encoding=encoding)
    if return_stderr:
      return (errno, (stdout, stderr))
    else:
      return (errno, stdout)
    
  @_apicall
  def AsyncExecuteCommandLine(self, cmd, dirname, *args, encoding=...):
    """Run the given command asynchronously in the given directory.

    ``cmd`` contains the executable to run, either the full path or the name on the
    ``PATH``.
    
    Additional command line arguments are passed as extra parameters via ``args``.
    
    The ``encoding`` is the encoding used to encode/decode text sent or received
    from the child command. It defaults to the default OS Commands encoding; use
    ``encoding=None`` to disable encoding/decoding and return bytes instances with
    the command output.

    Returns a handler instance that is used asynchronously to monitor the progress of the
    command and to obtain its output and exit status. This can be placed into a timeout
    function installed with ``InstallTimeout``.  For example:
    
    .. python::
    
      handler = wingapi.AsyncExecuteCommandLine('ls', '/path/to/dir', '-al')
      def watch():
        if handler.Iterate():
          stdout, stderr, err, exit_status = handler.Terminate()
          print "Done"
          print stdout
          return False
        else:
          print "Start time (relative to time.time()):", handler.time
          print "Iteration #", handler.count
          print "Output so far: %i characters", len(handler.stdout)
          return True
      wingapi.InstallTimeout(500, watch)

    Be sure that the timeout function returns ``True`` until the handler has completed,
    so that the timeout is called again and ``Terminate`` is eventually called.
    
    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, or ``None`` on success
      exit_status -- Exit status of the child process, or ``None`` if it was
                     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 the ``handler.SendToChild()`` method.
    The signature of the method is ``WriteToChild(self, s, flush=True, close_after=False):``
    and the flush and close_after arguments control whether to stream to the child
    will be flushed and closed after writing.  The string passed will be
    encoded unless it is a bytes instance. Some processes wait for the pipe 
    to be closed before continuing."""

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

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

  @_apicall
  def AsyncExecuteCommandLineEB(self, cmd, dirname, env, bufsize, *args, encoding=...):
    """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, the buffer is set to that number of bytes. To pass the
    project-defined environment to this call, use ``CAPIProject.GetEnvironment``."""
    
    from wingbase import spawn
    handler = spawn.CAsyncExecute(cmd, env, dirname, 10000.0, bufsize, *args, encoding=encoding)
    return handler

  @_apicall
  def GetExecutionTarget(self):
    """Get the default execution target for commands run by the IDE with CreateChildProcess().
    This is determined by the Python Executable setting in Project Properties.  Returns
    one of:
    
      ``('host', host_id)`` -- Runs the command on the remote host configuration with given
      host_id or the local host when host_id is ''
          
      ``('container', container_id)`` -- Runs the command on the given container configuration.
        
      ``('cluster, cluster_id, service, in_cluster)`` -- Runs the command on the given cluster
      service, either in-cluster or in a synthesized out-of-cluster instance of the given
      cluster service.
      
    Note that remote hosts, containers, and clusters are only available in Wing Pro.

    """
    
    hostname, pyexec = project.GetEffectiveValue(project.attribs.kPyExec)
  
    host = project.GetEffectiveHostname()
    container = project.GetEffectiveContainer()

    if isinstance(pyexec, project.CClusterPython):
      return ('cluster', pyexec.cluster, pyexec.container, pyexec.in_cluster)
    elif container:
      return ('container', container['id'])
    elif host:
      return ('host', host)
    else:
      return ('host', '')
    
  @_apicall
  def CreateChildProcess(self, args, terminated_cb, io_cb=None, target=None, env=None,
                         dirname=None, separate_stderr=True, timeout=None, encoding='utf-8'):
    """Create a child process that runs asynchronously and communicates with the caller
    through the given callbacks.

    ``CreateChildProcess()`` is a more flexible replacement for the ``AsyncExecute*`` and
    ``Execute*`` API calls, with support for executing commands on a remote host, container,
    or cluster service, as well as the local host.
    
    **Arguments**
    
    ``args`` is a list that contains all the arguments for the child process, including
    the executable name or path as the first list item.
    
    ``terminated_cb(timeout, exc, exit_code)`` is called when the process exits. If the
    process timed out then ``timeout`` is set to True. When the process failed to start,
    ``exc`` is set to a string describing the reason for failure. Otherwise, ``exc`` is
    ``None`` and ``exit_code`` contains the exit code given by the child process (usually
    0 indicates success and other codes indicate failure, but this is defined by the
    implementation of the child process)

    ``io_cb(stdout, stderr)`` is called (if given) with any output from the child process.
    If output to ``stderr`` and ``stdout`` is not being kept separate, all output will be
    reported through the first argument.  The output is a string or bytes, depending on the
    value for ``encoding`` (see below).
    
    ``target`` indicates where the command should be run.  When this is None, it is the
    host or container used by the current project's ``Python Executable``.  Otherwise,
    see GetExecutionTarget() for allowable values.
    
    ``env`` is a dictionary containing the environment to use, or ``None`` for the default
    environment on the selected target.
      
    ``dirname`` is the starting directory for the process, or ``None`` to use the default for
    the selected target.
      
    ``seperate_stderr`` is True when ``stderr`` output should be kept separate from ``stdout``.
  
    ``timeout`` is the time in seconds after which the command should automatically be
    terminated, or None not to terminate.  
    The ``encoding`` is the encoding used to encode/decode text sent or received from the
    child command. Use ``None`` to disable encoding/decoding.  When an encoding is given,
    output returned from the process will be in ``str`` instances.  When ``None`` is
    used, output is instead returned as ``bytes``.

    **Return Value**
    
    ``GetChildProcess`` returns a process control instance with the following available methods:
    
      ``GetOutput()`` -- returns all the output seen so far from the child process.  The output
      is a string unless an output encoding was given using the ``encoding`` argument
      to CreateChildProcess().
      
      ``GetStdErrOutput()`` -- like ``GetOutput()`` but returns all the ``stderr`` output seen so far
      from the child process. This should only be called when ``separate_stderr`` was set to
      ``True`` when calling ``CreateChildProcess()``.
      
      ``_SendInputToChild(text, close)`` -- Sends the given text to the child process, optionally
      closing the socket after doing so.
      
      ``GetExitCode()`` -- Gets the child process exit code, or ``None`` if the process is still
      running.
      
      ``Kill()`` -- Terminates the child process.
      
    These methods can only be called before ``terminate_cb`` exits.  After that the
    process control instance is destroyed and may no longer be used.
    
    """
    
    from edit import cap_oscommands

    host = None
    container = None
    
    if target is None:
      host = project.GetEffectiveHostname()
      container = project.GetEffectiveContainer()
      if container:
        container = container['id']
        
    elif target[0] == 'host':
      host = target[1]
      
    elif config.kAvailContainers and target[0] == 'container':
      container = target[1]
      
    elif config.kAvailContainers and target[0] == 'cluster':
      cluster, service, in_cluster = target[1:]
      container = self.fSingletons.fLaunchMgr.fClusters.SynthesizeClusterContainer(cluster, service, in_cluster)
      
    else:
      raise RuntimeError("Invalid value for 'target'")
      
    proc = cap_oscommands.CreateChildProcess(host, container, args, env, dirname, io_encoding=encoding,
                                             separate_stderr=separate_stderr, timeout=timeout)
    
    def terminated(obj):
      terminated_cb(obj.fTimedOut, None, obj.GetExitCode())
      obj.destroy()
    def start_failed(obj, exc):
      terminated_cb(False, exc, None)
      obj.destroy()
      
    proc.connect_while_alive('terminated', terminated, self)
    proc.connect_while_alive('start-failed', start_failed, self)

    def received_output(p, output):
      if callable(io_cb):
        io_cb(output, None)

    def received_stderr_output(p, output):
      if callable(io_cb):
        io_cb(None, output)
     
    proc.connect('received-output', received_output)
    proc.connect('received-stderr-output', received_stderr_output)
    
    proc.Start()
    
    return proc

  #------------------------------------------------------------------------
  #@ Sub-Process Control with OS Commands
    
  @_apicall
  def AddOSCommand(self, cmd, dirname, env, flags, *args):
    """Add the given command line to the ``OS Commands`` tool. The ``cmd`` can be the whole
    command line as a list, or just the executable if its arguments are passed through
    ``args``.
    
    ``dirname`` can be ``None`` to indicate using the project-defined default starting
    directory.
    
    ``env`` can be ``None`` to use the project defaults, or a dictionary to add values to
    the project-defined environment.
    
    ``flags`` is a dictionary containing zero or more of the following:
    
      ``title``: The display title for the command  Default: same as ``cmd`` argument.
      
      ``hostname``: The name of the configured remote host to run on, or ``''`` to indicate
      local host. Default: ``None`` which indicates that project settings will be used.
                     
      ``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 internal command ID for the added command.
    
    This adds a ``Command Line`` style OS Command. Adding ``Python File`` and ``Named
    Entry Point`` style OS Commands is not supported by the API."""

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

    view = cap_oscommands.GetOSCommandsPanel()

    title = flags.get('title', cmd)
    hostname = flags.get('hostname', None)
    if hostname is None:
      hostname = (proj.attribs.kProject, '')
    elif hostname == '':
      hostname = (proj.attribs.kDefault, '')
    else:
      hostname = (proj.attribs.kCustom, hostname)
    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 wingbase import spawn
    cmd = spawn.ParseCmdArgs(cmd)
    cmdline = spawn._win32_cmd_line_from_argv(tuple(cmd) + args)
    scmd = cap_oscommands.CCommandLineCommand(
      cmd=cmdline, title=title, hostname=hostname, rundir=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
    
  @_apicall
  def RemoveOSCommand(self, cmd_id):
    """Remove an OS Command.
    
    ``cmd_id`` is the internal command ID returned from ``AddOSCommand``.
    
    The command is terminated first if it is currently running."""
    
    if not config.kAvailOSCommands:
      raise NotImplementedError
    from edit import cap_oscommands
    view = cap_oscommands.GetOSCommandsPanel()
    cmd = view._FindCmdByID(cmd_id)
    view.RemoveCommand(cmd)
    
  @_apicall
  def ExecuteOSCommand(self, cmd_id, show=True):
    """Execute the given command in the ``OS Commands`` tool, using the internal command
    ID returned from previous call to ``AddOSCommand``.
    
    If ``show`` is True then the ``OS Commands`` tool will be shown. Otherwise, the tool
    is shown only if the command was configured to always show the tool when executed."""
    
    if not config.kAvailOSCommands:
      raise NotImplementedError
    from edit import cap_oscommands
    
    view = cap_oscommands.GetOSCommandsPanel()
    if show:
      view = self.fSingletons.fGuiMgr.ShowPanel(config.kOSCommandPanel)
      
    if view._FindCmdByID(cmd_id) is None:
      raise KeyError('OS Command not found %s' % cmd_id)
    
    view.Execute(cmd_id)
    
  @_apicall
  def TerminateOSCommand(self, cmd_id):
    """Terminate an OS Command if it is currently running.
    
    ``cmd_id`` is the internal command ID returned from ``AddOSCommand``."""
    
    if not config.kAvailOSCommands:
      raise NotImplementedError
    from edit import cap_oscommands
    view = cap_oscommands.GetOSCommandsPanel()
    view.Terminate(cmd_id)
    
  #------------------------------------------------------------------------
  #@ Scripting Framework Utilities
    
  @_apicall
  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)
    
  @_apicall
  def EnablePlugin(self, plugin_id, enable):
    """Enable to disable a plugin.
    
    This 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.fWinMgr, 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.fPerspectiveMgr, id
  def __PythonRuntimeChanged(self):
    def forward(*args):
      self.emit('python-runtime-changed')
    id = self.fSingletons.fProjMgr.connect('python-runtime-changed', forward)
    return self.fSingletons.fProjMgr, 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,
                         'python-runtime-changed': __PythonRuntimeChanged,
                       }
    
#@! editor

#@ API support for the editor has two parts:
#@
#@ **(1)** ``CAPIDocument`` is used to access the buffer that contains the text for one or
#@ more editors. Multiple editors may share a single buffer, and buffers are also used for
#@ search or source analysis operations.
#@
#@ **(2)** ``CAPIEditor`` is used to access a single editor in the user interface, with a
#@ single file open in it. Each editor tab in Wing is a separate editor.

###########################################################################
# Document API
###########################################################################

class CAPIDocument(_CAPIBase):
  """API to access an open editor document. This class should not be instantiated
  directly. Use the methods on ``CAPIApplication`` and ``CAPIEditor`` instead.

  A single document may be shared by multiple open editors, and/or search and
  static code analysis tasks.
    
  The document uses an internal utf-8 encoded buffer and positions returned from
  methods or used as arguments are positions in that utf-8 buffer.  However, text
  is returned as a str instance, which is not utf-8 encoded just like all other
  Python 3 str instances.  Because of this, the len() of the str instances
  returned may not be equal to the len() of the internal utf-8 buffer.
  
  **Signals**
  
  A callback can be connected to the following signals using ``Connect(signal, cb)``:
      
    ``destroy``: The document is closing.  Calls cb(doc:CAPIDocument).
    
    ``modified``: The document's text has been modified. Calls ``cb(insert:bool,
    pos:int, length:int, text:str, lines_added:int)`` where:
    
      ``insert`` is ``True`` if text was inserted and ``False`` if text was deleted.
      
      ``pos`` is the position of the change.
      
      ``length`` is the length of text affected.
      
      ``text`` is the text that was inserted or deleted.
      
      ``lines_added`` is the number of lines added.
      
    ``presave``: The document is about to be saved to disk. Calls ``cb(filename:str,
    encoding:str)`` where ``filename`` and ``encoding`` are ``None`` if the
    document-specified 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``: The document has entered or left a save point, where it matches the
    copy that was read from or written to disk. Calls ``cb(save_point:bool)`` where
    ``save_point`` is True if a save point was reached and ``False`` if leaving the save
    point.
                       
    ``filename-changed``: The filename for this document has changed. Calls
    ``cb(old_name:str, new_name:str)`` where ``old_name`` and ``new_name`` are full
    paths.
  
  Use ``Disconnect(signal_id)`` to preemptively disconnect a signal handler, where
  ``signal_id`` is the signal ID previously returned from ``Connect``."""

  def __init__(self, singletons, cache):
    """Constructor for this class"""
    
    _CAPIBase.__init__(self, singletons, cache,
                       ('modified', 'presave', 'save-point',
                        'filename-changed'))
    self.fCache = cache
    
  #-----------------------------------------------------------------------
  #@ General Access
  
  @_apicall
  def GetMimeType(self):
    """Get the mime type for this document, as determined by file name, contents, and
    `Files > File Types > Extra File Types
    <script:show_preferences_gui#prefname="main.extra-mime-types">`__ preference."""
    
    return self.fSingletons.fFileAttribMgr.GetProbableMimeType(self.fCache.fLocation)
    
  @_apicall
  def GetFilename(self):
    """Get the file name this document. For untitled or scratch buffers, the file name is
    prefixed with ``unknown:``. For remote files, the file name will be a URL. Use
    ``IsUrl`` to distinguish between file names and URLs."""
    
    return _LocationToFilename(self.fCache.fLocation)
  
  @_apicall
  def GetEditors(self):
    """Get all existing editors for this document.  This may be an empty list if the
    document is only open for searching or static analysis."""
    
    eds = self.fSingletons.fEditMgr.GetEditors(self.fCache.fLocation)
    retval = []
    for ed in eds:
      retval.append(CAPIEditor._GetWrapper(ed))
    return retval

  #-----------------------------------------------------------------------
  #@ Buffer Access
  
  @_apicall
  def GetText(self):
    """Get the document's contents as a string"""
    
    return self.GetCharRange(0, self.GetLength())
    
  @_apicall
  def SetText(self, txt):
    """Set the document contents, replacing any existing content.  The ``txt`` must be
    either a unicode string or utf-8 encoded text."""
    
    self.fCache.fDoc.delete_chars(0, self.GetLength())
    self.fCache.fDoc.insert_string(0, txt)
    
  @_apicall
  def DeleteChars(self, start, end):
    """ Delete characters in given range, including the character starting at the ``end`` offset.
    The offsets are utf-8 offsets."""
    
    after_end = self.fCache.AdjustUtf8Pos(end, +1)
    if after_end <= start:
      return
    self.fCache.fDoc.delete_chars(start, after_end - start)
    
  @_apicall
  def InsertChars(self, pos, txt):
    """Insert characters at the given position. The ``txt`` must either be an
    unicode string or utf-8 encoded bytes instance. The pos is the offset in
    the internal utf-8 encoded buffer."""
    
    self.fCache.fDoc.insert_string(pos, txt)
    
  @_apicall
  def GetLength(self):
    """Get the total length of document's utf-8 buffer."""
    
    return self.fCache.fDoc.length()
  
  @_apicall
  def GetLineCount(self):
    """Get the total number of lines in the document."""
    
    return self.fCache.fDoc.lines_total()

  @_apicall
  def GetCharRange(self, start, end):
    """Get the text in the given range as a string. The offsets are relative
    to the utf-8 encoded buffer.  Note that the string returned will be a str
    instance and its len() may not equal end - start """
    
    if start == end:
      return ''
    else:
      end = min(self.GetLength(), end)
      return self.fCache.fDoc.get_char_range(start, end - start)

  @_apicall
  def GetLineNumberFromPosition(self, pos):
    """Get the line number (0=first) at the given position in the utf-8 encoded buffer"""
    
    return self.fCache.fDoc.line_from_position(pos)
  
  @_apicall
  def GetLineStart(self, lineno):
    """Get the character position for the start of the given line number (0=first). The
    offset is relative to the utf-8 encoded buffer."""
    
    return self.fCache.fDoc.line_start(lineno)

  @_apicall
  def GetLineEnd(self, lineno):
    """Get the character position for the end of given line number (0=first). The offset
    is relative to the utf-8 encoded buffer."""
    
    return self.fCache.fDoc.line_end(lineno)
  
  @_apicall
  def GetAsFileObject(self):
    """Get the document's contents in a file-like object with read() and readline()
    methods. Both methods return str instances """
    
    from cache import textcache
    return textcache.ScintDocAsFile(self.fCache.fDoc, binary=False)
  
  #-----------------------------------------------------------------------
  #@ Undo/Redo
  
  @_apicall
  def BeginUndoAction(self):
    """Mark the start of an undoable action group. All edits between this call and
    ``EndUndoAction`` will be undone in a single ``undo`` operation.
    
    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 a follows:
    
    .. python::

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

    """
    self.fCache.fDoc.begin_undo_action()
  
  @_apicall
  def EndUndoAction(self):
    """Mark the end of an undoable action group."""
    
    self.fCache.fDoc.end_undo_action()

  @_apicall
  def CanUndo(self):
    """Check whether undo is available."""
    
    return self.fCache.fDoc.can_undo()
  
  @_apicall
  def CanRedo(self):
    """Check whether redo is available."""
    
    return self.fCache.fDoc.can_redo()
  
  @_apicall
  def Undo(self):
    """Undo one edit action in the document."""
    
    self.fCache.fDoc.undo()
  
  @_apicall
  def Redo(self):
    """Redo edits previously undone with ``Undo``."""
    
    self.fCache.fDoc.redo()

  #-----------------------------------------------------------------------
  #@ Saving
  
  @_apicall
  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.CreateFromName(filename)
      self.fCache.WriteToLocation(loc)
    else:
      self.fCache.WriteToLocation()
    
  @_apicall
  def IsSavePoint(self):
    """Check whether the buffer matches its file on disk.  Returns ``True`` if it does."""
    
    return self.fCache.fDoc.is_save_point()
  
  #------------------------------------------------------------------------
  # 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 API
###########################################################################    
    
class CAPIEditor(_CAPIBase):
  """API to access an editor.  This class should not be instantiated directly.
  Use the methods on ``CAPIApplication`` instead.
  
  **Signals**

  A callback can be connected to the following signals using ``Connect(signal, cb)``:
            
    ``destroy``: The editor has been destroyed.  Calls ``cb(editor:CAPIEditor)``.
    
    ``selection-changed``: The current selection has changed. Calls ``cb(start:int,
    end:int)`` with the new selection, relative to the utf-8 contents of the
    ``CAPIDocument``.
    
    ``selection-lines-changed``: The starting and/or ending line for the selection has
    changed. In Python files this is *not* emitted when the selection moves to a new
    physical line within the same logical line of code. Calls ``cb(first_line,
    last_line)`` with the new first and last lines (0=first line in file).
                               
    ``scrolled``: The editor view has scrolled. Calls ``cb(top_line)`` with the new first
    visible line (0=first line in file).
                               
    ``visible-lines-changed``: The range of visible lines has changed. Calls cb(top_line,
    ``bottom_line)`` with the new top and bottom lines (0=first line in file).
                               
    ``readonly-edit-attempt``: An edit was attempted and rejected on a readonly file.
    ``Calls cb()`` without arguments.
                               
    ``data-entry-stopped``: Data entry mode has stopped. Calls ``cb(data_entry_id)`` where
    ``data_entry_id`` is the ID returned from ``StartDataEntry``.
      
  Use ``Disconnect(signal_id)`` to preemptively disconnect a signal handler, where
  ``signal_id`` is the signal ID previously returned from ``Connect``."""
  
  def __init__(self, singletons, editor):
    """Constructor for this class"""
    
    _CAPIBase.__init__(self, singletons, editor, [
      'selection-changed', 'scrolled', 'visible-lines-changed',
      'selection-lines-changed', 'readonly-edit-attempt',
      'data-entry-stopped'
    ])
    self.fEditor = editor

  #-----------------------------------------------------------------------
  #@ General Access
  
  @_apicall
  def IsReadOnly(self):
    """Check whether the editor is readonly.  Returns ``True`` or ``False``."""
    
    return self.fEditor.fReadOnly
  
  @_apicall
  def SetReadOnly(self, readonly):
    """Set whether or not the editor is read-only.  ``readonly`` should be ``True``
    or ``False``."""
    
    self.fEditor.SetReadOnly(readonly)
    
  @_apicall
  def GetDocument(self):
    """Get the ``CAPIDocument`` object being shown in this view."""
    
    wrap = CAPIDocument._GetWrapper(self.fEditor.fCache)
    isinstance(wrap, CAPIDocument)
    return wrap
  
  #-----------------------------------------------------------------------
  #@ Selections
  
  @_apicall
  def GetSelection(self):
    """Get ``(start, end)`` for the selection on the editor. ``start`` is always less
    than ``end``. The offsets are relative to the utf-8 encoded text in the editor's
    ``CAPIDocument``."""
    
    return self.fEditor.GetSelection()
    
  @_apicall
  def GetSelectedDottedName(self):
    """Get ``(dotted_name, lineno)`` for the current selection on the editor.  The
    dotted_name may be a simple symbol like 'text', an expression like
    'modname.classname.attrib', or None if no dotted name is found at the current
    selection position."""
    
    return self.fEditor._FindSelectedDottedName()
    
  @_apicall
  def GetAnchorAndCaret(self):
    """Get the current selection anchor and caret position.  The anchor may come after
    the caret position if the user has selected backwards in the text.  The offsets are
    relative to the utf-8 encoded text in the editor's ``CAPIDocument``."""
    
    return (self.fEditor._fScint.get_anchor(), self.fEditor._fScint.get_current_pos())

  @_apicall
  def SetSelection(self, start, end, expand=1):
    """ Set the selection on the editor, optionally expanding any folded parts to show the
    selection. ``start`` is the selection anchor and ``end`` is the caret position. The
    anchor can be before or after the caret. Does not alter scroll position. Offsets are
    relative to the utf-8 encoded text in the editor's ``CAPIDocument``."""
    
    if expand:
      self.fEditor.SetSelection(start, end, select=True)
    else:
      self.fEditor._fScint.set_sel(start, end)

  @_apicall
  def GetClickLocation(self):
    """Get the offset in the utf-8 encoded text buffer for the last mouse click on the
    editor."""
    
    return self.fEditor._FindPoint()[1]
  
  @_apicall
  def GetSourceScope(self):
    """ Get the current source scope, based on position of selection or insertion caret.
    
    Returns ``None`` if nothing is selected or a list that contains a filename, a line
    number (0=first), and zero or more source symbol names indicating the nested scope
    that the user has selected.
    
    For example:
    
    .. python::
    
      ["/x/y/z.py", 120, "Class1", "Method1"]
      
    For untitled or scratch buffers, the file name is prefixed with ``unknown:``. For
    remote files, the file name will be a URL. Use ``IsUrl`` to distinguish between file
    names and URLs."""
    
    t = self.fEditor.GetSourceScopes()[0]
    scopes = [symbolinfo.SplitScopeNameFromSuffixes(sym)[0] for sym in t[2:]]
    return [_LocationToFilename(t[0]), t[1] - 1] + scopes
  
  #-----------------------------------------------------------------------
  #@ Scrolling and Visual State
  
  @_apicall
  def GetFirstVisibleLine(self):
    """Get the line number of the first visible line (0=first in file) on screen in this
    editor."""
    
    return self.fEditor.GetFirstVisibleLine()
  
  @_apicall
  def GetNumberOfVisibleLines(self):
    """Get the number of visible lines on screen for this editor."""
    
    return self.fEditor._fScint.lines_on_screen()
    
  @_apicall
  def ScrollToLine(self, lineno, select=0, pos='slop', store_history=1, callout=0):
    """Scroll so that given line (0=first) or selection is visible in the editor.
    
    ``select`` can be one of:
    
      ``0`` to make no changes in selection.
      
      ``1`` to select the whole line.
      
      ``2`` to place the caret at the start of the line.
      
      ``(start, end)`` to select the given character range, relative to the utf-8
      buffer for the editor.  In this case, ``lineno`` may be set to ``-1`` to
      compute the line number from the selection.

    ``pos`` can be one of:

      ``slop`` to ensure visibility without a specific position.
      
      ``center`` to always center the line on the display.
      
      ``top`` to always position the line at the top of the display.
      
      Only ``center`` and ``top`` will work if the editor has not yet been shown.
        
    Set ``store_history`` to ``False`` to avoid remembering the current editor position in
    the visit history.
    
    Set ``callout`` to ``True`` to briefly display a callout to highlight the
    given text selection."""
    
    if lineno >= 0:
      lineno += 1
    self.fEditor.ScrollToLine(lineno, select, pos, store_history, callout)
  
  @_apicall
  def GetVisualState(self, errs=[], style='all'):
    """Get the current visual state of the editor, including scroll position, selection,
    and so forth.
    
    The ``style`` of the state may be one of:
    
      'all' to capture all of the application visual state
      
      'tools-and-editors' to capture which tools are visible, the overall layout of the
      windows, and which editors are open (but not details like scroll positions,
      selection, or current search string)
      
      'tools-only' to capture only which tools are visible and the overall layout of the
      windows (but not which editors are open).
    
    Returns an opaque dictionary with the state, for later use with ``SetVisualState``.
    
    Any errors encountered are added to ``errs`` as strings."""
    
    if style == 'all':
      constraint = None
    else:
      constraint = 'perspective_%s' % style.replace('-', '_')
    return self.fEditor.GetVisualState(errs, constraint)
  
  @_apicall
  def SetVisualState(self, state):
    """Restore a visual state previously obtained with ``GetVisualState``."""
    
    return self.fEditor.SetVisualState(state, [])
  
  #-----------------------------------------------------------------------
  #@ Folding
  
  @_apicall
  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
    
  @_apicall
  def FoldUnfold(self, fold_check_cb):
    """Folds or unfolds all the fold points, as determined by the given call back, which
    is called with (line_text, line_number) where line_number 0=first. The callback should
    return 1 to expand, 0 to collapse, and -1 to leave a fold point untouched. If the
    callback is not a callable, all the folds are either expanded or collapsed according
    to the value of ``bool(fold_check_cb)``.
    
    Returns a list of lineno's that were folded and a list of line numbers (0=first) that
    were expanded."""

    # API changed in Wing 10, but fall back to allowing fold_check_cb that accepts
    # just one argument, the line text
    def fold_check_wrapper(line, lineno):
      try:
        return fold_check_cb(line, lineno)
      except TypeError:
        return fold_check_cb(line)
      
    if not self.FoldingAvailable():
      return set(), set()

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

  #-----------------------------------------------------------------------
  #@ Indentation
  
  @_apicall
  def GetTabSize(self):
    """Get the effective tab size for this editor, as determined by the contents of the
    file and indentation preferences."""
    
    return self.fEditor.GetTabSize()

  @_apicall
  def GetIndentSize(self):
    """Get the indent size for this editor, as determined by the contents of the file and
    indentation preferences."""

    return self.fEditor._fIndentSize
  
  @_apicall
  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()

  @_apicall
  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)

  @_apicall
  def GetEol(self):
    """Get one end-of-line that matches the content of this editor. Returns one of:
    ``"\\n"``, ``"\\r"``, or ``"\\r\\n"``."""
    
    return self.fEditor._GetEol()
  
  #-----------------------------------------------------------------------
  #@ Command Execution
  #@
  #@ These methods execute editor commands documented in the ``Active Editor Commands``
  #@ section of the `Editor Commands <ref:commands/edit>`__ reference."""
  
  @_apicall
  def CommandAvailable(self, cmd_name, **args):
    """Check the whether an editor command is available for execution with the given
    arguments. Arguments may be omitted if they don't affect command availability, which
    most don't.
    
    The available commands for an editor are documented in the ``Active Editor Commands``
    section of the `Editor Commands <ref:commands/edit>`__ reference."""
    
    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)
    
  @_apicall
  def ExecuteCommand(self, cmd_name, **args):
    """Execute the given command in the editor. Any command arguments are passed on the
    command line via ``args``.  This is used to execute commands in an editor even if
    it does not have focus.
    
    The available commands for an editor are documented in the ``Active Editor Commands``
    section of the `Editor Commands <ref:commands/edit>`__ reference."""
    
    cmd_map = self.fEditor.GetCommandMap()
    cmd = cmd_map._GetCommand(cmd_name)
    if cmd is None:
      return
    self.fSingletons.fCmdMgr.Execute(cmd, **args)
    
  #-----------------------------------------------------------------------
  #@ Snippets and Data Entry mode
    
  class CAPIFieldMetaData:
    """Stores meta data for fields used with the ``meta_data`` argument for
    ``StartDataEntry`` and ``PasteSnippet``, in order to control how fields
    are filled and visited.
    
    Available keywords arguments for the constructor are:

      ``auto_enter_from`` specifies the field index (0=first) 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. Default: ``-1``, which indicates
      no auto-entering for the field.
        
      ``force_tab_stop`` may be set to ``True`` to force including the field as a tab stop
      even if it is auto-entered from another field. This allows the user to change the
      auto-entered value. Default: ``False``
        
      ``skip_tab_stop`` is set to ``True`` to skip this field when traversing fields. This
      is useful for placing a marker that is tracked during editing but not visited as a
      tab stop. Default: ``False``
    
    For example to auto-enter a value from field ``3``:
    
    .. python::
    
      meta = CAPIFieldMetaData(auto_enter_from=3, force_tab_stop=True)
      
    """
  
    def __init__(self, auto_enter_from=-1, force_tab_stop=False, skip_tab_stop=False):
      """Constructor for this class."""
      
      self.fAutoEnterFrom = auto_enter_from
      self.fForceTabStop = force_tab_stop
      self.fSkipTabStop = skip_tab_stop
      
  # Backward compatibility; name changed in Wing 7.0
  _CAPIFieldMetaData = CAPIFieldMetaData
  
  @_apicall
  def PasteSnippet(self, txt, fields, auto_terminate=False, meta_data={}):
    """Paste a utf-8 text snippet into the editor and place the editor into inline data
    entry mode. Snippet syntax is documented in `Snippet Syntax
    <ref:edit/snippet-syntax>`__.
    
    The ``txt`` is the text to paste.
    
    ``fields`` provides the ``(start, end)`` offsets within that text for fields the user
    can enter or alter.
    
    Set ``auto_terminate`` to stop data entry mode when the last field is reached.
    
    To control behavior of the fields, set ``meta_data`` to a dictionary from field index
    (0=first) to ``CAPIEditor._CAPIFieldMetaData``."""
    
    self.fEditor.PasteTemplate(txt, fields, auto_terminate, meta_data)
  
  @_apicall
  def StartDataEntry(self, fields, active_range=(0,-1), goto_first=True,
                     auto_terminate=False, meta_data={}):
    """Start inline data entry mode so the user can use the ``Tab`` and ``Shift-Tab`` keys
    to move between data fields inline in the editor.
    
    ``fields`` is a list of ``(start, end)`` positions where the fields are located,
    in tab traversal order.
    
    ``active_range`` indicates the range of text within which the data mode will exist.
    Data entry terminates if the caret moves outside of this range or if the user presses
    ``Esc``. The default range of ``(0, -1)`` indicates the entire document.
    
    When ``goto_first`` is set, the first field in the tab sequence will become the
    current selection.
    
    Set ``auto_terminate`` to stop data entry mode when the last field is reached.
    
    To control behavior of the fields, set ``meta_data`` to a dictionary from field index
    (0=first) to ``CAPIEditor._CAPIFieldMetaData``.
    
    Returns ``None`` if data entry failed to start or otherwise a unique ID for the data
    entry action.
    
    This may be invoked recursively so that another data entry action is used to fill in a
    field of a previously created data entry action."""
    
    data_entry = self.fEditor._fDataEntrySupport.StartDataEntry(fields, active_range, goto_first, auto_terminate, meta_data)
    if data_entry is None:
      return None
    else:
      return id(data_entry)
    
  @_apicall
  def StopDataEntry(self):
    """Exit inline data entry mode.  If ``StartDataEntry`` was recursively invoked
    then the innermost data entry action is exited."""
    
    self.fEditor._fDataEntrySupport.StopDataEntry()
    
  @_apicall
  def ActiveDataEntry(self):
    """Get the id of the active data entry action, or ``None`` if there is none."""
    
    data_entry = self.fEditor._fDataEntrySupport.ActiveDataEntry()
    if data_entry is None:
      return None
    else:
      return id(data_entry)
    
  #-----------------------------------------------------------------------
  #@ Utilities
  
  @_apicall
  def GrabFocus(self):
    """Set keyboard focus on this editor."""
    
    self.fEditor.BecomeActive()
    
  @_apicall
  def SendKeys(self, keys):
    """Send a string of one or more keys to the editor so they are processed as if they
    were typed by the user. Key processing includes any auto-editing, auto-indentation,
    etc."""
    
    for key in keys:
      event = wgtk.qt_utils._CSentKeyEvent(wgtk.QEvent.Type.KeyPress,
                                           wgtk.KeyStringToKeyValue(key),
                                           wgtk.kNoModifier, key)
      wgtk.gQtApp.sendEvent(self.fEditor._fScint, event)
      wgtk.CallCppDelete(event)
      
  #------------------------------------------------------------------------
  # 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(data_entry):
      self.emit('data-entry-stopped', id(data_entry))
    self.fEditor.connect('data-entry-stopped', data_entry_stopped)
    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

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

class CAPIProject(_CAPIBase):
  """ API to access the project.  This class should not be instantiated directly.
  Use ``CAPIApplication.GetProject()`` instead.
  
  **Signals**
  
  A callback can be connected to the following signals using ``Connect(signal, cb)``:
      
    ``destroy``: The project is closing.  Calls ``cb(proj:CAPIProject)``.
    
    ``files-added``: Files have been added. Calls ``cb(filenames)`` where ``filenames`` is
    a list of full paths. File names starting with ``ssh:`` are on a remote host. Use
    ``IsUrl`` to distinguish between urls and local file names.
    
    ``files-removed``: Files have been removed.  Calls cb(filenames) where ``filenames``
    is the same as for the ``files-added`` signal above.
  
  Use ``Disconnect(signal_id)`` to preemptively disconnect a signal handler, where
  ``signal_id`` is the signal ID previously returned from ``Connect``."""
  
  def __init__(self, singletons, project):
    """Constructor for this class"""
    
    _CAPIBase.__init__(self, singletons, project, ('files-added', 'files-removed'))
    self.fProject = project

  #-----------------------------------------------------------------------
  #@ Project Contents
  
  @_apicall
  def GetAllFiles(self):
    """Returns a list of all the full path filenames in this project.
    
    File names starting with ``ssh:`` are on a remote host. Use ``IsUrl`` to distinguish
    between urls and local file names. """
    
    locs = self.fProject.GetFiles()
    return [_LocationToFilename(loc) for loc in locs]
    
  @_apicall
  def GetAllDirectories(self, top_only=False):
    """ Get list of full path names for all directories in this project.
    
    If ``top_only`` is ``True``, only top-level directories are returned.
    
    Directory names starting with ``ssh:`` are on a remote host. Use ``IsUrl`` to
    distinguish between urls and local file names."""
    
    locs = self.fProject._GetDirs(top_only)
    return [_LocationToFilename(loc) for loc in locs]
    
  @_apicall
  def AddFiles(self, files):
    """Add the files with given full path filenames to the project."""
    
    self.fProject.AddFiles([_FilenameToLocation(x) for x in files])
    
  @_apicall
  def RemoveFiles(self, files):
    """Remove the given full path filenames from the project."""
    
    self.fProject.RemoveFiles([_FilenameToLocation(x) for x in files])
    
  @_apicall
  def AddDirectory(self, dirname, recursive=True, filter='*', include_hidden=False,
                   watch_disk=True, excludes=()):
    """Add the given directory to the project, given its full path name.
    
    When ``recursive`` is ``True``, all children, grand-children, etc, are also added.
    
    The ``filter`` specifies which files to display.
    
    Set ``include_hidden`` to ``True`` to show also hidden files like ``.name``,
    ``*pyc``, and ``*~``.
    
    Set ``watch_disk`` to watch the disk for changes and update the ``Project`` tool
    accordingly.
    
    Set ``excludes`` to a list of relative path names from ``dirname`` for files to
    explicitly exclude from the display.
    
    If the directory is already in the project this call will replace it properties
    according to the arguments."""
    
    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)
    
  @_apicall
  def RemoveDirectory(self, dirname):
    """Remove the given directory, and any recursively added sub-directories, from the
    project."""
    
    self.fProject.RemoveDirectory(_FilenameToLocation(dirname))
    
  #-----------------------------------------------------------------------
  #@ Project Properties
  
  @_apicall
  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 environment inherited at startup with
    any values set in ``Project Properties`` and ``File Properties``.
    
    If the given ``filename`` is ``None``, only the project-wide settings are used.
    
    If a ``Python Path`` is set in ``Project Properties`` 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 environment that is
    configured for the given file (or project-wide if ``filename`` is ``None``) and not
    inherited environment values. This result can be used as the basis for calling
    ``SetEnvironment``."""

    import proj.project

    loc = _FilenameToLocation(filename)
    
    # Returning only user changes to inherited base env
    if overrides_only:
      if config.kAvailLaunchConfigs and loc is not None:
        env_list = self.fSingletons.fLaunchMgr.GetLaunchAttrib(proj.attribs.kEnvVars, loc)
        if env_list is None:
          env_list = []
      else:
        which, env_list = self.fSingletons.fFileAttribMgr[proj.attribs.kEnvVars, loc]
        if which == 'default' or which == 'project':
          env_list = []

      env = {}
      for item in env_list:
        if item.find('=') > 0:
          parts = item.split('=')
          key = parts[0]
          value = '='.join(parts[1:])
          env[key] = value
          
      if set_pypath:
        if config.kAvailLaunchConfigs and loc is not None:
          pypath = self.fSingletons.fLaunchMgr.GetLaunchAttrib(proj.attribs.kPyPath, loc)
        else:
          which, pypath = self.fSingletons.fFileAttribMgr[proj.attribs.kPyPath, loc]
          if which != 'custom':
            pypath = None
        if pypath is not None:
          env['PYTHONPATH'] = textutils.JoinWithEnv(pypath, os.pathsep)
          
      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 is not None:
        env['PYTHONPATH'] = textutils.JoinWithEnv(pypath, os.pathsep)
        
    return env
  
  @_apicall
  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(filename,
    set_pypath=False)``."""
    
    import proj.project

    loc = _FilenameToLocation(filename)
    return proj.project.ExpandEnvVars(txt, loc, self.fProject)
  
  @_apicall
  def SetEnvironment(self, filename, base, env={}):
    """Set the runtime environment for debugging or executing the given file or for the
    project as a whole if filename is ``None``.
    
    The argument ``base`` indicates which base environment the given environment should
    modify:
    
      ``'startup'``: Modify startup environment
      
      ``'project'``: Modify 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 the order of the environment keys is important, use collections.OrderedDict
    for the value of this argument.
    
    If ``PYTHONPATH`` is included in the environment, it is stored in (or cleared from)
    the ``Python Path`` property in ``Project Properties`` or ``File Properties`` and not
    the ``Environment`` property.
    
    Using ``SetFileLaunchConfig`` and related API is preferable when ``filename`` is not
    ``None``.
    """
    
    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, bytes):
        t = t.decode(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 = textutils.SplitWithEnv(value, os.pathsep)
      else:
        env_cvt.append("%s=%s" % (key, value))
      
    if config.kAvailLaunchConfigs and loc is not None:
      self.fSingletons.fLaunchMgr.SetLaunchAttrib(proj.attribs.kEnvVars, loc, (which, env_cvt))
    else:
      self.fSingletons.fFileAttribMgr.SetValue(proj.attribs.kEnvVars, loc, (which, env_cvt))
    
    if pypath is not None:
      which, value = ('custom', pypath)
      if config.kAvailLaunchConfigs and loc is not None:
        self.fSingletons.fLaunchMgr.SetLaunchAttrib(proj.attribs.kPyPath, loc, (which, value))
      else:
        self.fSingletons.fFileAttribMgr.SetValue(proj.attribs.kPyPath, loc, (which, value))
      
  @_apicall
  def GetPythonExecutable(self, filename):
    r"""Get the Python executable set for the given file or the project as a whole
    if the ``filename`` is ``None``.
    
    Returns ``None`` if using the inherited value, which is the project-wide value for a
    file, and the default found Python for a project (when ``filename`` is ``None``). The
    default Python can be determined with ``CAPIApplication.FindPython``.
    
    ``GetFileLaunchConfig`` and related API is preferable when ``filename`` is not ``None``.
    
    The executable can be on a remote host, in which case this function returns a value in
    the form ``ssh://hostname/`` where ``hostname`` is the ``Identifier`` of the `Remote
    Host <ref:proj/remote-hosts>`__ that specifies which Python to use.
    
    The executable may be a command that activates a virtualenv or other environment.
    In this case, the value returned is in the form ``env://command_line``. 
    
    If executable includes arguments, spaces within arguments are managed by escaping them
    with \ or by delimiting arguments with quotes."""

    import proj.attribs
    from proj import project
    
    loc = _FilenameToLocation(filename)
    if config.kAvailLaunchConfigs and loc is not None:
      hostname, pyexec = project.GetEffectiveValue(proj.attribs.kPyExec, loc)
      if hostname:
        pyexec = 'ssh://%s/' % hostname
      elif isinstance(pyexec, project.CEnvActivatedPython):
        pyexec = 'env://%s' % pyexec
      elif pyexec == '':
        pyexec = None
      return pyexec

    which, pyexec = self.fSingletons.fFileAttribMgr.GetValue(proj.attribs.kPyExec, loc)
    if which == 'project' and loc is not None:
      which, pyexec = self.fSingletons.fFileAttribMgr.GetValue(proj.attribs.kPyExec, None)
      
    if which == 'remote':
      pyexec = 'ssh://%s/' % pyexec
    elif which == 'activate':
      pyexec = 'env://%s' % pyexec
    elif which != 'custom':
      pyexec = None
      
    if not pyexec:
      pyexec = None

    return pyexec
    
  @_apicall
  def SetPythonExecutable(self, filename, executable):
    r"""Set the Python executable to use when debugging or executing the given file. The
    filename may be ``None`` to set the project-wide ``Python Executable``. The executable
    may be ``None`` to use the project-wide value or default found Python.
    
    The executable can be a url in the form ``ssh://hostname/`` (without any additional
    url path) to use the Python specified by the `Remote Host <ref:proj/remote-hosts>`__
    for ``hostname``. In this case, if a non-``None`` filename is given it must also be a
    file on that remote host, using a url in the form ``ssh://hostname/path/to/file.py``.
    
    The executable may be a command that activates a virtualenv or other environment,
    in which case it is in the form ``env://command_line``.
    
    If the executable includes arguments, spaces within the command line must be managed
    by escaping them with \ or delimiting arguments with quotes.

    Using SetFileLaunchConfig() and related API is preferable when ``filename`` is
    not ``None``."""

    import proj.attribs

    if executable is None:
      if filename is None:
        which = 'default'
      else:
        which = 'project'
      executable = ''
    elif executable.startswith('ssh://'):
      which = 'remote'
      loc = location.CreateFromName(executable)
      executable = loc.fHostname
      if self.fSingletons.fLaunchMgr.fRemoteHosts.FindRemoteHost(executable) is None:
        raise RuntimeError("Hostname must be configured first")
      if filename is not None and not filename.startswith(executable):
        raise RuntimeError("Executable and filename hostnames must match")
    elif executable.startswith('env://'):
      which = 'activate'
      executable = project.CEnvActivatedPython(executable[len('env://'):])
    else:
      which = 'custom'
      
    loc = _FilenameToLocation(filename)
    if config.kAvailLaunchConfigs and loc is not None:
      self.fSingletons.fLaunchMgr.SetLaunchAttrib(proj.attribs.kPyExec, loc, (which, executable))
    else:
      self.fSingletons.fFileAttribMgr.SetValue(proj.attribs.kPyExec, loc, (which, executable))
    
  @_apicall
  def GetPythonExecutableProperties(self, executable=None):
    """Get information about the default Python executable or a given Python
    executable. If the executable argument is None, the default executable
    for the project will be used; otherwise the argument is a str in the same
    format as for SetPythonExecutable.
    
    On success, the return value is a dictionary containing the following values:
    
      fullpath    : The full path to the interpreter's 'python.exe' or 'python'
      version     : The Python version in #.#.# form
      prefix      : The value of sys.prefix
      baseprefix  : The value of sys.base_prefix
      pypath      : The builtin Python Path (an item set to None indicates current directory)
      keywords    : The keywords in this Python version
      builtins    : The builtins in this Python version
      
    Each of these values is None if the executable was not found or is invalid.
    
    If the interpreter has not yet been inspected then None is returned instead of a
    dictionary. In this case, an inspection is launched and this method may be called
    again later to obtain the values.
    
    """
    
    from proj import project

    interp_mgr = self.fSingletons.fInterpMgr

    default_params = self.fSingletons.fProjMgr.GetDefaultInspectParams()
    if default_params is None:
      return None

    if executable is None:
      hostname = default_params.hostname
      pyexec = default_params.cmd
    elif executable.startswith('ssh://'):
      loc = location.CreateFromName(executable)
      hostname = loc.fHostname
      pyexec =loc.fFilename
    elif executable.startswith('env://'):
      hostname = ''
      pyexec = project.CEnvActivatedPython(executable[len('env://'):])
    else:
      hostname = ''
      pyexec = executable

    params = default_params._replace(hostname=hostname, cmd=pyexec)
    inspect_results = interp_mgr.GetInspectResults(params, True)
    if inspect_results is None:
      return None
    
    retval = {
      'fullpath': inspect_results.fullpath, 
      'version': inspect_results.version, 
      'prefix': inspect_results.prefix, 
      'baseprefix': inspect_results.baseprefix, 
      'pypath': inspect_results.pypath, 
      'keywords': inspect_results.keywords, 
      'builtins': inspect_results.builtins,
      'usersite': inspect_results.usersite,
    }
    
    return retval
    
  @_apicall
  def GetInitialDirectory(self, filename):
    """Get the initial directory for debugging the given file. The ``filename`` may be
    ``None`` to set the project-wide setting. Returns ``None`` when using the startup
    directory.
    
    Using ``GetFileLaunchConfig`` and related API is preferable when ``filename``
    is not ``None``."""
    
    import proj.attribs
    import proj.project
    
    loc = _FilenameToLocation(filename)
    if config.kAvailLaunchConfigs and loc is not None:
      dirname = proj.project.GetEffectiveValue(proj.attribs.kInitialDir, loc)
      if dirname == '':
        dirname = None
      return dirname
    
    which, dirname = self.fSingletons.fFileAttribMgr.GetValue(proj.attribs.kInitialDir, loc)
    if which != 'custom' and loc is not None:
      which, dirname = self.fSingletons.fFileAttribMgr.GetValue(proj.attribs.kInitialDir, None)
    if which != 'custom':
      dirname = None
      
    return dirname
    
  @_apicall
  def SetInitialDirectory(self, filename, dirname):
    """Set the initial directory to use when debugging the given file. The ``filename``
    may be ``None`` to set the project-wide setting. The ``dirname`` may be ``None`` to
    use the startup directory.
    
    Using ``SetFileLaunchConfig`` and related API is preferable when ``filename`` is not
    ``None``."""
    
    import proj.attribs
    
    if dirname is None:
      if filename is None:
        which = 'default'
      else:
        which = 'project'
      dirname = ''
    else:
      which = 'custom'
      
    loc = _FilenameToLocation(filename)
    if config.kAvailLaunchConfigs and loc is not None:
      self.fSingletons.fLaunchMgr.SetLaunchAttrib(proj.attribs.kInitialDir, loc, (which, dirname))
    else:
      self.fSingletons.fFileAttribMgr.SetValue(proj.attribs.kInitialDir, loc, (which, dirname))
    
  @_apicall
  def GetMainEntryPoint(self):
    """Get the full path for the main entry point for this project. This returns a string
    in the form ``"entry:name"`` if the main entry point is a `Named Entry Point
    <ref:debug/named-entry-points>`__, or ``None`` if there is none, in which case
    debugging and execution start with the current editor file."""

    loc, launch_id, entry_point = self.fSingletons.fDebugMgr.GetMainEntryPoint()
    if entry_point:
      return 'entry:' + entry_point
    else:
      return _LocationToFilename(loc)
  
  @_apicall
  def SetMainEntryPoint(self, filename):
    """Set the main entry point for this project. Pass in a full path, a string in the form
    ``"entry:name"`` to use a `Named Entry Point <ref:debug/named-entry-points>`__, or
    ``None`` to unset the main entry point so that debugging and execution start with
    the current editor file."""

    if filename.startswith('entry:'):
      loc = textutils.AsStr(filename)
    else:
      loc = _FilenameToLocation(filename)
      
    import proj.attribs
    self.fSingletons.fFileAttribMgr[proj.attribs.kMainDebugFile] = loc
    
  @_apicall
  def SetDebugChildProcesses(self, enable=True):
    """Enable or disable child process debugging in this project. Set ``enable`` to
    ``True`` to always debug child processes, ``False`` to never debug child processes,
    and ``None`` to refer to the `Debugger > Processes > Debug Child Processes
    <script:show_preferences_gui#prefname="debug.multi-process-debug-sub-processes">`__
    preference."""

    import proj.attribs
    if not enable in (True, False, None):
      enable = bool(enable)
      
    self.fSingletons.fFileAttribMgr[proj.attribs.kDebugSubProcesses] = enable
    
  #-----------------------------------------------------------------------
  #@ Launch Configurations
  
  @_apicall
  def GetLaunchConfigs(self):
    """Get a list of the internal IDs for all the defined `Launch Configurations
    <ref:proj/launch-configs>`__ in the project."""
    
    if not config.kAvailLaunchConfigs:
      raise NotImplementedError
    return list(self.fSingletons.fLaunchMgr.GetAllLaunchConfigs().keys())
  
  @_apicall
  def CreateLaunchConfig(self):
    """Create a new `Launch Configuration <ref:proj/launch-configs>`__. Returns the launch
    configuration's internal ID."""
    
    if not config.kAvailLaunchConfigs:
      raise NotImplementedError

    launch_id, flags, launch_config = self.fSingletons.fLaunchMgr._CreateLaunchAttrib()
    self.fSingletons.fLaunchMgr.SaveLaunch(launch_id, launch_config, flags,
                                           merge_flags=True)
    return launch_id
  
  @_apicall
  def GetLaunchAttrib(self, launch_id, attrib, include_hostname=False):
    """Get a `Launch Configuration <ref:proj/launch-configs>`__ attribute.
    
    ``launch_id`` is the internal launch configuration ID.
    
    ``attrib`` is the attribute to get, which may be one of the following.  The
    return value varies in type, according to which attribute was retrieved:

      ``name``: The display name of the launch configuration.
      
      ``runargs``: The run arguments as a string.
      
      ``rundir``: ``(which, value)`` where ``which`` is one of ``'project'`` to use the
      project-defined value, ``'default'`` to use the directory of the file being
      launched, or ``'custom'`` to use the specified string ``value``.
                    
      ``env``: ``(which, env)`` where ``which`` is one of ``'project'`` to use the
      project-defined value, ``'merge'`` to use env to add/remove from the project-defined
      value, ``'default'`` to use the startup environment, or ``'custom'`` to use ``env``
      to add/remove from the startup environment. ``env`` is a list of ``var=value``
      strings where ``value`` can be blank to remove the named ``var`` from the modified
      environment.
                    
      ``buildcmd``: ``(which, value)`` where ``which`` is one of ``'project'`` to use the
      project-defined value, ``'default'`` to use no build command, or ``'custom'`` to use
      the specified value, which is the OS Command internal ID for a build command defined
      with ``CAPIApplication.AddOSCommand`` or by the user in the ``OS Commands`` tool.
                    
      ``pyexec``: ``(which, pyexec)`` where ``which`` is one of ``'default'`` to use the
      project-defined value, or ``'custom'`` to use given ``pyexec`` string. When arg
      ``include_hostname`` is ``True``, this is instead in the form ``(which, (hostname,
      pyexec))`` where hostname is ``''`` to indicate localhost.
                    
      ``pypath``: ``(which, value)`` where which is one of ``'project'`` to use the
      project-defined value, ``'default'`` to use the startup value, or ``'custom'`` to
      use the specified ``value``, which is a list of strings.
                    
      ``pyrunargs``: ``(which, value)`` where ``which`` is one of ``'project'`` to use the
      project-defined value, ``'default'`` to use ``'-u'``, or ``'custom'`` to use the
      specified string value
                    
      ``shared``: ``True`` or ``False``, to indicate whether the launch configuration is
      shared between all projects.
    
    Any string value can contain environment variable references in the form
    ``${ENVNAME}`` or ``$(ENVNAME)``.
    
    Raises ``KeyError`` if the specified launch configuration does not exist.
    
    Compatibility note:
    
    In Wing 6+ the pyexec attribute stored by Wing internally changed from ``(which,
    pyexec``) to ``(which, (hostname, pyexec))`` where hostname is ``''`` for localhost.
    Set ``include_hostname=True`` to receive that form in the return value.
    
    In Wing 6+ the pypath attribute stored by Wing internally changed from a string with
    ``os.pathsep`` delimiter to a list of strings.
    
    """

    if not config.kAvailLaunchConfigs:
      raise NotImplementedError
    
    flags, launch_config = self.fSingletons.fLaunchMgr.FindLaunchByID(launch_id)
    if launch_config is None:
      raise KeyError("Launch configuration id %s does not exist" % launch_id)
    
    if attrib == 'shared':
      return flags['shared']
    elif attrib == 'runargs':
      return launch_config['runargs']
    elif attrib == 'pyexec' and not include_hostname:
      hostname, pyexec = launch_config['pyexec']
      return pyexec
    else:
      return launch_config[attrib]
  
  @_apicall
  def SetLaunchAttrib(self, launch_id, attrib, value):
    """Set a single `Launch Configuration <ref:proj/launch-configs>`__ attribute.
    
    ``launch_id`` is the internal launch configuration ID to modify.
    
    See ``GetLaunchAttrib`` for the valid attribute names and values. It is up to the
    caller to validate the values specified.
    
    Raises ``KeyError`` if the specified launch configuration does not exist."""
    
    if not config.kAvailLaunchConfigs:
      raise NotImplementedError
    
    # Convert pyexec attrib to (hostname, pyexec) form for backward compatibility
    if attrib == 'pyexec' and isinstance(value, str):
      value = ('', value)

    flags, launch_config = self.fSingletons.fLaunchMgr.FindLaunchByID(launch_id)
    if launch_config is None:
      raise KeyError("Launch configuration id %s does not exist" % launch_id)
    
    if attrib == 'shared':
      flags['shared'] = value
    else:
      launch_config[attrib] = value
      
    self.fSingletons.fLaunchMgr.SaveLaunch(launch_id, launch_config, flags,
                                           merge_flags=False)
  
  @_apicall
  def DeleteLaunchConfig(self, launch_id):
    """Delete the given `Launch Configuration <ref:proj/launch-configs>`__. Any files or
    `Named Entry Points <ref:debug/named-entry-points>`__ that reference the
    configuration will revert to using the project-defined environment."""

    if not config.kAvailLaunchConfigs:
      raise NotImplementedError
    
    self.fSingletons.fLaunchMgr.DeleteLaunch(launch_id)
  
  @_apicall
  def GetFileLaunchConfig(self, filename):
    """Get the internal ID of the `Launch Configuration <ref:proj/launch-configs>`__ used
    by default with the given filename. Returns ``None`` if the project-wide configuration
    is being used."""
    
    if not config.kAvailLaunchConfigs:
      raise NotImplementedError

    import proj.attribs
    
    loc = _FilenameToLocation(filename)
    which, (runargs, launch_id) = self.fSingletons.fFileAttribMgr[proj.attribs.kLaunchConfig, loc]
    
    if which == 'custom':
      return launch_id
    else:
      return None
    
  @_apicall
  def SetFileLaunchConfig(self, filename, launch_id):
    """Set the default `Launch Configuration <ref:proj/launch-configs>`__ to use with the
    given file.
    
    ``filename`` is the full path of the file or a URL in form
    ``ssh://hostname/path/to/file.py`` for remote files, where ``hostname`` is the
    ``Identifier`` of a `Remote Host <ref:proj/remote-hosts>`__.
    
    ``launch_id`` is the internal launch configuration ID."""
    
    if not config.kAvailLaunchConfigs:
      raise NotImplementedError

    import proj.attribs
    
    loc = _FilenameToLocation(filename)
    
    which, (runargs, ignore) = self.fSingletons.fFileAttribMgr[proj.attribs.kLaunchConfig, loc]
    which = 'custom'
      
    self.fSingletons.fFileAttribMgr.SetValue(proj.attribs.kLaunchConfig, loc,
                                             (which, (runargs, launch_id)))
  
  @_apicall
  def ClearFileLaunchConfig(self, filename, runargs):
    """Clear the default `Launch Configuration <ref:proj/launch-configs>`__ for the given
    file so that launching the file will use the project-defined environment and the given
    ``runargs``.
    
    ``filename`` is the full path of the file or a URL in form
    ``ssh://hostname/path/to/file.py`` for remote files, where ``hostname`` is the
    ``Identifier`` of a `Remote Host <ref:proj/remote-hosts>`__.
    
    ``runargs`` are the run arguments to use, as a string."""
    
    if not config.kAvailLaunchConfigs:
      raise NotImplementedError

    import proj.attribs
    
    loc = _FilenameToLocation(filename)
    
    which, (ignore, launch_id) = self.fSingletons.fFileAttribMgr[proj.attribs.kLaunchConfig, loc]
    which = 'project'
    
    self.fSingletons.fFileAttribMgr.SetValue(proj.attribs.kLaunchConfig, loc,
                                             (which, (runargs, launch_id)))
    
  #-----------------------------------------------------------------------
  #@ Named Entry Points
  
  @_apicall
  def GetNamedEntryPoints(self):
    """Get a list of names for all the defined `Named Entry Points
    <ref:debug/named-entry-points>`__ in the project."""
    
    if not config.kAvailLaunchConfigs:
      raise NotImplementedError

    return list(self.fSingletons.fLaunchMgr.fEntryPoints.GetAllEntryPoints().keys())
  
  @_apicall
  def CreateNamedEntryPoint(self, name):
    """Create a new `Named Entry Point <ref:debug/named-entry-points>`__. Raises
    ``KeyError`` if the name already exists."""
    
    if not config.kAvailLaunchConfigs:
      raise NotImplementedError

    flags, entry_point = self.fSingletons.fLaunchMgr.fEntryPoints.FindEntryPoint(name)
    if entry_point is not None:
      raise KeyError("Named entry point %s already exists" % name)
    
    flags, entry_point = self.fSingletons.fLaunchMgr.fEntryPoints._CreateEntryPoint()
    self.fSingletons.fLaunchMgr.fEntryPoints.SaveEntryPoint(name, entry_point,
                                                            flags,
                                                            merge_flags=True)
  
  @_apicall
  def GetNamedEntryPointAttrib(self, name, attrib):
    """Get an attribute for the given `Named Entry Point <ref:debug/named-entry-points>`__.
    The valid attribute names are as follows.  The return value varies in type, according
    to which attribute is being retrieved:
    
      ``filename``: The Python file to launch. This is the full path or a URL in the form
      ``ssh://hostname/path/to/file.py`` for remote files where ``hostname`` is the
      ``Identifier`` of a `Remote Host <ref:proj/remote-hosts>`__.
      
      ``runargs``: The command line arguments to use when the named entry point's
      ``launch-id`` is ``None``. This valuecan contain environment variable references in
      the form ``${ENVNAME}`` or `` $(ENVNAME)``.
      
      ``launch-id``: The internal ID of the `Launch Configuration
      <ref:proj/launch-configs>`__ to use, or ``None`` to use the project-defined
      environment with the command line arguments in the ``runargs attribute``.
                              
      ``key-binding-debug``: The key binding for debugging the Launch Configuration.
      See `Key Names <ref:custom/key-names>`__ for details on valid key names.
      
      ``key-binding-execute``: The key binding for executing the Launch Configuration.
      
      ``auto-show``: ``True`` when the named entry point dialog should be shown
      automatically before launching.
    
    Raises ``KeyError`` if the named entry point does not exist."""
    
    if not config.kAvailLaunchConfigs:
      raise NotImplementedError

    import proj.attribs
    
    flags, entry_point = self.fSingletons.fLaunchMgr.fEntryPoints.FindEntryPoint(name)
    if entry_point is None:
      raise KeyError("Named entry point %s does not exist" % name)
    
    if attrib.startswith('key-binding-'):
      return flags[attrib]
    
    elif attrib == 'filename':
      loc = entry_point['loc']
      return _LocationToFilename(loc)
    
    elif attrib == 'launch-id':
      which, (runargs, launch_id) = entry_point[attrib]
      if which == proj.attribs.kProject:
        return None
      else:
        return launch_id
      
    elif attrib == 'runargs':
      which, (runargs, launch_id) = entry_point['launch-id']
      return runargs
      
    else:
      return entry_point[attrib]
  
  @_apicall
  def SetNamedEntryPointAttrib(self, name, attrib, value):
    """Set a `Named Entry Point <ref:debug/named-entry-points>`__ attribute.  ``name``
    is the entry point's name.
    
    See ``GetNamedEntryPointAttrib`` for the valid attribute names and values. It is up to
    the caller to validate the values specified.
    
    Raises ``KeyError`` if the named entry point does not exist."""
    
    if not config.kAvailLaunchConfigs:
      raise NotImplementedError

    import proj.attribs
    
    flags, entry_point = self.fSingletons.fLaunchMgr.fEntryPoints.FindEntryPoint(name)
    if entry_point is None:
      raise KeyError("Named entry point %s does not exist" % name)
    
    if attrib.startswith('key-binding-'):
      flags[attrib] = value
    elif attrib == 'filename':
      entry_point['loc'] = _FilenameToLocation(value)
    elif attrib == 'launch-id':
      which, (runargs, launch_id) = entry_point['launch-id']
      if value is not None:
        entry_point['launch-id'] = (proj.attribs.kCustom, (runargs, value))
      else:
        entry_point['launch-id'] = (proj.attribs.kProject, (runargs, launch_id))
    elif attrib == 'runargs':
      which, (old_runargs, launch_id) = entry_point['launch-id']
      entry_point['launch-id'] = (which, (value, launch_id))
    else:
      entry_point[attrib] = value
      
    self.fSingletons.fLaunchMgr.fEntryPoints.SaveEntryPoint(name, entry_point,
                                                            flags,
                                                            merge_flags=False)
  
  @_apicall
  def DeleteNamedEntryPoint(self, name):
    """Delete the given `Named Entry Point <ref:debug/named-entry-points>`__."""
    
    if not config.kAvailLaunchConfigs:
      raise NotImplementedError

    self.fSingletons.fLaunchMgr.fEntryPoints.DeleteEntryPoint(name)
    
  #-----------------------------------------------------------------------
  #@ Attributes
  #@
  #@ Use these to store information in a project file.

  @_apicall
  def GetAttribute(self, attrib_name, filename=None):
    """Get the value for given named attribute previously set with ``SetAttribute``.
    
    If ``filename`` is not ``None``, the attribute is a per-file attribute for the given
    file. Otherwise, it's a project-wide attribute.
    
    Raises ``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]
    
  @_apicall
  def SetAttribute(self, attrib_name, value, filename=None):
    """Set value for the given attribute. This is used to store data in the project file.
    Attributes may either be associated with the project as a whole or with a particular
    file.
    
    ``attrib_name`` is the attribute name, which can be any string containing letters,
    numbers, and dashes.
    
    The ``attrib_name`` is uniquified internally to avoid conflicts between scripts. If
    this is not desired, so that other scripts can also 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."""
    
    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
    
  #-----------------------------------------------------------------------
  #@ Utilities
  
  @_apicall
  def GetFilename(self):
    """Gets the filename where the project is stored, as a full path.  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.
    
    For untitled or scratch buffers, the file name is prefixed with ``unknown:``. For
    remote files, the file name will be a URL. Use ``IsUrl`` to distinguish between file
    names and URLs."""

    loc = self.fProject.GetLocation()
    return _LocationToFilename(loc)
    
  @_apicall
  def GetSelectedFile(self):
    """Returns the full path filename of the currently selected file on the ``Project``
    tool, or ``None`` if there is no selection.
    
    File names starting with ``ssh:`` are on a remote host. Use ``IsUrl`` to distinguish
    between urls and local file names."""

    # 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])
    
  #------------------------------------------------------------------------
  #@ Run Arguments

  @_apicall
  def GetRunArguments(self, filename):
    """Get the run arguments for debugging the given file, or ``''`` if there
    are none. The ``filename`` should not be ``None``.
    
    The value returned may come from the file's launch configuration, if one is being used,
    or otherwise from the file properties."""
    
    import debug.client.attribs
    
    loc = _FilenameToLocation(filename)
    if config.kAvailLaunchConfigs and loc is not None:
      return self.fSingletons.fLaunchMgr.GetLaunchAttrib(debug.client.attribs.kRunArgs, loc)
    else:
      return self.fSingletons.fFileAttribMgr.GetValue(debug.client.attribs.kRunArgs, loc)

  @_apicall
  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.
    
    The value is set into the file's launch configuration, if one is being used,
    or otherwise into the file properties.
    
    See ``add_recent`` to ``False`` to prevent adding the arguments to the
    recent arguments list."""
    
    import debug.client.attribs
    import proj.properties

    if args is None:
      args = ''
      
    loc = _FilenameToLocation(filename)
    if config.kAvailLaunchConfigs and loc is not None:
      self.fSingletons.fLaunchMgr.SetLaunchAttrib(debug.client.attribs.kRunArgs, loc, args)
    else:
      self.fSingletons.fFileAttribMgr.SetValue(debug.client.attribs.kRunArgs, loc, args)
      
    if args != '' and add_recent:
      proj.properties.UpdateRecentRunArgs(gApplication.fSingletons.fFileAttribMgr,
                                          loc, args)
  
  #------------------------------------------------------------------------
  # 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

#@ The debugger API consists of two parts:
#@
#@ **(1)** ``CAPIDebugger`` is the debug manager, which is used to manage multiple debug
#@ processes.
#@
#@ **(2)** ``CAPIDebugRunState`` is used to start, control, inspect, and terminate a
#@ single debug process.

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

class CAPIDebugger(_CAPIBase):
  """API for the debugger as a whole. This class should not be instantiated directly. Use
  ``wingapi.gApplication.GetDebugger()`` instead.
  
  **Signals**
  
  A callback can be connected to the following signals using ``Connect(signal, cb)``:

    ``new-runstate``: A new runstate has been created.  Calls
    ``cb(runstate:CAPIDebugRunState)``.
    
    ``current-runstate-changed``: A new runstate has been selected as the current
    runstate.  Calls ``cb(runstate:CAPIDebugRunState)``.
  
  Use ``Disconnect(signal_id)`` to preemptively disconnect a signal handler, where
  ``signal_id`` is the signal ID previously returned from ``Connect``."""
  
  def __init__(self, singletons, debugger):
    """Constructor for this class"""
    
    _CAPIBase.__init__(self, singletons, debugger, ('new-runstate', 
                                                    'current-runstate-changed'))
    if 0:
      from debug.client import manager
      isinstance(debugger, manager.CDebugManager)
    self.fDebugger = debugger
    
  @_apicall
  def GetRunStates(self):
    """Get the list of all ``CAPIDebugRunState`` objects that currently exist in the
    debugger."""

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

    rs = self.fDebugger.CurrentRunState()
    if rs is None:
      return None
    wrap = CAPIDebugRunState._GetWrapper(rs)
    isinstance(wrap, CAPIDebugRunState)
    return wrap
  
  @_apicall
  def SetCurrentRunState(self, rs):
    """Set the currently active ``CAPIDebugRunState``."""
    
    self.fDebugger.SetCurrentRunState(rs.fRunState)
    
  #------------------------------------------------------------------------
  # Internal support for signal forwarding

  def __NewRunstate(self):
    def new_runstate(rs):
      if rs is not None and rs._fActiveInfo.origin != 'internal':
        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 debug process. This class should not be instantiated
  directly. Use the methods on ``CAPIDebugger`` instead.
  
  Each run state is associated with a single debug process. It is created before any debug
  process is started, takes care of starting and controlling individual debug sessions,
  and outlives individual debug process termination in order to support subsequent
  inspection or launch of a new debug process.
  
  **Signals**

  A callback can be connected to the following signals using ``Connect(signal, cb)``:
      
    ``debug-started``: A debug session has been started.  Calls ``cb()``.
    
    ``debug-terminated``: The debug session has ended.  Calls ``cb()``.
    
    ``exception``: The debug process has encountered an exception.  Calls ``cb()``.
    
    ``paused``: The debug process has paused or reached a breakpoint.  Calls ``cb()``.
    
    ``running``: The debug process has started running again.  Calls ``cb()``.
  
  Use ``Disconnect(signal_id)`` to preemptively disconnect a signal handler, where
  ``signal_id`` is the signal ID previously returned from ``Connect``."""
  
  def __init__(self, singletons, runstate):
    """Constructor for this class"""
    
    _CAPIBase.__init__(self, singletons, runstate,
                       ('debug-started', 'debug-terminated', 'exception', 'paused', 'running',))
    self.fRunState = runstate
    self.fBpMgr = self.fSingletons.fDebugMgr.fBpMgr
    
  #------------------------------------------------------------------------
  #@ Starting and Stopping Debug
  
  @_apicall
  def Run(self, filename, stop_on_first=0, launch_id=None):
    """Start debug, using the given file as the main entry point.
    
    ``filename`` is the full path of the file to debug. For remote files, ``filename`` is
    in the form ``ssh://hostname/path/to/file.py`` where ``hostname`` is the
    ``Identifier`` of a `Remote Host <ref:proj/remote-hosts>`__.
    
    Set ``stop_on_first`` to stop immediately on the first line of code.  Otherwise
    debugging continues until it reaches a breakpoint, exception, or program
    termination.
    
    Set ``launch_id`` to the internal ID of a `Launch Configuration
    <ref:proj/launch-configs>`__ to use for the debug environment, or to ``None`` to use
    the file's default environment configured in ``Project Properties`` or ``File
    Properties``."""
    
    if launch_id is not None and not config.kAvailLaunchConfigs:
      raise NotImplementedError
    
    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.SetLaunchID(launch_id)
    launcher.Launch()
    
  @_apicall
  def RunNamedEntryPoint(self, name, stop_on_first=0):
    """Run the given `Named Entry Point <ref:debug/named-entry-points>`__ with its
    configured launch environment.
    
    Set ``stop_on_first`` to stop immediately on the first line of code.  Otherwise
    debugging continues until it reaches a breakpoint, exception, or program
    termination."""

    if not config.kAvailLaunchConfigs:
      raise NotImplementedError

    from debug.client import runstate
    launcher = runstate.CDebugProcessLauncher(self.fRunState)
    launcher.SetFirstStop(stop_on_first)
    launcher.UseNamedEntryPoint(name)
    launcher.Launch()

  @_apicall
  def Kill(self):
    """Stop debugging by terminating the debug process. If the debug process was launched
    by Wing, all its child processes are also terminated."""
    
    self.fRunState.Kill()
    
  #------------------------------------------------------------------------
  #@ Flow Control
  
  kStepOverInstruction = 1
  kStepOverLine = 2
  kStepOverStatement = 3
  kStepOverBlock = 4
  @_apicall
  def Step(self, over=1, out=0):
    """ Step in the code, either into, over, or out of the current execution point, as
    follows:
    
    * If ``out`` is True then step out
    * If ``over`` is ``CAPIDebugRunState.kStepOverInstruction`` step over the
      current instruction
    * If ``over`` is ``CAPIDebugRunState.kStepOverLine`` step over the current
      physical line
    * If ``over`` is ``CAPIDebugRunState.kStepOverStatement`` step over the
      current statement
    * If ``over`` is ``CAPIDebugRunState.kStepOverBlock`` step over or finish
      the current block
    * If ``over`` is a ``(start_line, end_line)`` tuple, step until debugging
      leaves that range of lines (0=first line)
    * In all other cases, step in"""
    
    if out:
      self.fRunState.StepOut()
      
    elif over == 1:
      gApplication.ExecuteCommand('step-over')
    elif over == 2:
      gApplication.ExecuteCommand('step-over-line')
    elif over == 3:
      gApplication.ExecuteCommand('step-over-statement')
    elif over == 4:
      gApplication.ExecuteCommand('step-over-block')
    elif isinstance(over, (tuple, list)):
      unit = (over[0] + 1, over[1] + 1)
      self.fRunState.StepOver(unit)
      
    else:
      self.fRunState.StepInto()

  @_apicall
  def RunToCursor(self):
    """ Run until the current editor caret location is reached, or to the next breakpoint,
    exception, or program termination if the caret's location is not reached first."""
    
    self.fRunState.RunToCursor()
  
  @_apicall
  def Continue(self):
    """Contine running the debug process to the next breakpoint, exception, or
    termination."""
    
    self.fRunState.Continue()
    
  #------------------------------------------------------------------------
  #@ Threads and Stacks
  
  @_apicall
  def GetThreads(self):
    """Get a list of ``(thread_id, name, running)`` for the active debug process, where
    ``thread_id`` is the thread ID, ``name`` is the thread function name, and ``running``
    is ``True`` if the thread is running or ``False`` if the thread is paused a breakpoint
    or exception.
    
    Returns ``None`` if no thread in the process is paused at a breakpoint or exception.
    
    The currently selected thread can be determined by calling ``GetStackFrame``."""
    
    if self.fRunState._fRunMode not in (debug.client.constants.kException,
                                        debug.client.constants.kStopped):
      return None
    return self.fRunState._fServer.GetThreads()
  
  @_apicall
  def GetStackIndex(self):
    """Get ``(thread_id, stack_index)`` where ``thread_id`` is the currently selected
    thread id and ``stack_index`` is ``(stack_no, frame_idx)`` where ``stack_no`` is the
    stack number (0=primary stack) and ``frame_idx`` is the frame index (0=outermost
    frame).
    
    If the thread is still running then ``thread_id`` and ``stack_index`` will both me
    ``None``.
    
    Stacks 1+ are PEP 3134 chained exception stacks, in order of the chain."""
    
    if self.fRunState._fRunMode not in (debug.client.constants.kException,
                                        debug.client.constants.kStopped):
      return None, None
    return self.fRunState._fServer.GetCurrentIndex()
  
  @_apicall
  def SetStackFrame(self, thread_id, idx):
    """Set the currently selected thread ID (``None`` to use current thread) and stack
    index. The stack index is in the form ``(stack_no, frame_idx)`` to allow access to
    chained exception stacks. Set ``stack_no`` to ``0`` for the primary stack and ``frame_idx`` to ``0``
    for the outermost frame.
    
    Returns ``(thread_id, stack_index)`` where ``thread_id`` is the actual thread ID and
    ``stack_index`` is the stack index that was actually set. If the thread is still
    running then ``thread_id`` and ``stack_index`` will both me ``None``.
    
    Compatibility note:
    
    This call changed in Wing 7.0 to support PEP 3134 chained exceptions. However, it
    still accepts idx set to an integer to indicate a frame in the primary stack. When idx
    is passed as an integer, the return value's stack index is also an integer."""
    
    if self.fRunState._fRunMode not in (debug.client.constants.kStopped,
                                        debug.client.constants.kException):
      return None, None
    
    try:
      idx = int(idx)
      idx = (0, idx)
      was_int = True
    except:
      was_int = False
    
    retval = self.fRunState._fServer.FrameSet(thread_id, idx, force=True)
  
    if was_int:
      return retval[0], retval[1][1]
    else:
      return retval
    
  SetStackIndex = SetStackFrame
  
  @_apicall
  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)`` where:
    
    ``filename`` is the full path of the file, or for remote files a URL in the form
    ``ssh://hostname/path/to/file.py`` where ``hostname`` is the ``Identifier`` of a
    `Remote Host <ref:proj/remote-hosts>`__.
    
    ``lineno`` is the line number (0=first line) or a tuple ``(start, end)`` to
    indicate the position of the current statement in template files.
    
    ``line_text`` is the text of the line or statement.
    
    ``scope`` is the name of the scope for this frame (for example, ``MyClass.MethodName``)
    
    ``local_varnames`` is a list of the local variable names for the frame.
    
    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 calling ``SetStackFrame``."""
    
    if self.fRunState._fRunMode not in (debug.client.constants.kStopped,
                                        debug.client.constants.kException):
      return None
    stack_no, stack = self.fRunState._fServer.GetStack()
    if stack is None:
      return None
    ctype, exc_info, stack = stack
    retval = [s[0] for s in stack]
    retval = [(f[debug.tserver.constants.kDFRFilename],
               f[debug.tserver.constants.kDFRLineno],
               f[debug.tserver.constants.kDFRCodeLine],
               f[debug.tserver.constants.kDFRCodeName],
               f[debug.tserver.constants.kDFRVarNames]) for f in retval]
    return retval
      
  #------------------------------------------------------------------------
  #@ Breakpoints
  
  @_apicall
  def SetBreak(self, filename, lineno, temp=0, cond=None, enable=1, ignore=0):
    """ Set a new breakpoint at the given position.
    
    ``filename`` is the full path of the file, or for remote files a URL in the form
    ``ssh://hostname/path/to/file.py`` where ``hostname`` is the ``Identifier`` of a
    `Remote Host <ref:proj/remote-hosts>`__.
    
    ``lineno`` is the line number at which to set the breakpoint (0=first line)
    
    ``temp`` is ``True`` to set a temporary breakpoint that will be removed the first time
    it is reached.
    
    ``cond`` is a conditional string that must evaluate to ``True`` in the context of the
    breakpoint's stack frame for the breakpoint to stop, or ``None`` to always stop on
    this breakpoint.
    
    ``enable`` can be set to ``False`` to disable stopping on the breakpoint.
    
    ``ignore`` is set to a value above ``0`` to ignore hitting the breakpoint that number
    of times before stopping on it.
    
    If a breakpoint already exists here, it is replaced.
    
    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)

  @_apicall
  def ClearBreak(self, filename, lineno):
    """Clear a breakpoint.

    ``filename`` is the full path of the file, or for remote files a URL in the form
    ``ssh://hostname/path/to/file.py`` where ``hostname`` is the ``Identifier`` of a
    `Remote Host <ref:proj/remote-hosts>`__.
    
    ``lineno`` is the breakpoint's line numbert (0=first line)"""

    loc = _FilenameToLocation(filename)
    self.fBpMgr.BreakClear(loc, lineno)
  
  @_apicall
  def ClearAllBreaks(self):
    """Clear all breakpoints."""
    self.fBpMgr.BreakClearAll()
  
  #-----------------------------------------------------------------------
  #@ Utilities
  
  @_apicall
  def GetProcessID(self):
    """Get the process ID of the active debug process. Returns ``None`` if there is no
    active process."""
    
    if self.fRunState._fActiveInfo.process is None:
      return None
    else:
      return self.fRunState._fActiveInfo.process.GetPid()
    
  @_apicall
  def GetStatus(self):
    """Get the status of the debug process.  Returns an integer, as follows:
    
    ::
      
      0 -- disconnected (no debug process)
      1 -- listening for a connection from an IDE-launched debug process
      2 -- connected to a debug process
      3 -- the debug process is running
      4 -- the debug process is stopped at a breakpoint or paused
      5 -- the debug process is stopped on an exception
      6 -- listening for a connection from a externally launched debug process
    
    """
    
    return self.fRunState._fRunMode
  
  #------------------------------------------------------------------------
  # Internal support for signal forwarding

  def __DebugStarted(self):
    def debug_started(rs, 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(rs, 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, stack_index):
      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
  
###########################################################################
# Search API
###########################################################################

class CAPISearch(_CAPIBase):
  """API for searching files and directories. One instance of this class should be
  instantiated for each search.  The arguments to the constructor are:
  
    ``txt`` -- The text to search for (required)
    
    ``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 False, 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 an instance of is created, use *one* of the following to start searching:
  
  .. python::
    
    SearchDirectory()
    SearchFile()
    SearchFiles()
  
  **Signals**
  
  Search results and status are reported through the following signals called
  asynchronously until the search completes.  The can be connected to a callback
  with ``Connect(signal, cb)``:

    ``start`` -- A new search was started. Calls ``cb()``.
    
    ``end`` -- The search completed or aborted. Calls ``cb()``.
    
    ``match`` -- One or more matches have been found. Calls ``cb(filename, matches)``
    where ``filename`` is the full path of the file, and ``matches`` is list of ``(lineno,
    linestart, line_text, positions)`` where ``lineno`` is the line number in the file
    (0=first), ``linestart`` is the position in the file where the line begins,
    ``line_text`` is the text for the line, and ``positions`` is a list of ``(start,
    end)`` tuples. The match signal may occur more than once per file or line, to report
    additional matches found. Line numbers are zero unless ``include_lineos`` was
    ``True``. All positions are from the start of the file.
                     
    ``dir`` -- Scanning a new directory. Calls ``cb(dirname:str)``.
    
    ``scanning`` -- Scanning a new file. Calls ``cb(filename)``.
    
    ``file-done`` -- Done scanning a file. Calls ``cb(filename)``.
    
  File and directory names passed to signal handlers are full paths or for remote
  files and directories, in the form ``ssh://hostname/path/to/item`` where ``hostname``
  is the ``Identifier`` of a `Remote Host <ref:proj/remote-hosts>`__.
  
  Use ``Disconnect(signal_id)`` to preemptively disconnect a signal handler, where
  ``signal_id`` is the signal ID previously returned from ``Connect``.
  
  **Example**

  .. python::

    s = CAPISearch("test", match_case=False)
    def match(filename, matches):
      print(filename, matches)
    def end():
      print("done")
    s.Connect('match', match)
    s.Connect('end', end)
    s.SearchFile('/path/to/myfile.txt')
  
  """
  
  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):
    """Constructor for this class."""
    
    import singleton
    import search.common
    
    _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 = searchutils.CMultiFileSearcher(txt, match_case, whole_words, 
                                                       omit_binary, search_style, 
                                                       include_linenos, opener_cb,
                                                       0, None, regex_flags)
    
  #-----------------------------------------------------------------------
  @_apicall
  def SearchDirectory(self, dirname, file_set, recursive):
    """Start searching the given directory for all files that match the file set,
    optionally recursively.
    
    The ``dirname`` is the full path of the directory name or for remote directories
    in the form ``ssh://hostname/path/to/dir`` where ``hostname`` is the ``Identifier``
    of a `Remote Host <ref:proj/remote-hosts>`__.
    
    The ``file_set`` can either be a name of a configured file filter stored in the
    'main.file-sets' preference or ``(includes, excludes)`` where ``includes`` and
    ``excludes`` are lists of tuples ``(spec_type, text)`` in which ``spec_type`` is one
    of 'wildcard-filename', 'mime-type', or 'wildcard-directory' and text is the pattern
    to apply to the file name, mime type, or directory name in order to filter which
    files are searched.
    
    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:
    
    .. python::
    
      [(('wildcard-filename', '*.py'),), (('wildcard-directory', 'test'),)]
    
    If the file filter in file_set is a string, an exception will be raised if it is not a
    valid file filter name."""

    import mainprefs
    from wingbase import pyutils

    if pyutils.isbasestring(file_set):
      filters = self.fSingletons.fPrefMgr[mainprefs.kFileFilters]
      file_set = filters[mainprefs._TranslatedNameToFilterName(file_set)]
  
    loc = _FilenameToLocation(dirname)
    self.fSearch.SearchDirectory(loc, file_set, recursive)

  #-----------------------------------------------------------------------
  @_apicall
  def SearchFiles(self, files):
    """Start searching all the given ``files``, which is a list of full path filenames or
    URLs in the form ``ssh://hostname/path/to/file.py`` where ``hostname`` is the
    ``Identifier`` of a `Remote Host <ref:proj/remote-hosts>`__.
    
    This accepts only filenames and not directories.  Use ``SearchDirectory`` to search
    a directory."""

    file_locs = (_FilenameToLocation(f) for f in files)
    self.fSearch.SearchFiles(file_locs)
      
  #-----------------------------------------------------------------------
  @_apicall
  def SearchFile(self, filename, start_pos=0):
    """Search a single file for search matches, optionally starting at a given point. The
    ``filename`` should be a full path or URL in the form ``ssh://hostname/path/to/file.py``
    where ``hostname`` is the ``Identifier`` of a `Remote Host <ref:proj/remote-hosts>`__.
    
    This can also be used to re-search a file previously searched through
    ``SearchDirectory`` if the file changes."""

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

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

    self.fSearch.Pause()
      
  #-----------------------------------------------------------------------
  @_apicall
  def Continue(self):
    """Continue the search process after it was previously paused with ``Pause``."""

    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,
  }


#@! analysis

#@ The static analysis API is used to inspect the structure and contents
#@ of Python files.  It consists of two parts:
#@
#@ **(1)** ``CAPISymbolInfo`` is used to describe a particular source symbol.
#@
#@ **(2)** ``CAPIStaticAnalysis`` is used to inspect a particular Python
#@ file.

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

class CAPISymbolInfo:
  """API to describe the inferred type for a particular source symbol. This class should
  not be instantiated directly. Instances of this class are returned from
  ``CAPIStaticAnalysis.GetSymbolInfo``.
  
  Type information is accessed with the following instance 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 ``isCallable`` is ``True``.
  
  ``docString``: The docstring for the type.

  """
  
  def __init__(self, t):
    """Constructor for this class."""
    
    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 NotImplementedError('unexpected value type')
      
    self.typeName = symbolinfo.RemoveAllScopeIndicies(t.fFullName)
    
    loc, logical, self.lineStart, line_end, self.pos = t.GetPointOfDefinition()
    if self.lineStart is None:
      self.lineCount = None
    else:
      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. This class should not be instantiated directly. Use ``CAPIApplication.GetAnalysis``
  instead."""
  
  #-----------------------------------------------------------------------
  def __init__(self, singletons, ana):
    """Constructor for this class"""
    
    _CAPIBase.__init__(self, singletons, ana)
    self.fAnalysis = ana
    
  #----------------------------------------------------------------------
  @_apicall
  def GetScopeContents(self, scope, timeout=0.5):
    """Get a list of all the symbols defined in the given scope.  
    
    ``scope`` is the name of the scope to inspect. For example, ``MyClass.MyMethod`` is
    the method ``MyMethod`` in class ``MyClass`` and ``MyClass.MyMethod.nested`` is a
    nested function ``nested`` within that method.
    
    Use ``''`` for the top level of the module.
    
    To obtain attributes for an instance, append ':' to the class name. For example,
    'MyClass:' provides the attributes for an instance of ``MyClass``.
    
    Set ``timeout`` to specify the maximum computation time in seconds.

    Returns a dictionary mapping symbol names to a sequence of one or more strings
    describing the symbol. The descriptors may be::
    
      imported -- The symbol is imported from another module
      class -- The symbol is a class or class attrib when 'attrib' is also present
      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
      attrib -- The symbol is an instance attribute
      
    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', 'attrib', 
      ])
    
    scope = symbolinfo.EnsureScopeNameIndicies(scope)
    
    engine = self.fSingletons.fAnalysisMgr.fInferenceEngine
    ctx = engine.NewContext(time.time() + timeout)
    is_inst_scope = symbolinfo.IsInstanceScope(scope)
    term = symbolinfo.FullyScopedForScopeName(self.fAnalysis.fLocation,
                                              scope)
    resolved = engine.Resolve(term, ctx)
    leaf_only = (not is_inst_scope)
    all_names = engine.GetCompletionNames(resolved, for_attrib=is_inst_scope,
                                          leaf_only=leaf_only, ctx=ctx)
    for symbol, domains in list(all_names.items()):
      all_names[symbol] = tuple(d for d in domains
                                if d in _kAllowedDomainValues)
      
    return all_names
       
  #-----------------------------------------------------------------------
  @_apicall
  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."""

    scope = self.fAnalysis.FindScopeContainingLine(lineno)
    return symbolinfo.RemoveAllScopeIndicies(scope)
    
  #----------------------------------------------------------------------
  @_apicall
  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 = []
    
    scope = symbolinfo.EnsureScopeNameIndicies(scope)
    
    for info in self.fAnalysis.GetProbableTypes(symbol, scope):
      if info is not None:
        retval.append(CAPISymbolInfo(info))
    
    return retval
  
  #-----------------------------------------------------------------------
  @_apicall
  def GetLogicalLineBounds(self, lineno):
    """ Find the first and last lines that contain the logical line spanning the
    given line number. This takes into account Python syntax that allows a single logical
    line to contain multiple physical lines.  All line numbers are 0=first."""

    logical = self.fAnalysis.GetLogical(lineno)
    if not logical:
      return (lineno, lineno)
    
    return (logical.fFirstLine, logical.fFirstLine + logical.fLineCount - 1)
    
  