"""Editor extensions that also serve as examples for scripting Wing.

See the Scripting chapter of the manual for more information on writing 
and using scripts.

Copyright (c) 2005-2015, Wingware All rights reserved.
Copyright (c) 2005, Ken Kinder All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Notes:

* sort_selected() was contributed by Ken Kinder
* Improved title_case() and auto-selection contributed by
  Yves Bastide
* Parts of toggle_case() thanks to Ram Rachum

"""

from __future__ import print_function

import os
import stat
import sys
import re
import wingapi

# Scripts can be internationalize with gettext.  Strings to be translated
# are sent to _() as in the code below.
import gettext
from wingbase import textutils
if sys.version_info < (3, 0):
  _ = gettext.translation('scripts_editor_extensions', fallback=True).ugettext
else:
  _ = gettext.translation('scripts_editor_extensions', fallback=True).gettext
  
# This special attribute is used so that the script manager can translate
# also docstrings for the commands found here
_i18n_module = 'scripts_editor_extensions'

def _active_editor_exists():
  app = wingapi.gApplication
  editor = app.GetActiveEditor()
  return editor is not None


########################################################################
# Editing commands

def delete_selected_lines():
  """Delete the line or range of lines that contain the current selection.
  This duplicates what the editor command delete-line does."""

  app = wingapi.gApplication
  editor = app.GetActiveEditor()
  doc = editor.GetDocument()
  start, end = editor.GetSelection()

  start_lineno = doc.GetLineNumberFromPosition(start)
  end_lineno = doc.GetLineNumberFromPosition(end)

  line_start = doc.GetLineStart(start_lineno)
  line_end = doc.GetLineEnd(end_lineno)

  doc.DeleteChars(line_start, line_end)

delete_selected_lines.available = _active_editor_exists

def comment_block_toggle():
  """Toggle block comment (with ## at start) on the selected lines in editor.
  This is a different style of block commenting than Wing implements by default
  (the default in Wing is intended to work better with some of the other
  editor functionality)"""

  app = wingapi.gApplication
  editor = app.GetActiveEditor()
  eol = editor.GetEol()
  doc = editor.GetDocument()
  start, end = editor.GetSelection()

  start_lineno = doc.GetLineNumberFromPosition(start)
  end_lineno = doc.GetLineNumberFromPosition(end)
  if end_lineno > start_lineno and end == doc.GetLineStart(end_lineno):
    end_lineno -= 1

  line_start = doc.GetLineStart(start_lineno)
  line_end = doc.GetLineEnd(end_lineno)
  line_end = min(line_end, doc.GetLength())

  txt = doc.GetCharRange(line_start, line_end)
  txt2 = []
  add_comment = txt.startswith('##')
  if add_comment:
    for line in txt.split(eol):
      if len(line) >= 2 and line[:2] == '##':
        line = line[2:]
      txt2.append(line)
  else:
    for line in txt.split(eol):
      if len(line.strip()) > 0:
        line = '##' + line
      txt2.append(line)
  txt2 = eol.join(txt2)

  doc.BeginUndoAction()
  try:
    doc.DeleteChars(line_start, line_end - 1)
    doc.InsertChars(line_start, txt2)
    after_start, after_end = editor.GetSelection()
    if add_comment:
      moved_start = start + textutils.Utf8Len('##')
    else:
      moved_start = max(start-textutils.Utf8Len('##'), line_start)
    editor.SetSelection(moved_start, after_end)
  finally:
    doc.EndUndoAction()

comment_block_toggle.available = _active_editor_exists

def toggle_mark_command(style="char", select_right=0):
  """Change between text-marking and non-text-marking mode.  Style is
  "char" for stream select, "block" for rectangular select, and "line" for line select.
  Set select_right=1 to select the character to right of the cursor
  when marking is toggled on."""
  
  app = wingapi.gApplication
  editor = app.GetActiveEditor()
  if editor.fEditor._fSelectMode is not None:
    app.ExecuteCommand('stop-mark-command-internal')
  else:
    # Select right isn't quite the same because the editor
    # doesn't support Brief's cursor+1 selection style
    if select_right:
      app.ExecuteCommand('forward-char')
    app.ExecuteCommand('set-mark-command', unit=style)
    if select_right:
      app.ExecuteCommand('backward-char')

def _toggle_mark_command_available():
  app = wingapi.gApplication
  editor = app.GetActiveEditor()
  return editor is not None
  
toggle_mark_command.available = _toggle_mark_command_available

_last_home_operation = (None, 0, 0)
_last_home_count = 0

def cursor_home():
  """Bring cursor to start of line, to start of visible area, or to 
  start of document each successive consecutive invocation of this
  command."""
  
  global _last_home_operation
  global _last_home_count

  app = wingapi.gApplication
  editor = wingapi.gApplication.GetActiveEditor()
  doc = editor.GetDocument()

  start, end = editor.GetSelection()
  if _last_home_operation == (doc.GetFilename(), start, end):
    _last_home_count += 1
  else:
    _last_home_count = 1
    
  if _last_home_count == 1:
    app.ExecuteCommand('beginning-of-line')

  elif _last_home_count == 2:
    lineno = editor.GetFirstVisibleLine()
    start = doc.GetLineStart(lineno)
    editor.SetSelection(start, start)

  else:
    app.ExecuteCommand('start-of-document')

  start, end = editor.GetSelection()
  _last_home_operation = (doc.GetFilename(), start, end)
  
_last_end_operation = (None, 0, 0)
_last_end_count = 0

def cursor_end():
  """Bring cursor to end of line, to end of visible area, or to 
  end of document each successive consecutive invocation of this
  command."""
  
  global _last_end_operation
  global _last_end_count

  app = wingapi.gApplication
  editor = wingapi.gApplication.GetActiveEditor()
  doc = editor.GetDocument()

  start, end = editor.GetSelection()
  if _last_end_operation == (doc.GetFilename(), start, end):
    _last_end_count += 1
  else:
    _last_end_count = 1
    
  if _last_end_count == 1:
    app.ExecuteCommand('end-of-line')

  elif _last_end_count == 2:
    lineno = editor.GetFirstVisibleLine()
    # Reaching through API:
    num_lines = editor.fEditor._fScint.lines_on_screen()
    lineno += num_lines - 1
    doc = editor.GetDocument()
    end = doc.GetLineEnd(lineno)
    editor.SetSelection(end, end)
 
  else:
    app.ExecuteCommand('end-of-document')

  start, end = editor.GetSelection()
  _last_end_operation = (doc.GetFilename(), start, end)
  
def _indent_chars(line):
  indent_chars = 0
  for i, c in enumerate(line):
    if c in ' \t':
      indent_chars += 1
    else:
      break
  return indent_chars

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

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

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

  ed.SetSelection(target, target)

def _start_of_block_available():
  ed = wingapi.gApplication.GetActiveEditor()
  return ed is not None

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

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

  first_indent = None
  end_of_block_lineno = None
  end_of_block_indent = None
  initial_lineno = doc.GetLineNumberFromPosition(pos)
  lineno = initial_lineno
  while lineno < doc.GetLineCount():
    line = doc.GetCharRange(doc.GetLineStart(lineno), doc.GetLineEnd(lineno))
    indent = _indent_chars(line)
    if first_indent is None:
      # Want the 2nd line if the first ends with a ':' 
      if indent != 0 and (not line.endswith(':') or lineno > initial_lineno):
        first_indent = indent
      end_of_block_lineno = lineno
      end_of_block_indent = indent
      lineno += 1
    elif indent >= first_indent:
      end_of_block_lineno = lineno
      end_of_block_indent = indent
      lineno += 1
    elif not line.strip():
      lineno += 1
    else:
      break

  target = doc.GetLineStart(end_of_block_lineno) + end_of_block_indent

  ed.SetSelection(target, target)

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

########################################################################
# Capitalization and other transforms of the current selection

import string

def _transform_multiple_selections(xform):
  """Transform current selections or current word with the given callback """
  
  from edit import utils
  
  editor = wingapi.gApplication.GetActiveEditor()
  ed = editor.fEditor
  doc = editor.GetDocument()
  by_word_ops = wingapi.edit.utils.ByWordOps(ed._fScint.get_doc())
  
  if ed._fSelectMode == 'block':
    start, end = ed.GetSelection()
    utils.TransformRectSelection(ed, start, end, xform)
    return
  
  doc.BeginUndoAction()
  try:
    sel_iter = ed._SelectionsIter()
    
    for sel in sel_iter:
      empty_selection = (sel.end == sel.start)
      if empty_selection:
        start, end = by_word_ops.AlnumRangeForPos(sel.start)
      else:
        start = sel.start
        end = sel.end
        
      if start is not None and end is not None and end > start:
        txt = ed._fScint.get_text_range(start, end)
        xformed = xform(txt)
        if xformed != txt:
          ed.fCache.ReplaceRange(start, end, xformed)
          if not empty_selection:
            ed._fMultiSel._SetSingleSelection(sel, start=start, end=start+len(xformed))
          else:
            ed._fMultiSel._SetSingleSelection(sel, start=start+len(xformed),
                                              end=start+len(xformed))
            
  finally:
    doc.EndUndoAction()

def lower_case():
  """Change current selection or current word to all lower case"""

  def xform(utf8):
    u = textutils.AsUnicode(utf8)
    return u.lower()

  _transform_multiple_selections(xform)

def upper_case():
  """Change current selection or current word to all upper case"""

  def xform(utf8):
    u = textutils.AsUnicode(utf8)
    return u.upper()

  _transform_multiple_selections(xform)

def title_case():
  """Change current selection or current word to capitalize first letter of each word"""

  def xform(b):
    """A better version of string.capwords"""
    
    u = textutils.AsUnicode(b)
    x = "".join([word.capitalize() for word in re.split(r"([\W_\d]+)", u)])
    return x

  editor = wingapi.gApplication.GetActiveEditor()
  _transform_multiple_selections(xform)

def toggle_case():
  """Toggle current selection or current word between common name formats: my_symbol_name,
  MySymbolName, and mySymbolName"""
  
  def xform(utf8):
    if len(utf8) == 0:
      return utf8
    
    u = textutils.AsUnicode(utf8)
    # Convert to CamelCaseFormat
    if utf8 == u.lower():
      parts = u.split('_')
      x = u''.join([p.capitalize() for p in parts])
    # Convert to camelCaseFormat
    elif u[0] == u[0].upper():
      x = u[0].lower() + u[1:]
    # Convert to under_score_format
    else:
      x = re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', u).lower().strip('_')
    
    return x
  
  _transform_multiple_selections(xform)
  
def hyphen_to_under():
  """Change hyphens (dashes) to underscores in current selection or current word"""

  def xform(utf8):
    u = textutils.AsUnicode(utf8)
    x = u.replace('-', '_')
    return x
  
  _transform_multiple_selections(xform)

def under_to_hyphen():
  """Change underscores to hyphens (dashes) in current selection or current word"""

  def xform(utf8):
    u = textutils.AsUnicode(utf8)
    x = u.replace('_', '-')
    return x
  
  _transform_multiple_selections(xform)

def surround(char):
  """Surround selected text with (), [], {}, "", '', <>, or \`\`.  Arg char
  should be the opening character.  If there is no selection, the current
  word is surrounded."""
  
  ed = wingapi.gApplication.GetActiveEditor()
  if ed is None:
    return
  
  pairs = "()[]{}''\"\"<>``"
  pos = pairs.find(char)
  if pos == -1:
    return
  closing = pairs[pos+1]
  
  def xform(x):
    return char + x + closing

  _transform_multiple_selections(xform)

def sort_selected():
  """Sort selected lines of text alphabetically"""

  app = wingapi.gApplication
  editor = app.GetActiveEditor()
  doc = editor.GetDocument()
  eol = editor.GetEol()
  start, end = editor.GetSelection()

  start_lineno = doc.GetLineNumberFromPosition(start)
  end_lineno = doc.GetLineNumberFromPosition(end)
  end_line_pos = doc.GetLineStart(end_lineno)
  if end_line_pos == end:
    end_lineno -= 1
  if end_lineno <= start_lineno:
    return
  
  line_start = doc.GetLineStart(start_lineno)
  line_end = doc.GetLineEnd(end_lineno)

  text = doc.GetCharRange(line_start, line_end).split(eol)
  text.sort()

  doc.BeginUndoAction()
  try:
    doc.DeleteChars(line_start, line_end-1)
    doc.InsertChars(line_start, eol.join(text))
  finally:
    doc.EndUndoAction()
  editor.SetSelection(line_start, line_end)

def copy_filename_to_clipboard():
  """Copy the filename for the currently selected file to the clipboard"""
  filenames = wingapi.gApplication.GetCurrentFiles()
  if filenames:
    wingapi.gApplication.SetClipboard('\n'.join(filenames))
  
def remove_prompts_and_paste():
  """Paste from clipboard after removing any >>> and ... prompts"""
  
  ed = wingapi.gApplication.GetActiveEditor()
  if not ed:
    return
  
  txt = wingapi.gApplication.GetClipboard()
  if not txt:
    return
  
  lines = txt.splitlines()
  for i, line in enumerate(lines):
    if line.startswith('>>> ') or line.startswith('... '):
      lines[i] = line[4:]
      
  doc = ed.GetDocument()
  start, end = ed.GetSelection()
  doc.DeleteChars(start, end)
  doc.InsertChars(start, ed.GetEol().join(lines))


########################################################################
# Alternate key behaviors

def _move_right(editor, count):
  for i in range(0, count):
    editor.ExecuteCommand('forward-char')

def vs_tab():
  """Modified tab indentation command that acts like tab in Visual Studio."""

  app = wingapi.gApplication
  editor = app.GetActiveEditor()
  tab_size = editor.GetIndentSize()
  doc = editor.GetDocument()
  start, end = editor.GetSelection()
  if start != end:
    editor.ExecuteCommand('indent-to-match')
    return

  lineno = doc.GetLineNumberFromPosition(start)
  line_start = doc.GetLineStart(lineno)
  chars = doc.GetCharRange(line_start, start)
  if len(chars) == 0:
    editor.ExecuteCommand('indent-to-match')
  else:
    chars = chars.replace('\t', tab_size * ' ')
    indents, fraction = divmod(len(chars), tab_size)
    if fraction == 0:
      fraction = tab_size
    else:
      fraction = tab_size - fraction
    doc.InsertChars(start, ' ' * fraction)
    _move_right(editor, fraction)

def _vs_tab_available():
  app = wingapi.gApplication
  return app.GetActiveEditor() is not None
vs_tab.available = _vs_tab_available

########################################################################
# Line ending conversion commands

def _buffer_convert(filter):
  app = wingapi.gApplication
  editor = app.GetActiveEditor()
  doc = editor.GetDocument()
  txt = doc.GetText()
  txt = filter(txt)
  state = editor.GetVisualState()
  doc.BeginUndoAction()
  try:
    doc.SetText(txt)
    editor.SetVisualState(state)
  finally:
    doc.EndUndoAction()

def convert_to_crlf_lineends():
  """Convert the current editor to use CR + LF style line endings"""

  def filter(txt):
    return '\r\n'.join(txt.splitlines())
  _buffer_convert(filter)

def convert_to_cr_lineends():
  """Convert the current editor to use CR style line endings"""

  def filter(txt):
    return '\r'.join(txt.splitlines())
  _buffer_convert(filter)

def convert_to_lf_lineends():
  """Convert the current editor to use LF style line endings"""

  def filter(txt):
    return '\n'.join(txt.splitlines())
  _buffer_convert(filter)

######################################################################
# Code folding utilities

def _should_also_fold_last(editor, lineno, also_fold):
  """Utility to handle case where Scintilla puts another fold mark at the end of a multi-line def or
  class if the last line of the def/class dedents to the same level of the top line. In this case we
  also want to also fold that mark if folding the top line."""
  
  def get_indent(lineno):
    doc = editor.GetDocument()
    line = doc.GetCharRange(doc.GetLineStart(lineno), doc.GetLineEnd(lineno))
    indent_size, non_indent_col = textutils.GetIndent(line, editor.GetTabSize())
    return indent_size
  
  analysis = wingapi.gApplication.GetAnalysis(editor.GetDocument().GetFilename())
  start_line, end_line = analysis.GetLogicalLineBounds(lineno)

  if end_line != lineno and get_indent(lineno) == get_indent(end_line):
    also_fold.add(end_line)
    
def fold_python_methods():
  """Fold up all Python methods, expand all classes, and leave other fold
  points alone"""

  editor = wingapi.gApplication.GetActiveEditor()
  analysis = wingapi.gApplication.GetAnalysis(editor.GetDocument().GetFilename())

  def _leading_space(line):
    return line[:len(line)-len(line.lstrip())]
  def _indent_size(line):
    indent = _leading_space(line)
    indent = indent.replace('\t', ' ' * 8)
    return len(indent)

  class_indents = []
  class_body_start = [0]
  also_fold = set()

  def _fold_methods(line, lineno):
    
    lstrip = line.lstrip()
    if len(lstrip) > 0 and not lstrip.startswith('#') and lineno >= class_body_start[0]:
      isize = _indent_size(line)
      while len(class_indents) > 0 and class_indents[-1] >= isize:
        class_indents.pop()
        
    if lineno in also_fold:
      return 0
    
    elif lstrip.startswith('class '):
      class_indents.append(isize)
      start_line, end_line = analysis.GetLogicalLineBounds(lineno)
      class_body_start[0] = end_line + 1
      return 1
    
    elif lstrip.startswith('def ') or lstrip.startswith('async def '):
      if len(class_indents) == 0:
        return 1
      else:
        _should_also_fold_last(editor, lineno, also_fold)
        return 0
      
    else:
      return -1

  folded, expanded = editor.FoldUnfold(_fold_methods)
  return folded, expanded

def _folding_available():
  editor = wingapi.gApplication.GetActiveEditor()
  return editor is not None and editor.FoldingAvailable()

fold_python_methods.available = _folding_available

def fold_python_classes():
  """Fold up all Python classes but leave other fold points alone"""

  editor = wingapi.gApplication.GetActiveEditor()

  also_fold = set()
  
  def _fold_classes(line, lineno):
    lstrip = line.lstrip()
    if lineno in also_fold:
      return 0
    elif lstrip.startswith('class '):
      _should_also_fold_last(editor, lineno, also_fold)
      return 0
    else:
      return -1

  folded, expanded = editor.FoldUnfold(_fold_classes)
  return folded, expanded

fold_python_classes.available = _folding_available

def fold_python_defs():
  """Fold up all Python methods functions but leave other fold points alone"""

  editor = wingapi.gApplication.GetActiveEditor()

  also_fold = set()
  
  def _fold_defs(line, lineno):
    lstrip = line.lstrip()
    if lineno in also_fold:
      return 0
    elif lstrip.startswith('def ') or lstrip.startswith('async def '):
      _should_also_fold_last(editor, lineno, also_fold)
      return 0
    else:
      return -1

  folded, expanded = editor.FoldUnfold(_fold_defs)
  return folded, expanded

fold_python_defs.available = _folding_available

def fold_python_classes_and_defs():
  """Fold up all Python classes, methods, and functions but leave other fold
  points alone"""

  editor = wingapi.gApplication.GetActiveEditor()

  also_fold = set()
  
  def _fold_classes_and_defs(line, lineno):
    lstrip = line.lstrip()
    if lineno in also_fold:
      return 0
    elif lstrip.startswith('class ') or lstrip.startswith('def ') or lstrip.startswith('async def '):
      _should_also_fold_last(editor, lineno, also_fold)
      return 0
    else:
      return -1

  folded, expanded = editor.FoldUnfold(_fold_classes_and_defs)
  return folded, expanded

fold_python_classes_and_defs.available = _folding_available

def vi_fold_more():
  """Approximation of zr key binding in vim"""
  
  folded, initial_expanded = fold_python_methods()
  if not folded:
    folded, expanded = fold_python_classes_and_defs()
    isinstance(initial_expanded, set)
    folded.difference_update(initial_expanded)
    if not folded:
      editor = wingapi.gApplication.GetActiveEditor()
      editor.FoldUnfold(0)
      
vi_fold_more.available = _folding_available

def vi_fold_less():
  """Approximation of zm key binding in vim"""
  
  initial_folded, expanded = fold_python_classes_and_defs()
  if not expanded:
    folded, expanded = fold_python_methods()
    expanded.difference_update(initial_folded)
    if not expanded:
      editor = wingapi.gApplication.GetActiveEditor()
      editor.FoldUnfold(1)
        
vi_fold_less.available = _folding_available


######################################################################
# A quick way to insert debug prints for those times that stepping
# through the debugger and/or using conditional breakpoints doesn't help

kWordChars = "_[]." + string.ascii_uppercase + string.ascii_lowercase + string.digits

def _getcontext():
  app = wingapi.gApplication
  editor = app.GetActiveEditor()
  if editor is None:
    return None

  context = editor.GetSourceScope()
  if len(context) > 0:
    dirname, filename = os.path.split(context[0])
    modname, ext = os.path.splitext(filename)
  else:
    return None
  if len(context) > 1:
    lineno = context[1]
  else:
    return None
  if len(context) > 2:
    scope = modname + '.' + ''.join(context[2:])
  else:
    scope = modname

  start, end = editor.GetSelection()
  doc = editor.GetDocument()
  line_start = doc.GetLineStart(lineno)
  line_end = doc.GetLineEnd(lineno)
  linetxt = doc.GetCharRange(line_start, line_end)
  from wingbase import textutils
  var = textutils.GetNearestWord(linetxt, start - line_start, kWordChars)
  if len(var) == 0:
    return None

  return modname, lineno, scope, var

def insert_debug_print():
  """Insert a print statement to print a selected variable name and
  value, along with the file and line number."""
  context = _getcontext()
  if context is None:
    return
  modname, lineno, scope, var = context
  txt = 'print "###%s, L: %i, %s:", %s' % (scope, lineno, var, var)

  app = wingapi.gApplication  
  editor = app.GetActiveEditor()
  doc = editor.GetDocument()
  line_start = doc.GetLineStart(lineno)
  line_end = doc.GetLineEnd(lineno)
  linetxt = doc.GetCharRange(line_start, line_end)

  indent = ''
  for c in linetxt:
    if c in ' \t':
      indent += c
    else:
      break

  doc.InsertChars(line_end, editor.GetEol() + indent + txt)

def _IsAvailable_insert_debug_print():
  return _getcontext() is not None

######################################################################
# Other examples

def search_python_docs():
  """Do a search on the Python documentation for the selected text
  in the current editor"""

  editor = wingapi.gApplication.GetActiveEditor()
  if editor is None:
    return
  doc = editor.GetDocument()
  start, end = editor.GetSelection()
  txt = doc.GetCharRange(start, end)
  import urllib
  url = urllib.urlencode((('q', txt + ' site:docs.python.org'), ('ie', 'utf-8')))
  url = "http://www.google.com/search?" + url
  wingapi.gApplication.OpenURL(url)
  
def watch_selection():
  """Add a debug watch for the selected text in the current editor"""

  editor = wingapi.gApplication.GetActiveEditor()
  if editor is None:
    return
  doc = editor.GetDocument()
  start, end = editor.GetSelection()
  txt = doc.GetCharRange(start, end)
  txt = txt.strip()
  if not txt:
    return
  wingapi.gApplication.ExecuteCommand('watch-expression(expr="%s")' % txt)

def cc_checkout():
  """Check the current file out of clearcase.  This is best used with Wing
  configured to auto-reload unchanged files."""

  app = wingapi.gApplication
  editor = app.GetActiveEditor()
  filename = editor.GetDocument().GetFilename()
  cmd = 'cleartool /checkout %s' % filename
  import os
  os.system(cmd)
  is_writable = (os.stat(filename)[stat.ST_MODE] & stat.S_IWRITE) != 0
  editor.SetReadOnly(not is_writable)

def toggle_vertical_split():
    """If editor is split, unsplit it and show the vertical tools panel.
    Otherwise, hide the vertical tools and split the editor left-right
    Assumes default windowing policy (combined toolbox & editor windows).
    Thanks to Jonathan March for this script."""
    app = wingapi.gApplication
    app.ExecuteCommand('focus-current-editor')
    if app.CommandAvailable('unsplit'):
        app.ExecuteCommand('unsplit', action='current')
        app.ExecuteCommand('show-vertical-tools')
    else:
        app.ExecuteCommand('hide-vertical-tools')
        app.ExecuteCommand('split-horizontally')
  
def toggle_toolbox_separate():
    """Toggle between moving the toolboxes to a separate window and
    the default single-window mode"""
    app = wingapi.gApplication
    import guimgr.prefs
    if app.GetPreference(guimgr.prefs.kWindowingPolicy) == 'combined-window':
      app.SetPreference(guimgr.prefs.kWindowingPolicy, 'separate-toolbox-window')
    else:
      app.SetPreference(guimgr.prefs.kWindowingPolicy, 'combined-window')
      
def insert_spaces_to_tab_stop(tab_size=0):
  """Insert spaces to reach the next tab stop (units of given tab size
  or editor's tab size if none is given)"""
  
  ed = wingapi.gApplication.GetActiveEditor()
  if ed is None:
    return
  doc = ed.GetDocument()
  
  if tab_size == 0:
    tab_size = ed.GetTabSize()
    
  start, end = ed.GetSelection()
  lineno = doc.GetLineNumberFromPosition(start)
  line_start = doc.GetLineStart(lineno)
  offset = start - line_start
  remainder = offset % tab_size
  
  insert = ' ' * (tab_size - remainder)
  if start != end:
    doc.DeleteChars(start, end-1)
  doc.InsertChars(start, ' ' * (tab_size - remainder))
  
  ed.SetSelection(start + (tab_size - remainder), start + (tab_size - remainder))
  
def insert_text(text):
  """Insert given text at current caret location, replacing any existing 
  selected text"""
  
  ed = wingapi.gApplication.GetActiveEditor()
  start, end = ed.GetSelection()
  doc = wingapi.gApplication.GetActiveDocument()
  doc.DeleteChars(start, end-1)
  doc.InsertChars(start, text)
  ed.SetSelection(start + len(text), start+len(text))

def indent_new_comment_line():
  """Enter a newline, indent to match previous line and insert a comment
  character and a space.  Assumes that auto-indent is enabled."""
  
  app = wingapi.gApplication  
  ed = app.GetActiveEditor()
  app.ExecuteCommand('new-line')
  sel = ed.GetSelection()
  doc = ed.GetDocument()
  lineno = doc.GetLineNumberFromPosition(sel[0])
  if lineno == 1:
    prior_line = ''
  else:
    prior_line = doc.GetCharRange(doc.GetLineStart(lineno-1),
                                  doc.GetLineEnd(lineno-1))
  prior_line = prior_line.lstrip()
  if prior_line.startswith('#'):
    count = 0
    while prior_line.startswith('#') and count < 10:
      count += 1
      prior_line = prior_line[1:]
    prefix = '%s ' % ('#' * count)
  else:
    prefix = '# '
  app.ExecuteCommand('insert-text(text="%s")' % prefix)

# Example of connecting to presave on documents so that some operation
# can be performed just before the document is saved to disk

def _connect_to_presave(doc):
  def _on_presave(filename, encoding):
    # Avoid operation when saving a copy to another location
    if filename is not None:
      return
    # Get editor and do action
    ed = wingapi.gApplication.OpenEditor(doc.GetFilename())
    if ed is not None:
      pass  # Insert your action here
  connect_id = doc.Connect('presave', _on_presave)

def _init():
  wingapi.gApplication.Connect('document-open', _connect_to_presave)
  for doc in wingapi.gApplication.GetOpenDocuments():
    _connect_to_presave(doc)

#_init()

# Example of connecting to active-window-changed on the application

def _init2():
  def _active_window_changed(win):
    """Save all documents automatically when Wing loses focus"""
    if win is None:
      docs = wingapi.gApplication.GetOpenDocuments()
      for doc in docs:
        if not doc.IsSavePoint():
          doc.Save()
  wingapi.gApplication.Connect('active-window-changed', _active_window_changed)
  
#_init2()

# Example of using arginfo to provide word list driven autocompletion
# via a script
  
def word_list_completion(word):
  """Provide simple word-list driven auto-completion on the current editor"""
  ed = wingapi.gApplication.GetActiveEditor()
  start, end = ed.GetSelection()
  doc = wingapi.gApplication.GetActiveDocument()
  doc.DeleteChars(start, end-1)
  doc.InsertChars(start, word)
  ed.SetSelection(start + len(word), start + len(word))
  
def _ArgInfo_word_list_completion():
  from wingutils import datatype
  from guiutils import formbuilder
  from command import commandmgr

  doc = wingapi.gApplication.GetActiveDocument()
  txt = doc.GetText()
  
  txt_list = list(txt)
  word_chars = string.letters + string.digits + '_'
  for i, c in enumerate(txt_list):
    if c not in word_chars:
      txt_list[i] = ' '

  words = ''.join(txt_list).split()
  choices = set()
  for word in words:
    choices.add(word)

  arginfo = {
    'word': commandmgr.CArgInfo(
      _("Word"), datatype.CType(''),
      formbuilder.CSmallTextGui(choices=list(choices)),
      _("Word:"), 'internal'
    )
  }
  return arginfo

word_list_completion.arginfo = _ArgInfo_word_list_completion
    
from command import commandmgr
from wingutils import datatype
from guimgr import keyboard

def describe_key_briefly(key):
  """ Display the commands that a key is bound to in the status area.  Does
  not fully work for the vi binding. """
  
  s = wingapi.gApplication.fSingletons
  
  key_seq = keyboard.KeySpecTextToTuple(key)
  if not key_seq:
    wingapi.gApplication.SetStatusMessage(key + _(' is not bound'))
    return
  
  view = s.fGuiMgr.GetActiveView()
  if view is None:
    ed = wingapi.gApplication.GetActiveEditor()
    if ed is not None:
      view = ed.fEditor
  if view is None:
    return
  
  key_maps = [s.fCmdMgr.fPrimaryKeyMap]
  try:
    view_map = view.GetKeyMap()
  except AttributeError:
    pass
  else:
    if view_map is not None:
      key_maps.insert(0, view_map)
  for key_map in key_maps:
    names = key_map.GetCommandNames(key_seq)
    if names:
      break
  
  if names:
    msg = key + _(' is bound to ') + '; '.join(names)
  else:
    msg = key + _(' is not bound')
  wingapi.gApplication.SetStatusMessage(msg)
  
describe_key_briefly.arginfo = {
  'key': commandmgr.CArgInfo(
    _("Key"), datatype.CType(''),
    keyboard.CKeyStrokeEntryFormlet(),
    _("Press Key(s) then Enter"), 'internal'
    )
  }
    
def kill_line_with_eol():
  """ Variant of emacs style kill-line command that always kills the eol 
  characters """
  
  ed = wingapi.gApplication.GetActiveEditor()
  doc = ed.GetDocument()
  start, end = ed.GetSelection()
  end_lineno = doc.GetLineNumberFromPosition(start)
  line_end = doc.GetLineEnd(end_lineno)
  if end == line_end:
    text = ''
  else:
    text = doc.GetCharRange(end, line_end)
  if text.strip() == '':
    wingapi.gApplication.ExecuteCommand('kill-line')
  else:
    wingapi.gApplication.ExecuteCommand('kill-line')
    wingapi.gApplication.ExecuteCommand('kill-line')

def copy_reference(include_text=True):
  """Copy 'filename, lineno (scope)' optionally followed by the current line
  or selected lines to the clipboard.  The scope is omitted if there isn't
  one or in a non-Python file."""
  
  app = wingapi.gApplication
  editor = app.GetActiveEditor()
  if editor is None:
    return
  doc = editor.GetDocument()
  filename = doc.GetFilename()
  ana = app.GetAnalysis(filename)
  start, end = editor.GetSelection()
  startline = doc.GetLineNumberFromPosition(start)
  endline = doc.GetLineNumberFromPosition(end)
  startpos = doc.GetLineStart(startline)
  endpos = doc.GetLineEnd(endline)
  txt = doc.GetCharRange(startpos, endpos)
  if ana:
    scope = ana.FindScopeContainingLine(startline)
  else:
    scope = None
  
  clip_txt = filename + ', '
  if startline == endline:
    clip_txt += 'line %i' % (startline + 1)
  else:
    clip_txt += 'lines %i-%i' % (startline + 1, endline + 1)
  if scope:
    clip_txt += ' (%s)' % scope
  if include_text:
    clip_txt += ':' + editor.GetEol() + txt
  
  app.SetClipboard(clip_txt)
  
# Last line that was smart copied; never contains an eol
SMART_COPIED_LINE = None

def smart_copy():
  """Implement a variant of clipboard copy that copies the whole
  current line if there is no selection on the editor."""
  
  global SMART_COPIED_LINE
  
  app = wingapi.gApplication
  editor = app.GetActiveEditor()
  if editor is None:
    return
  selection = editor.GetSelection()
  if selection[1] - selection[0] > 0:
    app.ExecuteCommand('copy')
    SMART_COPIED_LINE = None
  else:
    doc = editor.GetDocument()
    lineno = doc.GetLineNumberFromPosition(selection[0])
    start = doc.GetLineStart(lineno)
    if lineno + 1 < doc.GetLineCount():
      end = doc.GetLineStart(lineno + 1)
    else:
      end = doc.GetLineEnd(lineno)
    editor.SetSelection(start, end)
    app.ExecuteCommand('copy')
    editor.SetSelection(selection[0], selection[1])
    SMART_COPIED_LINE = doc.GetCharRange(start, end)
    if SMART_COPIED_LINE.endswith(editor.GetEol()):
      SMART_COPIED_LINE = SMART_COPIED_LINE[:-len(editor.GetEol())]

def smart_cut():
  """Implement a variant of clipboard cut that cuts the whole
  current line if there is no selection on the editor."""
 
  global SMART_COPIED_LINE
 
     
  app = wingapi.gApplication
  editor = app.GetActiveEditor()
  if editor is None:
    return
  selection = editor.GetSelection()
  if selection[1] - selection[0] > 0:
    app.ExecuteCommand('cut')
    SMART_COPIED_LINE = None
  else:
    doc = editor.GetDocument()
    lineno = doc.GetLineNumberFromPosition(selection[0])
    start = doc.GetLineStart(lineno)
    if lineno + 1 < doc.GetLineCount():
      end = doc.GetLineStart(lineno + 1)
      column = selection[0] - start
      start_next_line = doc.GetLineStart(lineno + 1)
      end_next_line = doc.GetLineEnd(lineno + 1)
      if start_next_line + column > end_next_line:
        new_caret = end_next_line
      else:
        new_caret = start_next_line + column   
    else:                                          
      end = doc.GetLineEnd(lineno)
      new_caret = start
    editor.SetSelection(start, end)
    SMART_COPIED_LINE = doc.GetCharRange(start, end)
    if SMART_COPIED_LINE.endswith(editor.GetEol()):
        SMART_COPIED_LINE = SMART_COPIED_LINE[:-len(editor.GetEol())]
    app.ExecuteCommand('cut')
    new_caret -= end - start
    editor.SetSelection(new_caret, new_caret)

def smart_paste():
  """ A variant of paste that inserts line just copied with smart-copy above
  current line. """
  
  global SMART_COPIED_LINE
  
  if editor is None:
    return
  
  txt = wingapi.gApplication.GetClipboard()
  selection = editor.GetSelection()
  
  txt_lines = (txt.splitlines() if txt is not None else ())
  if txt is None or selection[0] != selection[1] or len(txt_lines) != 1 \
     or txt_lines[0] != SMART_COPIED_LINE:
    wingapi.gApplication.ExecuteCommand('paste')
  else:
    doc = editor.GetDocument()
    lineno = doc.GetLineNumberFromPosition(selection[0])
    start = doc.GetLineStart(lineno)
    column = selection[0] - start

    doc.InsertChars(start, txt_lines[0] + editor.GetEol())

kUrlChars = string.punctuation + string.ascii_uppercase + string.ascii_lowercase + string.digits
kFileChars = '-_.+#/\\' + string.ascii_uppercase + string.ascii_lowercase + string.digits

def _word_at_position(editor, pos, valid_chars):
  doc = editor.GetDocument()
  lineno = doc.GetLineNumberFromPosition(pos)
  line_start = doc.GetLineStart(lineno)
  line_end = doc.GetLineEnd(lineno)
  linetxt = doc.GetCharRange(line_start, line_end)
  from wingbase import textutils
  return textutils.GetNearestWord(linetxt, pos - line_start, valid_chars)
  
def _open_url_at_position(editor, pos):
  word = _word_at_position(editor, pos, kUrlChars)
  if not word.startswith('http:') and not word.startswith('https:'):
    print("Not a url: %s" % word)
    return
  wingapi.gApplication.OpenURL(word)
  
def _open_file_at_position(editor, pos):
  word = _word_at_position(editor, pos, kFileChars)
  if not word:
    print("Not a file name")
    return
  curdir = os.path.dirname(editor.GetDocument().GetFilename())
  savedir = os.getcwd()
  filename = word
  try:
    os.chdir(curdir)
    filename = os.path.abspath(word)
  finally:
    os.chdir(savedir)
  wingapi.gApplication.OpenEditor(filename)
  
def open_url_from_editor():
  """Open the url at caret in the current editor"""
  
  editor = wingapi.gApplication.GetActiveEditor()
  if not editor:
    return
  pos = editor.GetSelection()[0]
  _open_url_at_position(editor, pos)

def open_clicked_url_from_editor():
  """Open the url being clicked in the current editor"""
  
  editor = wingapi.gApplication.GetActiveEditor()
  if not editor:
    return
  pos = editor.GetClickLocation()
  _open_url_at_position(editor, pos)
# Uncomment the next lines to add this to the editor's context menu
#open_clicked_url_from_editor.contexts = [wingapi.kContextEditor()]
#open_clicked_url_from_editor.label = _("Open Clicked URL")

def open_filename_from_editor():
  """Open the filename at the caret in current editor"""
  
  editor = wingapi.gApplication.GetActiveEditor()
  if not editor:
    return
  pos = editor.GetSelection()[0]
  _open_file_at_position(editor, pos)

def open_clicked_filename_from_editor():
  """Open the filename being clicked in the current editor"""
  
  editor = wingapi.gApplication.GetActiveEditor()
  if not editor:
    return
  pos = editor.GetClickLocation()
  _open_file_at_position(editor, pos)
# Uncomment the next lines to add this to the editor's context menu
#open_clicked_filename_from_editor.contexts = [wingapi.kContextEditor()]
#open_clicked_filename_from_editor.label = _("Open Clicked Filename")

def batch_search_current_directory():
  """Initial batch search for the current editor's directory"""
  
  editor = wingapi.gApplication.GetActiveEditor()
  if not editor:
    return
  doc = editor.GetDocument()
  fn = doc.GetFilename()
  dirname = os.path.dirname(fn).replace('\\', '/').replace('"', r'\"')
  wingapi.gApplication.ExecuteCommand('batch-search(look_in="%s")' % dirname)


def close_all_readonly():
  """ Close all readonly files """

  to_examine = set(wingapi.gApplication.GetAllFiles())
  
  filenames_to_close = set()
  doc_list = wingapi.gApplication.GetOpenDocuments()
  for doc in doc_list:
    fn = doc.GetFilename()
    ed_list = list(doc.GetEditors())
    if len(ed_list) > 0 and fn in to_examine:
      to_examine.discard(fn)
      if any(ed.IsReadOnly() for ed in ed_list):
        filenames_to_close.add(fn)
      
  # For files for which no editor has been created, check the disk permissions
  for fn in to_examine:
    try:
      ro = not os.access(fn, os.R_OK)
    except Exception:
      pass
    else:
      if ro:
        filenames_to_close.add(fn)
      
  loc_list = [wingapi._FilenameToLocation(fn) for fn in filenames_to_close]
  gui_mgr = wingapi.gApplication.fSingletons.fGuiMgr
  gui_mgr.CloseDocuments(loc_list, can_quit=False)


def set_executable_bit(set_bit=True):
  """ Set the current file's executable bit in its permissions.  If set_bit
  is true (the default), the executable bit is set; if set_bit is false,
  the executable bit is cleared.  This doesn't do anything on windows. """
  
  doc = wingapi.gApplication.GetActiveDocument()
  
  if sys.platform == 'win32':
    return
  
  filename = doc.GetFilename()
  try:
    mode = os.stat(filename).st_mode
  except OSError:
    # Just return if can't get mode; the file may not exist
    return
  
  if set_bit:
    if mode & stat.S_IRUSR:
      mode |= stat.S_IXUSR
    if mode & stat.S_IRGRP:
      mode |= stat.S_IXGRP
    if mode & stat.S_IROTH:
      mode |= stat.S_IXOTH
  else:
    mode &= ~(stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
    
  os.chmod(filename, mode)
  
