# File: voom_vim.py
# Last Modified: 2017-02-18
# Version: 5.3
# Description: VOoM -- two-pane outliner plugin for Python-enabled Vim
# Website: http://www.vim.org/scripts/script.php?script_id=2657
# Author: Vlad Irnov (vlad DOT irnov AT gmail DOT com)
# License: CC0, see http://creativecommons.org/publicdomain/zero/1.0/

"""This module is meant to be imported by voom.vim ."""

import vim
import sys, os, re
import traceback
import bisect
# lazy imports
shuffle = None # random.shuffle

PY_VERSION = sys.version_info[0]
IS_PY2 = PY_VERSION==2
if PY_VERSION > 2:
    xrange = range

#Vim = sys.modules['__main__']

# See autoload/voom.vim ( ../../voom.vim ) for conventions.
# Function "voom_WhatEver()" in this file is Python code for Vim function
# "voom#WhatEver()" in "autoload/voom.vim" .


#---Constants and Settings--------------------{{{1=

# VO is instance of VoomOutline class, stored in dict VOOMS.
# VOOMS is created in autoload/voom.vim: less disruption if this module is reloaded.
#VOOMS = {} # {body: VO, ...}

# {filetype: make_head_<filetype> function, ...}
MAKE_HEAD = {}

# default start fold marker string and regexp
MARKER = '{{{'                            #}}}
MARKER_RE = re.compile(r'{{{(\d*[1-9]\d*)(x?)')   #}}}

# {'markdown': 'markdown', 'tex': 'latex', ...}
if vim.eval("exists('g:voom_ft_modes')")=='1':
    FT_MODES = vim.eval('g:voom_ft_modes')
else:
    FT_MODES = {}
# default markup mode
if vim.eval("exists('g:voom_default_mode')")=='1':
    DEFAULT_MODE = vim.eval('g:voom_default_mode')
else:
    DEFAULT_MODE = 'fmr'

# which Vim register to use for copy/cut/paste operations
if vim.eval("exists('g:voom_clipboard_register')")=='1':
    CLIPBOARD = vim.eval('g:voom_clipboard_register')
elif vim.eval("has('clipboard')")=='1':
    CLIPBOARD = '+'
else:
    CLIPBOARD = 'o'

# allow/disallow Move Left when nodes are not at the end of their subtree
if vim.eval("exists('g:voom_always_allow_move_left')")=='1':
    ALWAYS_ALLOW_MOVE_LEFT = int(vim.eval('g:voom_always_allow_move_left')) != 0
else:
    ALWAYS_ALLOW_MOVE_LEFT = False


#---Outline Construction----------------------{{{1o


class VoomOutline: #{{{2
    """Outline data for one Body buffer.
    Instantiated from Body by voom#Init().
    """
    def __init__(self,body):
        assert body == int(vim.eval("bufnr('')"))


def voom_Init(body): #{{{2
    VO = VoomOutline(body)
    VO.bnodes = [] # Body lnums of headlines
    VO.levels = [] # headline levels
    VO.body = body
    VO.Body = vim.current.buffer
    VO.tree = None # will set later
    VO.Tree = None # will set later
    VO.snLn = 1 # will change later if different
    # first Tree line is Body buffer name and path
    VO.bname = vim.eval('l:firstLine')
    # Body &filetype
    VO.filetype = vim.eval('&filetype')
    VO.enc = get_vim_encoding()


    ### get markup mode, l:qargs is mode's name ###
    mmode = vim.eval('l:qargs').strip() or FT_MODES.get(VO.filetype, DEFAULT_MODE)
    mName = 'voom_vimplugin2657.voom_mode_%s' %mmode
    try:
        __import__(mName)
        mModule = sys.modules[mName]
    except ImportError:
        vim.command("call voom#ErrorMsg('VOoM: cannot import Python module: %s')" %mName.replace("'","''"))
        return
        # no need to catch other import errors -- Vim code will check l:MTYPE

    VO.mModule = mModule
    VO.mmode = mmode
    VO.bname += ', %s' %mmode
    vim.command("let l:mmode='%s'" %mmode.replace("'","''"))

    ### define mode-specific methods ###
    VO.MTYPE = getattr(mModule, 'MTYPE', 1)
    # "fmr" mode, markup mode for fold markers
    if VO.MTYPE == 0:
        f = getattr(mModule, 'hook_makeOutline', 0)
        if f:
            VO.makeOutline = f
        else:
            if VO.filetype in MAKE_HEAD:
                VO.makeOutline = makeOutlineH
            else:
                VO.makeOutline = makeOutline
        VO.newHeadline = getattr(mModule, 'hook_newHeadline', 0) or newHeadline
        VO.changeLevBodyHead = changeLevBodyHead
        VO.hook_doBodyAfterOop = 0

        # start fold marker string and regexp ("fmr" modes)
        marker = vim.eval('&foldmarker').split(',')[0]
        VO.marker = marker
        if marker==MARKER:
            VO.marker_re = MARKER_RE
        else:
            VO.marker_re = re.compile(re.escape(marker) + r'(\d*[1-9]\d*)(x?)')

        # chars to strip from right side of Tree headlines ("fmr" modes)
        if vim.eval("exists('g:voom_rstrip_chars_{&ft}')")=="1":
            VO.rstrip_chars = vim.eval("g:voom_rstrip_chars_{&ft}")
        else:
            VO.rstrip_chars = vim.eval("&commentstring").split('%s')[0].strip() + " \t"

    # not an "fmr" markup mode: not for fold markers
    else:
        VO.makeOutline = getattr(mModule, 'hook_makeOutline', 0) or makeOutline
        VO.newHeadline = getattr(mModule, 'hook_newHeadline', 0) or newHeadline
        # These must be False if not defined by the markup mode.
        VO.changeLevBodyHead = getattr(mModule, 'hook_changeLevBodyHead', 0)
        VO.hook_doBodyAfterOop = getattr(mModule, 'hook_doBodyAfterOop', 0)

    ### the end ###
    # if we don't get here because of error, l:MTYPE is not set and Vim code bails out
    vim.command('let l:MTYPE=%s' %VO.MTYPE)
    VOOMS[body] = VO


def voom_TreeCreate(): #{{{2
    """This is part of voom#TreeCreate(), called from Tree."""
    body = int(vim.eval('a:body'))
    blnr = int(vim.eval('a:blnr')) # Body cursor lnum
    VO = VOOMS[body]

    # VO.MTYPE other than 0 means it is not an "fmr" mode
    if VO.MTYPE:
        computeSnLn(body, blnr)
        # reST, wiki files often have most headlines at level >1
        vim.command('setl fdl=2')
        return

    bnodes = VO.bnodes
    Body = VO.Body
    z = len(bnodes)

    ### compute snLn, create Tree folding

    # find bnode marked with '='
    # find bnodes marked with 'o'
    snLn = 0
    marker_re = VO.marker_re
    marker_re_search = marker_re.search
    oFolds = []
    for i in xrange(1,z):
        bline = Body[bnodes[i]-1]
        # part of Body headline after marker+level+'x'
        bline2 = bline[marker_re_search(bline).end():]
        if not bline2: continue
        if bline2[0]=='=':
            snLn = i+1
        elif bline2[0]=='o':
            oFolds.append(i+1)
            if bline2[1:] and bline2[1]=='=':
                snLn = i+1

    # create Tree folding
    if oFolds:
        cFolds = foldingFlip(VO,2,z,oFolds)
        foldingCreate(2,z,cFolds)

    if snLn:
        vim.command('call voom#SetSnLn(%s,%s)' %(body,snLn))
        VO.snLn = snLn
        # set blnShow if Body cursor is on or before the first headline
        if z > 1 and blnr <= bnodes[1]:
            vim.command('let l:blnShow=%s' %bnodes[snLn-1])
    else:
        # no Body headline is marked with =
        # select current Body node
        computeSnLn(body, blnr)


def makeOutline(VO, blines): #{{{2
    """Return (tlines, bnodes, levels) for Body lines blines.
    blines is either Vim buffer object (Body) or list of buffer lines.
    """
    # blines is usually Body. It is list of clipboard lines during Paste.
    # This function is slower when blines is Vim buffer object instead of
    # Python list. But overall time to do outline update is the same and memory
    # usage is less because we don't create new list (see v3.0 notes)

    # Optimized for buffers in which most lines don't have fold markers.

    # NOTE: duplicate code with makeOutlineH(), only head construction is different
    marker = VO.marker
    marker_re_search = VO.marker_re.search
    Z = len(blines)
    tlines, bnodes, levels = [], [], []
    tlines_add, bnodes_add, levels_add = tlines.append, bnodes.append, levels.append
    c = VO.rstrip_chars
    for i in xrange(Z):
        if not marker in blines[i]: continue
        bline = blines[i]
        m = marker_re_search(bline)
        if not m: continue
        lev = int(m.group(1))
        head = bline[:m.start()].lstrip().rstrip(c).strip('-=~').strip()
        tline = ' %s%s|%s' %(m.group(2) or ' ', '. '*(lev-1), head)
        tlines_add(tline)
        bnodes_add(i+1)
        levels_add(lev)
    return (tlines, bnodes, levels)


def makeOutlineH(VO, blines): #{{{2
    """Identical to makeOutline(), duplicate code. The only difference is that
    a custom function is used to construct Tree headline text.
    """
    # NOTE: duplicate code with makeOutline(), only head construction is different
    marker = VO.marker
    marker_re_search = VO.marker_re.search
    Z = len(blines)
    tlines, bnodes, levels = [], [], []
    tlines_add, bnodes_add, levels_add = tlines.append, bnodes.append, levels.append
    h = MAKE_HEAD[VO.filetype]
    for i in xrange(Z):
        if not marker in blines[i]: continue
        bline = blines[i]
        m = marker_re_search(bline)
        if not m: continue
        lev = int(m.group(1))
        head = h(bline,m)
        tline = ' %s%s|%s' %(m.group(2) or ' ', '. '*(lev-1), head)
        tlines_add(tline)
        bnodes_add(i+1)
        levels_add(lev)
    return (tlines, bnodes, levels)


#--- make_head functions --- {{{2

def make_head_html(bline,match):
    s = bline[:match.start()].strip().strip('-=~').strip()
    if s.endswith('<!'):
        return s[:-2].strip()
    else:
        return s
MAKE_HEAD['html'] = make_head_html

#def make_head_vim(bline,match):
#    return bline[:match.start()].lstrip().rstrip('" \t').strip('-=~').strip()
#MAKE_HEAD['vim'] = make_head_vim

#def make_head_py(bline,match):
#    return bline[:match.start()].lstrip().rstrip('# \t').strip('-=~').strip()
#for ft in 'python ruby perl tcl'.split():
#    MAKE_HEAD[ft] = make_head_py


def updateTree(body,tree): #{{{2
    """Construct outline for Body body.
    Update lines in Tree buffer if needed.
    This can be run from any buffer as long as Tree is set to ma.
    """
    ### Construct outline.
    VO = VOOMS[body]
    assert VO.tree == tree
    #blines = VO.Body[:] # wasteful, see v3.0 notes
    tlines, bnodes, levels  = VO.makeOutline(VO, VO.Body)
    tlines[0:0], bnodes[0:0], levels[0:0] = [VO.bname], [1], [1]
    VO.bnodes, VO.levels = bnodes, levels

    ### Add the = mark.
    snLn = VO.snLn
    Z = len(bnodes)
    # snLn got larger than the number of nodes because some nodes were
    # deleted while editing the Body
    if snLn > Z:
        snLn = Z
        vim.command('call voom#SetSnLn(%s,%s)' %(body,snLn))
        VO.snLn = snLn
    tlines[snLn-1] = '=%s' %tlines[snLn-1][1:]

    ### Compare Tree lines, draw as needed.
    # Draw all Tree lines only when needed. This is optimization for large
    # outlines, e.g. >1000 Tree lines. Drawing all lines is slower than
    # comparing all lines and then drawing nothing or just one line.

    Tree = VO.Tree
    #tlines_ = Tree[:]
    if not len(Tree)==len(tlines):
        Tree[:] = tlines
        vim.command('let l:ok=1')
        return

    # If only one line is modified, draw that line only. This ensures that
    # editing (and inserting) a single headline in a large outline is fast.
    # If more than one line is modified, draw all lines from first changed line
    # to the end of buffer.
    draw_one = False
    for i in xrange(len(tlines)):
        if not tlines[i]==Tree[i]:
            if draw_one==False:
                draw_one = True
                diff = i
            else:
                Tree[diff:] = tlines[diff:]
                vim.command('let l:ok=1')
                return
    if draw_one:
        Tree[diff] = tlines[diff]

    vim.command('let l:ok=1')
    # why l:ok is needed:  VOoM**voom_notes.txt#id_20110213212708


def computeSnLn(body, blnr): #{{{2
    """Compute Tree lnum for node at line blnr in Body body.
    Assign Vim and Python snLn vars.
    """
    # snLn should be 1 if blnr is before the first node, top of Body
    VO = VOOMS[body]
    snLn = bisect.bisect_right(VO.bnodes, blnr)
    vim.command('call voom#SetSnLn(%s,%s)' %(body,snLn))
    VO.snLn = snLn


def voom_UnVoom(body): #{{{2
    if body in VOOMS: del VOOMS[body]


def voom_Voominfo(): #{{{2
    body, tree = int(vim.eval('l:body')), int(vim.eval('l:tree'))
    vimvars = vim.eval('l:vimvars')
    print('%s CURRENT VOoM OUTLINE %s' %('-'*10, '-'*18))
    if not tree:
        print('current buffer %s is not a VOoM buffer' %body)
    else:
        VO = VOOMS[body]
        assert VO.tree == tree
        print(VO.bname)
        print('Body buffer %s, Tree buffer %s' % (body, tree))
        print('markup mode:      %s' % (VO.mmode))
        print('markup mode file: "%s"' % (os.path.abspath(VO.mModule.__file__)))
        if VO.MTYPE==0:
            print('headline markers: %s1, %s2, %s3, ...' % (VO.marker, VO.marker, VO.marker))
    print('%s VOoM INTERNALS %s' %('-'*10, '-'*24))
    print('Python version: %s' % (sys.version))
    print('s:PYCMD = %s' % repr((vim.eval('s:PYCMD'))))
    if vimvars:
        print("_VOoM2657.FT_MODES = %s" % repr(FT_MODES))
        print("_VOoM2657.DEFAULT_MODE = %s" % repr(DEFAULT_MODE))
        print("_VOoM2657.CLIPBOARD = %s" % repr(CLIPBOARD))
        print("_VOoM2657.ALWAYS_ALLOW_MOVE_LEFT = %s" % repr(ALWAYS_ALLOW_MOVE_LEFT))
        print('_VOoM2657 :     "%s"' % (os.path.abspath(sys.modules['voom_vimplugin2657.voom_vim'].__file__)))
        print(vimvars)


def voom_ReloadAllPre(): #{{{2
    if IS_PY2:
        sys.exc_clear()
    del sys.modules['voom_vimplugin2657.voom_vim']
    for k in list(sys.modules.keys()):
        #if k.startswith('voom_vimplugin2657.voom_mode_'):
        if k.startswith('voom_vimplugin2657.'):
            del sys.modules[k]


#---Outline Traversal-------------------------{{{1
# Functions for getting node's parents, children, ancestors, etc.
# Nodes here are Tree buffer lnums.
# All we do is traverse VO.levels.


def nodeHasChildren(VO, lnum): #{{{2
    """Determine if node at Tree line lnum has children."""
    levels = VO.levels
    if lnum==1 or lnum==len(levels): return False
    elif levels[lnum-1] < levels[lnum]: return True
    else: return False


def nodeSubnodes(VO, lnum): #{{{2
    """Number of all subnodes for node at Tree line lnum."""
    levels = VO.levels
    z = len(levels)
    if lnum==1 or lnum==z: return 0
    lev = levels[lnum-1]
    for i in xrange(lnum,z):
        if levels[i]<=lev:
            return i-lnum
    return z-lnum


def nodeParent(VO, lnum): #{{{2
    """Return lnum of closest parent of node at Tree line lnum."""
    levels = VO.levels
    lev = levels[lnum-1]
    if lev==1: return None
    for i in xrange(lnum-2,0,-1):
        if levels[i] < lev: return i+1


def nodeAncestors(VO, lnum): #{{{2
    """Return lnums of ancestors of node at Tree line lnum."""
    levels = VO.levels
    lev = levels[lnum-1]
    if lev==1: return []
    ancestors = []
    for i in xrange(lnum-2,0,-1):
        levi = levels[i]
        if levi < lev:
            lev = levi
            ancestors.append(i+1)
            if lev==1:
                ancestors.reverse()
                return ancestors
    # we get here if there are no nodes at level 1 (wiki mode)
    ancestors.reverse()
    return ancestors


def nodeUNL(VO, lnum): #{{{2
    """Compute UNL of node at Tree line lnum.
    Return list of headlines.
    """
    Tree = VO.Tree
    levels = VO.levels
    if lnum==1: return ['top-of-buffer']
    parents = nodeAncestors(VO,lnum)
    parents.append(lnum)
    heads = [Tree[ln-1].split('|',1)[1] for ln in parents]
    return heads


def nodeSiblings(VO, lnum): #{{{2
    """Return lnums of siblings for node at Tree line lnum.
    These are nodes with the same parent and level as lnum node. Sorted in
    ascending order. lnum itself is included. First node (line 1) is never
    included, that is minimum lnum in results is 2.
    """
    levels = VO.levels
    lev = levels[lnum-1]
    siblings = []
    # scan back
    for i in xrange(lnum-1,0,-1):
        levi = levels[i]
        if levi < lev:
            break
        elif levi==lev:
            siblings[0:0] = [i+1]
    # scan forward
    for i in xrange(lnum,len(levels)):
        levi = levels[i]
        if levi < lev:
            break
        elif levi==lev:
            siblings.append(i+1)
    return siblings


def rangeSiblings(VO, lnum1, lnum2): #{{{2
    """Return lnums of siblings for nodes in Tree range lnum1,lnum2.
    These are nodes with the same parent and level as lnum1 node.
    First node (first Tree line) is never included, that is minimum lnum in results is 2.
    Return None if range is ivalid.
    """
    if lnum1==1: lnum1 = 2
    if lnum1 > lnum2: return None
    levels = VO.levels
    lev = levels[lnum1-1]
    siblings = [lnum1]
    for i in xrange(lnum1,lnum2):
        levi = levels[i]
        # invalid range
        if levi < lev:
            return None
        elif levi==lev:
            siblings.append(i+1)
    return siblings


def getSiblingsGroups(VO, siblings): #{{{2
    """Return list of groups of siblings in the region defined by 'siblings'
    group, which is list of siblings in ascending order (Tree lnums).
    Siblings in each group are nodes with the same parent and level.
    Siblings in each group are in ascending order.
    List of groups is reverse-sorted by level of siblings and by parent lnum:
        from RIGHT TO LEFT and from BOTTOM TO TOP.
    """
    if not siblings: return []
    levels = VO.levels
    lnum1, lnum2 = siblings[0], siblings[-1]
    lnum2 = lnum2 + nodeSubnodes(VO,lnum2)

    # get all parents (nodes with children) in the range
    parents = [i for i in xrange(lnum1,lnum2) if levels[i-1]<levels[i]]
    if not parents:
        return [siblings]

    # get children for each parent
    results_dec = [(levels[lnum1-1], 0, siblings)]
    for p in parents:
        sibs = [p+1]
        lev = levels[p] # level of siblings of this parent
        for i in xrange(p+1, lnum2):
            levi = levels[i]
            if levi==lev:
                sibs.append(i+1)
            elif levi < lev:
                break
        results_dec.append((lev, p, sibs))

    results_dec.sort()
    results_dec.reverse()
    results = [i[2] for i in results_dec]
    assert len(parents)+1 == len(results)
    return results


def nodesBodyRange(VO, ln1, ln2, withSubnodes=False): #{{{2
    """Return Body start and end lnums (bln1, bln2) corresponding to nodes at
    Tree lnums ln1 to ln2. Include ln2's subnodes if withSubnodes."""
    bln1 = VO.bnodes[ln1-1]
    if withSubnodes:
        ln2 += nodeSubnodes(VO,ln2)
    if ln2 < len(VO.bnodes):
        bln2 = VO.bnodes[ln2]-1
    else:
        bln2 = len(VO.Body)
    return (bln1,bln2)
    # (bln1,bln2) can be (1,0), see voom_TreeSelect()
    # this is what we want: getbufline(body,1,0)==[]


#---Outline Navigation------------------------{{{1


def voom_TreeSelect(): #{{{2
    # Get first and last lnums of Body node for Tree line lnum.
    lnum = int(vim.eval('l:lnum'))
    body = int(vim.eval('l:body'))
    VO = VOOMS[body]
    VO.snLn = lnum
    vim.command('let l:blnum1=%s' %(VO.bnodes[lnum-1]))
    if lnum < len(VO.bnodes):
        vim.command('let l:blnum2=%s' %(VO.bnodes[lnum]-1 or 1))
    else:
        vim.command("let l:blnum2=%s" %(len(VO.Body)+1))
    # "or 1" takes care of situation when:
    # lnum is 1 (first Tree line) and first Body line is a headline.
    # In that case VO.bnodes is [1, 1, ...] and (l:blnum1,l:blnum2) is (1,0)


def voom_TreeToStartupNode(): #{{{2
    body = int(vim.eval('l:body'))
    VO = VOOMS[body]
    bnodes = VO.bnodes
    Body = VO.Body
    marker_re = VO.marker_re
    z = len(bnodes)
    # find Body headlines marked with '='
    lnums = []
    for i in xrange(1,z):
        bline = Body[bnodes[i]-1]
        # part of Body headline after marker+level+'x'+'o'
        bline2 = bline[marker_re.search(bline).end():]
        if not bline2: continue
        if bline2[0]=='=':
            lnums.append(i+1)
        elif bline2[0]=='o':
            if bline2[1:] and bline2[1]=='=':
                lnums.append(i+1)
    vim.command('let l:lnums=%s' %repr(lnums))


def voom_EchoUNL(): #{{{2
    bufType = vim.eval('l:bufType')
    body = int(vim.eval('l:body'))
    tree = int(vim.eval('l:tree'))
    lnum = int(vim.eval('l:lnum'))

    VO = VOOMS[body]
    assert VO.tree == tree

    if bufType=='Body':
        lnum = bisect.bisect_right(VO.bnodes, lnum)

    heads = nodeUNL(VO,lnum)
    UNL = ' -> '.join(heads)
    vim.command("let @n='%s'" %UNL.replace("'", "''"))
    vim.command("echo ''") # prevent fusion with previous message
    for h in heads[:-1]:
        vim.command("echon '%s'" %(h.replace("'", "''")))
        vim.command("echohl TabLineFill")
        vim.command("echon ' -> '")
        vim.command("echohl None")
    vim.command("echon '%s'" %(heads[-1].replace("'", "''")))


def voom_Grep(): #{{{2
    body = int(vim.eval('l:body'))
    tree = int(vim.eval('l:tree'))
    VO = VOOMS[body]
    assert VO.tree == tree
    bnodes = VO.bnodes
    matchesAND, matchesNOT = vim.eval('l:matchesAND'), vim.eval('l:matchesNOT')
    inhAND, inhNOT = vim.eval('l:inhAND'), vim.eval('l:inhNOT')

    # Convert blnums of mathes into tlnums, that is node numbers.
    tlnumsAND, tlnumsNOT = [], [] # lists of AND and NOT "tlnums" dicts

    # Process AND matches.
    counts = {} # {tlnum: count of all AND matches in this node, ...}
    blnums = {} # {tlnum: blnum of first AND match in this node, ...}
    inh_only = {} # tlnums of nodes added to an AND match by inheritance only
    idx = 0 # index into matchesAND and inhAND
    for L in matchesAND:
        tlnums = {} # {tlnum of node with a match:0, ...}
        for bln in L:
            bln = int(bln)
            tln = bisect.bisect_right(bnodes, bln)
            tlnums[tln] = 0
            if tln in counts:
                counts[tln]+=1
            else:
                counts[tln] = 1
            if not tln in blnums:
                blnums[tln] = bln
            elif blnums[tln] > bln or counts[tln]==1:
                blnums[tln] = bln
        # inheritace: add subnodes for each node with a match
        if int(inhAND[idx]):
            for t in list(tlnums.keys()):
                subn = nodeSubnodes(VO,t)
                for s in xrange(t+1,t+subn+1):
                    if not s in tlnums:
                        tlnums[s] = 0
                        counts[s] = 0
                        blnums[s] = bnodes[s-1]
                        # node has no match, added thank to inheritance only
                        inh_only[s] = 0
        idx+=1
        tlnumsAND.append(tlnums)

    # Process NOT matches.
    idx = 0 # index into matchesNOT and inhNOT
    for L in matchesNOT:
        tlnums = {} # {tlnum of node with a match:0, ...}
        for bln in L:
            bln = int(bln)
            tln = bisect.bisect_right(bnodes, bln)
            tlnums[tln] = 0
        # inheritace: add subnodes for each node with a match
        if int(inhNOT[idx]):
            for t in list(tlnums.keys()):
                subn = nodeSubnodes(VO,t)
                for s in xrange(t+1,t+subn+1):
                    tlnums[s] = 0
        idx+=1
        tlnumsNOT.append(tlnums)

    # There are only NOT patterns.
    if not matchesAND:
        tlnumsAND = [{}.fromkeys(xrange(1,len(bnodes)+1))]

    # Compute intersection.
    results = sorted(intersectDicts(tlnumsAND, tlnumsNOT))
    #print(results)

    # Compute max_size to left-align UNLs in the qflist.
    # Add missing data for each node in results.
    nNs = {} # {tlnum : 'N' if node has all AND matches, 'n' otherwise, ...}
    max_size = 0
    for t in results:
        # there are only NOT patterns
        if not matchesAND:
            blnums[t] = bnodes[t-1]
            counts[t] = 0
            nNs[t] = 'n'
        # some nodes in results do not contain all AND matches
        elif inh_only:
            if t in inh_only:
                nNs[t] = 'n'
            else:
                nNs[t] = 'N'
        # every node in results contains all AND matches
        else:
            nNs[t] = 'N'
        size = len('%s%s%s' %(t, counts[t], blnums[t]))
        if size > max_size:
            max_size = size

    # Make list of dictionaries for setloclist() or setqflist().
    loclist = []
    for t in results:
        size = len('%s%s%s' %(t, counts[t], blnums[t]))
        spaces = ' '*(max_size - size)
        UNL = ' -> '.join(nodeUNL(VO,t)).replace("'", "''")
        #text = 'n%s:%s%s|%s' %(t, counts[t], spaces, UNL)
        text = '%s%s:%s%s|%s' %(nNs[t], t, counts[t], spaces, UNL)
        d = "{'text':'%s', 'lnum':%s, 'bufnr':%s}, " %(text, blnums[t], body)
        loclist .append(d)
    #print('\n'.join(loclist))

    vim.command("call setqflist([%s],'a')" %(''.join(loclist)) )


def intersectDicts(dictsAND, dictsNOT): #{{{2
    """Arguments are two lists of dictionaries. Keys are Tree lnums.
    Return dict: intersection of all dicts in dictsAND and non-itersection with
    all dicts in dictsNOT.
    """
    if not dictsAND:
        return {}
    if len(dictsAND)==1:
        res = dictsAND[0]
    else:
        res = {}
        # intersection with other dicts in dictsAND
        for key in dictsAND[0]:
            for d in dictsAND[1:]:
                if not key in d:
                    break
            else:
                res[key] = 0
    # non-intersection with all dicts in dictsNOT
    for d in dictsNOT:
        for key in d:
            if key in res:
                del res[key]
    return res


#---Outline Operations------------------------{{{1o
# voom_Oop... functions are called from voom#Oop... Vim functions.
# They use local Vim vars set by the caller and can create and change Vim vars.
# Most of them set lines in Tree and Body via vim.buffer objects.
#
# l:blnShow is initially set by the VimScript caller to -1.
# Returning before setting l:blnShow means no changes were made.
# If Python code fails, l:blnShow also stays at -1.
# Subsequent VimScript code relies on l:blnShow.


def setLevTreeLines(tlines, levels, j): #{{{2
    """Set level of each Tree line in tlines to corresponding level from levels.
    levels should be VO.levels.
    j is index of the first item in levels.
    """
    results = []
    i = 0
    for t in tlines:
        results.append('%s%s%s' %(t[:2], '. '*(levels[j+i]-1), t[t.index('|'):]))
        i+=1
    return results


def changeLevBodyHead(VO, h, levDelta): #{{{2
    """Increase or decrease level number of Body headline by levDelta.
    NOTE: markup modes can replace this function with hook_changeLevBodyHead.
    """
    if levDelta==0: return h
    m = VO.marker_re.search(h)
    level = int(m.group(1))
    return '%s%s%s' %(h[:m.start(1)], level+levDelta, h[m.end(1):])


def newHeadline(VO, level, blnum, ln): #{{{2
    """Return (tree_head, bodyLines).
    tree_head is new headline string in Tree buffer (text after |).
    bodyLines is list of lines to insert in Body buffer.
    """
    tree_head = 'NewHeadline'
    bodyLines = ['---%s--- %s%s' %(tree_head, VO.marker, level), '']
    return (tree_head, bodyLines)


def setClipboard(s): #{{{2
    """Set Vim register CLIPBOARD (usually +) to string s."""
    # important: use '' for Vim string
    vim.command("let @%s = '%s'" %(CLIPBOARD, s.replace("'", "''")))

    # The above failed once: empty clipboard after copy/delete >5MB outline.
    # Could not reproduce after Windows restart. Probably stale system. Thus
    # the following check.
    # Python 2. The check adds about 0.09 sec for each 1MB in the clipboard.
    # 30-40% increase overall in the time of Copy operation (yy).

    if IS_PY2: # Python 2: s is bytes string
        len_s = len(s)
    else:      # Python 3: s is unicode string
        len_s = len(s.encode(get_vim_encoding(), 'replace'))
    if not vim.eval('len(@%s)' %CLIPBOARD) == '%s' % len_s:
        vim.command("call voom#ErrorMsg('VOoM: error setting clipboard (Vim register %s)')" %CLIPBOARD)
        # empty clipboard to prevent Paste with erroneous data
        vim.command("let @%s=''" %CLIPBOARD)
        return -1
    return 0


def voom_OopVerify(): #{{{2
    body, tree = int(vim.eval('a:body')), int(vim.eval('a:tree'))
    VO = VOOMS[body]
    assert VO.tree == tree
    ok = True

    tlines, bnodes, levels  = VO.makeOutline(VO, VO.Body)
    if not len(VO.Tree)==len(tlines)+1:
        vim.command("call voom#ErrorMsg('VOoM: outline verification failed: wrong Tree size')")
        vim.command("call voom#ErrorMsg('VOoM: OUTLINE MAY BE CORRUPT!!! YOU MUST UNDO THE LAST OPERATION!!!')")
        ok = False
        return
    tlines[0:0], bnodes[0:0], levels[0:0] = [VO.bname], [1], [1]
    snLn = VO.snLn
    tlines[snLn-1] = '=%s' %tlines[snLn-1][1:]

    if not VO.bnodes == bnodes:
        vim.command("call voom#ErrorMsg('VOoM: outline verification failed: wrong bnodes')")
        vim.command("call voom#ErrorMsg('VOoM: OUTLINE MAY BE CORRUPT!!! YOU MUST UNDO THE LAST OPERATION!!!')")
        return
    if not VO.levels == levels:
        ok = False
        vim.command("call voom#ErrorMsg('VOoM: outline verification failed: wrong levels')")
    if not VO.Tree[:] == tlines:
        ok = False
        vim.command("call voom#ErrorMsg('VOoM: outline verification failed: wrong Tree lines')")

    if ok:
        vim.command("let l:ok=1")


def voom_OopSelEnd(): #{{{2
    """This is part of voom#Oop() checks.
    Selection in Tree starts at line ln1 and ends at line ln2.
    Selection can have many sibling nodes: nodes with the same level as ln1 node.
    Return lnum of last node in the last sibling node's branch.
    Return 0 if selection is invalid.
    """
    body = int(vim.eval('l:body'))
    ln1, ln2  = int(vim.eval('l:ln1')), int(vim.eval('l:ln2'))
    if ln1==1: return 0
    levels = VOOMS[body].levels
    z, lev0 = len(levels), levels[ln1-1]
    for i in xrange(ln1,z):
        lev = levels[i]
        # invalid selection: there is node with level smaller than that of ln1 node
        if i+1 <= ln2 and lev < lev0: return 0
        # node after the last sibling node's branch
        elif i+1 > ln2 and lev <= lev0: return i
    return z


def voom_OopSelectBodyRange(): # {{{2
    body, tree = int(vim.eval('l:body')), int(vim.eval('l:tree'))
    ln1, ln2 = int(vim.eval('l:ln1')), int(vim.eval('l:ln2'))
    VO = VOOMS[body]
    assert VO.tree == tree
    bln1, bln2 = nodesBodyRange(VO, ln1, ln2)
    vim.command("let [l:bln1,l:bln2]=[%s,%s]" %(bln1,bln2))


def voom_OopEdit(): # {{{2
    body, tree = int(vim.eval('l:body')), int(vim.eval('l:tree'))
    lnum, op = int(vim.eval('l:lnum')), vim.eval('a:op')
    VO = VOOMS[body]
    assert VO.tree == tree
    if op=='i':
        bLnr = VO.bnodes[lnum-1]
    elif op=='I':
        if lnum < len(VO.bnodes):
            bLnr = VO.bnodes[lnum]-1
        else:
            bLnr = len(VO.Body)
    vim.command("let l:bLnr=%s" %(bLnr))


def voom_OopInsert(as_child=False): #{{{2
    body, tree = int(vim.eval('l:body')), int(vim.eval('l:tree'))
    ln, ln_status = int(vim.eval('l:ln')), vim.eval('l:ln_status')
    VO = VOOMS[body]
    assert VO.tree == tree
    Body, Tree, levels, snLn = VO.Body, VO.Tree, VO.levels, VO.snLn

    # Compute where to insert and at what level.
    # Insert new headline after node at ln.
    # If node is folded, insert after the end of node's tree.
    # default level
    lev = levels[ln-1]
    # after first Tree line
    if ln==1: lev=1
    # as_child always inserts as first child of current node, even if it's folded
    elif as_child: lev+=1
    # after last Tree line, same level
    elif ln==len(levels): pass
    # node has children, it can be folded
    elif lev < levels[ln]:
        # folded: insert after current node's branch, same level
        if ln_status=='folded': ln += nodeSubnodes(VO,ln)
        # not folded, insert as child
        else: lev+=1

    # remove = mark before modifying Tree
    Tree[snLn-1] = ' ' + Tree[snLn-1][1:]

    # insert headline in Tree and Body
    # bLnum is Body lnum after which to insert new headline
    if ln < len(levels):
        bLnum = VO.bnodes[ln]-1
    else:
        bLnum = len(Body)

    tree_head, bodyLines = VO.newHeadline(VO,lev,bLnum,ln)

    treeLine = '= %s|%s' %('. '*(lev-1), tree_head)
    Tree[ln:ln] = [treeLine]
    Body[bLnum:bLnum] = bodyLines

    vim.command('let l:bLnum=%s' %(bLnum+1))

    # write = mark and set snLn to new headline
    Tree[ln] = '=' + Tree[ln][1:]
    VO.snLn = ln+1
    vim.command('call voom#SetSnLn(%s,%s)' %(body, ln+1))


def voom_OopCopy(): #{{{2
    body = int(vim.eval('l:body'))
    ln1, ln2 = int(vim.eval('l:ln1')), int(vim.eval('l:ln2'))
    VO = VOOMS[body]
    Body, bnodes = VO.Body, VO.bnodes

    # body lines to copy
    bln1 = bnodes[ln1-1]
    if ln2 < len(bnodes): bln2 = bnodes[ln2]-1
    else: bln2 = len(Body)
    blines = Body[bln1-1:bln2]
    setClipboard('\n'.join(blines))

    vim.command('let l:pyOK=1')


def voom_OopCut(): #{{{2
    body, tree = int(vim.eval('l:body')), int(vim.eval('l:tree'))
    ln1, ln2 = int(vim.eval('l:ln1')), int(vim.eval('l:ln2'))
    lnUp1 = int(vim.eval('l:lnUp1'))
    VO = VOOMS[body]
    assert VO.tree == tree
    Body, Tree = VO.Body, VO.Tree
    bnodes, levels = VO.bnodes, VO.levels

    # diagram {{{
    # .............. blnUp1-1
    # ============== blnUp1=bnodes[lnUp1-1]
    # ..............
    # ============== bln1=bnodes[ln1-1]
    # range being
    # deleted
    # .............. bln2=bnodes[ln2]-1, or last Body line
    # ==============
    # .............. }}}

    ### copy and delete body lines
    bln1 = bnodes[ln1-1]
    if ln2 < len(bnodes): bln2 = bnodes[ln2]-1
    else: bln2 = len(Body)
    blines = Body[bln1-1:bln2]
    error = setClipboard('\n'.join(blines))
    if error:
        vim.command("call voom#ErrorMsg('VOoM (cut): outline operation aborted')")
        vim.command("call voom#OopFromBody(%s,%s,-1)" %(body,tree))
        vim.command('let l:pyOK=1')
        return
    Body[bln1-1:bln2] = []

    blnShow = bnodes[lnUp1-1] # does not change

    ### update bnodes
    # decrement lnums after deleted range
    delta = bln2-bln1+1
    for i in xrange(ln2,len(bnodes)):
        bnodes[i]-=delta
    # cut
    bnodes[ln1-1:ln2] = []

    ### delete range in levels (same as in Tree)
    levels[ln1-1:ln2] = []

    if VO.hook_doBodyAfterOop:
        VO.hook_doBodyAfterOop(VO, 'cut', 0,  None, None,  None, None,  bln1-1, ln1-1)

    ### ---go back to Tree---
    vim.command("call voom#OopFromBody(%s,%s,%s)" %(body,tree,blnShow))

    ### remove = mark before modifying Tree
    snLn = VO.snLn
    Tree[snLn-1] = ' ' + Tree[snLn-1][1:]
    ### delete range in Tree (same as in levels))
    Tree[ln1-1:ln2] = []

    ### add snLn mark
    Tree[lnUp1-1] = '=' + Tree[lnUp1-1][1:]
    VO.snLn = lnUp1

    # do this last to tell vim script that there were no errors
    vim.command('let l:blnShow=%s' %blnShow)
    vim.command('let l:pyOK=1')


def voom_OopPaste(): #{{{2
    body, tree = int(vim.eval('l:body')), int(vim.eval('l:tree'))
    ln, ln_status = int(vim.eval('l:ln')), vim.eval('l:ln_status')
    VO = VOOMS[body]
    assert VO.tree == tree
    Body, Tree = VO.Body, VO.Tree
    levels, bnodes = VO.levels, VO.bnodes

    ### clipboard
    pText = vim.eval('@%s' %CLIPBOARD)
    if not pText:
        vim.command("call voom#ErrorMsg('VOoM (paste): clipboard is empty')")
        vim.command("call voom#OopFromBody(%s,%s,-1)" %(body,tree))
        vim.command('let l:pyOK=1')
        return
    pBlines = pText.split('\n') # Body lines to paste
    pTlines, pBnodes, pLevels = VO.makeOutline(VO, pBlines)

    ### verify that clipboard is a valid outline
    if pBnodes==[] or pBnodes[0]!=1:
        vim.command("call voom#ErrorMsg('VOoM (paste): invalid clipboard--first line is not a headline')")
        vim.command("call voom#OopFromBody(%s,%s,-1)" %(body,tree))
        vim.command('let l:pyOK=1')
        return
    lev_ = pLevels[0]
    for lev in pLevels:
        # there is node with level smaller than that of the first node
        if lev < pLevels[0]:
            vim.command("call voom#ErrorMsg('VOoM (paste): invalid clipboard--root level error')")
            vim.command("call voom#OopFromBody(%s,%s,-1)" %(body,tree))
            vim.command('let l:pyOK=1')
            return
        # level incremented by 2 or more
        elif lev-lev_ > 1:
            vim.command("call voom#WarningMsg('VOoM (paste): inconsistent levels in clipboard--level incremented by >1', ' ')")
        lev_ = lev

    ### compute where to insert and at what level
    # insert nodes after node at ln at level lev
    # if node is folded, insert after the end of node's tree
    lev = levels[ln-1] # default level
    # after first Tree line: use level of next node in case min level is not 1 (wiki mode)
    if ln==1:
        if len(levels)>1: lev = levels[1]
        else: lev=1
    # after last Tree line, same level
    elif ln==len(levels): pass
    # node has children, it can be folded
    elif lev < levels[ln]:
        # folded: insert after current node's branch, same level
        if ln_status=='folded': ln += nodeSubnodes(VO,ln)
        # not folded, insert as child
        else: lev+=1

    ### adjust levels of nodes being inserted
    levDelta = lev - pLevels[0]
    if levDelta:
        pLevels = [(lev+levDelta) for lev in pLevels]
        f = VO.changeLevBodyHead
        if f:
            for bl in pBnodes:
                pBlines[bl-1] = f(VO, pBlines[bl-1], levDelta)

    ### insert body lines in Body
    # bln is Body lnum after which to insert
    if ln < len(bnodes): bln = bnodes[ln]-1
    else: bln = len(Body)
    Body[bln:bln] = pBlines
    blnShow = bln+1

    ### update bnodes
    # increment bnodes being pasted
    for i in xrange(0,len(pBnodes)):
        pBnodes[i]+=bln
    # increment bnodes after pasted region
    delta = len(pBlines)
    for i in xrange(ln,len(bnodes)):
        bnodes[i]+=delta
    # insert pBnodes after ln
    bnodes[ln:ln] = pBnodes

    ### insert new levels in levels
    levels[ln:ln] = pLevels

    ### start and end lnums of inserted region
    ln1 = ln+1
    ln2 = ln+len(pBnodes)

    if VO.hook_doBodyAfterOop:
        VO.hook_doBodyAfterOop(VO, 'paste', levDelta,
                    blnShow, ln1,
                    blnShow+len(pBlines)-1, ln2,
                    None, None)

    ### ---go back to Tree---
    vim.command("call voom#OopFromBody(%s,%s,%s)" %(body,tree,blnShow))

    # remove = mark before modifying Tree
    snLn = VO.snLn
    Tree[snLn-1] = ' ' + Tree[snLn-1][1:]
    ### adjust levels of new headlines, insert them in Tree
    if levDelta:
        pTlines = setLevTreeLines(pTlines, levels, ln1-1)
    Tree[ln:ln] = pTlines

    ### start and end lnums of inserted region
    vim.command('let l:ln1=%s' %ln1)
    vim.command('let l:ln2=%s' %ln2)
    # set snLn to first headline of inserted nodes
    Tree[ln1-1] = '=' + Tree[ln1-1][1:]
    VO.snLn = ln1

    # do this last to tell vim script that there were no errors
    vim.command('let l:blnShow=%s' %blnShow)
    vim.command('let l:pyOK=1')


def voom_OopUp(): #{{{2
    body, tree = int(vim.eval('l:body')), int(vim.eval('l:tree'))
    ln1, ln2 = int(vim.eval('l:ln1')), int(vim.eval('l:ln2'))
    lnUp1, lnUp2 = int(vim.eval('l:lnUp1')), int(vim.eval('l:lnUp2'))
    VO = VOOMS[body]
    assert VO.tree == tree
    Body, Tree = VO.Body, VO.Tree
    bnodes, levels = VO.bnodes, VO.levels

    # diagram {{{
    # .............. blnUp1-1
    # ============== blnUp1=bnodes[lnUp1-1]
    # range before
    # which to move
    # ..............
    # ============== bln1=bnodes[ln1-1]
    # range being
    # moved
    # .............. bln2=bnodes[ln2]-1, or last Body line
    # ==============
    # .............. }}}

    ### compute change in level
    # current level of root nodes in selection
    levOld = levels[ln1-1]
    # new level of root nodes in selection
    # lnUp1 is fist child of lnUp2, insert also as first child
    if levels[lnUp2-1] + 1 == levels[lnUp1-1]:
        levNew = levels[lnUp1-1]
    # all other cases, includes insertion after folded node
    else:
        levNew = levels[lnUp2-1]
    levDelta = levNew-levOld

    ### body lines to move
    bln1 = bnodes[ln1-1]
    if ln2 < len(bnodes): bln2 = bnodes[ln2]-1
    else: bln2 = len(Body)
    blines = Body[bln1-1:bln2]
    if levDelta:
        f = VO.changeLevBodyHead
        if f:
            for bl in bnodes[ln1-1:ln2]:
                blines[bl-bln1] = f(VO, blines[bl-bln1], levDelta)

    ### move body lines: cut, then insert
    # insert before line blnUp1, it will not change after bnodes update
    blnUp1 = bnodes[lnUp1-1]
    blnShow = blnUp1
    Body[bln1-1:bln2] = []
    Body[blnUp1-1:blnUp1-1] = blines

    ###update bnodes
    # increment lnums in the range before which the move is made
    delta = bln2-bln1+1
    for i in xrange(lnUp1-1,ln1-1):
        bnodes[i]+=delta
    # decrement lnums in the range which is being moved
    delta = bln1-blnUp1
    for i in xrange(ln1-1,ln2):
        bnodes[i]-=delta
    # cut, insert
    nLines = bnodes[ln1-1:ln2]
    bnodes[ln1-1:ln2] = []
    bnodes[lnUp1-1:lnUp1-1] = nLines

    ### update levels (same as for Tree)
    nLevels = levels[ln1-1:ln2]
    if levDelta:
        nLevels = [(lev+levDelta) for lev in nLevels]
    # cut, then insert
    levels[ln1-1:ln2] = []
    levels[lnUp1-1:lnUp1-1] = nLevels

    if VO.hook_doBodyAfterOop:
        VO.hook_doBodyAfterOop(VO, 'up', levDelta,
                    blnShow, lnUp1,
                    blnShow+len(blines)-1, lnUp1+len(nLevels)-1,
                    bln1-1+len(blines), ln1-1+len(nLevels))

    ### ---go back to Tree---
    vim.command("call voom#OopFromBody(%s,%s,%s)" %(body,tree,blnShow))

    ### remove snLn mark before modifying Tree
    snLn = VO.snLn
    Tree[snLn-1] = ' ' + Tree[snLn-1][1:]

    ### update Tree (same as for levels)
    tlines = Tree[ln1-1:ln2]
    if levDelta:
        tlines = setLevTreeLines(tlines, levels, lnUp1-1)
    # cut, then insert
    Tree[ln1-1:ln2] = []
    Tree[lnUp1-1:lnUp1-1] = tlines

    ### add snLn mark
    Tree[lnUp1-1] = '=' + Tree[lnUp1-1][1:]
    VO.snLn = lnUp1

    # do this last to tell vim script that there were no errors
    vim.command('let l:blnShow=%s' %blnShow)
    vim.command('let l:pyOK=1')


def voom_OopDown(): #{{{2
    body, tree = int(vim.eval('l:body')), int(vim.eval('l:tree'))
    ln1, ln2 = int(vim.eval('l:ln1')), int(vim.eval('l:ln2'))
    lnDn1, lnDn1_status = int(vim.eval('l:lnDn1')), vim.eval('l:lnDn1_status')
    # note: lnDn1 == ln2+1
    VO = VOOMS[body]
    assert VO.tree == tree
    Body, Tree = VO.Body, VO.Tree
    bnodes, levels = VO.bnodes, VO.levels

    # diagram {{{
    # ..............
    # ============== bln1=bnodes[ln1-1]
    # range being
    # moved
    # .............. bln2=bnodes[ln2]-1
    # ============== blnDn1=bnodes[lnDn1-1]
    # range after
    # which to move
    # .............. blnIns=bnodes[lnIns]-1, or last Body line
    # ==============
    # .............. }}}

    ### compute change in level, and line after which to insert
    # current level
    levOld = levels[ln1-1]
    # new level is either that of lnDn1 or +1
    levNew = levels[lnDn1-1]
    # line afer which to insert
    lnIns = lnDn1
    if lnDn1==len(levels): # end of Tree
        pass
    # lnDn1 has children; insert as child unless it's folded
    elif levels[lnDn1-1] < levels[lnDn1]:
        if lnDn1_status=='folded':
            lnIns += nodeSubnodes(VO,lnDn1)
        else:
            levNew+=1
    levDelta = levNew-levOld

    ### body lines to move
    bln1 = bnodes[ln1-1]
    bln2 = bnodes[ln2]-1
    blines = Body[bln1-1:bln2]
    if levDelta:
        f = VO.changeLevBodyHead
        if f:
            for bl in bnodes[ln1-1:ln2]:
                blines[bl-bln1] = f(VO, blines[bl-bln1], levDelta)

    ### move body lines: insert, then cut
    if lnIns < len(bnodes): blnIns = bnodes[lnIns]-1
    else: blnIns = len(Body)
    Body[blnIns:blnIns] = blines
    Body[bln1-1:bln2] = []

    ### update bnodes
    # increment lnums in the range which is being moved
    delta = blnIns-bln2
    for i in xrange(ln1-1,ln2):
        bnodes[i]+=delta
    # decrement lnums in the range after which the move is made
    delta = bln2-bln1+1
    for i in xrange(ln2,lnIns):
        bnodes[i]-=delta
    # insert, cut
    nLines = bnodes[ln1-1:ln2]
    bnodes[lnIns:lnIns] = nLines
    bnodes[ln1-1:ln2] = []

    ### compute and set new snLn, blnShow
    snLn_ = VO.snLn
    snLn = lnIns+1-(ln2-ln1+1)
    VO.snLn = snLn
    vim.command('let snLn=%s' %snLn)

    blnShow = bnodes[snLn-1] # must compute after bnodes update

    ### update levels (same as for Tree)
    nLevels = levels[ln1-1:ln2]
    if levDelta:
        nLevels = [(lev+levDelta) for lev in nLevels]
    # insert, then cut
    levels[lnIns:lnIns] = nLevels
    levels[ln1-1:ln2] = []

    if VO.hook_doBodyAfterOop:
        VO.hook_doBodyAfterOop(VO, 'down', levDelta,
                    blnShow, snLn,
                    blnShow+len(blines)-1, snLn+len(nLevels)-1,
                    bln1-1, ln1-1)

    ### ---go back to Tree---
    vim.command("call voom#OopFromBody(%s,%s,%s)" %(body,tree,blnShow))

    ### remove snLn mark before modifying Tree
    Tree[snLn_-1] = ' ' + Tree[snLn_-1][1:]

    ### update Tree (same as for levels)
    tlines = Tree[ln1-1:ln2]
    if levDelta:
        tlines = setLevTreeLines(tlines, levels, snLn-1)
    # insert, then cut
    Tree[lnIns:lnIns] = tlines
    Tree[ln1-1:ln2] = []

    ### add snLn mark
    Tree[snLn-1] = '=' + Tree[snLn-1][1:]

    # do this last to tell vim script that there were no errors
    vim.command('let l:blnShow=%s' %blnShow)
    vim.command('let l:pyOK=1')


def voom_OopRight(): #{{{2
    body, tree = int(vim.eval('l:body')), int(vim.eval('l:tree'))
    ln1, ln2 = int(vim.eval('l:ln1')), int(vim.eval('l:ln2'))
    VO = VOOMS[body]
    assert VO.tree == tree
    Body, Tree = VO.Body, VO.Tree
    bnodes, levels = VO.bnodes, VO.levels

    ### Move right means increment level by 1 for all nodes in the range.

    cannotmove = False
    # can't move right if ln1 node is child of previous node
    if levels[ln1-1] > levels[ln1-2]:
        cannotmove = True
    # move right is not allowed
    elif VO.MTYPE > 1:
        cannotmove = True
        vim.command("call voom#ErrorMsg('VOoM: operation ''Move Right'' is not available in this markup mode')")
    if cannotmove:
        vim.command("let &fdm=b_fdm")
        vim.command("call voom#OopFromBody(%s,%s,-1)" %(body,tree))
        vim.command('let l:doverif=0')
        vim.command('let l:pyOK=1')
        return

    ### change levels of Body headlines
    f = VO.changeLevBodyHead
    if f:
        for bln in bnodes[ln1-1:ln2]:
            Body[bln-1] = f(VO, Body[bln-1], 1)

    # new snLn will be set to ln1
    blnShow = bnodes[ln1-1]

    ### change levels of VO.levels (same as for Tree)
    nLevels = levels[ln1-1:ln2]
    nLevels = [(lev+1) for lev in nLevels]
    levels[ln1-1:ln2] = nLevels

    if VO.hook_doBodyAfterOop:
        if ln2 < len(bnodes): blnum2 = bnodes[ln2]-1
        else: blnum2 = len(Body)
        VO.hook_doBodyAfterOop(VO, 'right', 1, blnShow, ln1, blnum2, ln2, None, None)

    ### ---go back to Tree---
    vim.command("let &fdm=b_fdm")
    vim.command("call voom#OopFromBody(%s,%s,%s)" %(body,tree,blnShow))

    ### change levels of Tree lines (same as for VO.levels)
    tlines = Tree[ln1-1:ln2]
    tlines = setLevTreeLines(tlines, levels, ln1-1)
    Tree[ln1-1:ln2] = tlines

    ### set snLn to ln1
    snLn = VO.snLn
    if not snLn==ln1:
        Tree[snLn-1] = ' ' + Tree[snLn-1][1:]
        snLn = ln1
        Tree[snLn-1] = '=' + Tree[snLn-1][1:]
        VO.snLn = snLn

    # do this last to tell vim script that there were no errors
    vim.command('let l:blnShow=%s' %blnShow)
    vim.command('let l:pyOK=1')


def voom_OopLeft(): #{{{2
    body, tree = int(vim.eval('l:body')), int(vim.eval('l:tree'))
    ln1, ln2 = int(vim.eval('l:ln1')), int(vim.eval('l:ln2'))
    VO = VOOMS[body]
    assert VO.tree == tree
    Body, Tree = VO.Body, VO.Tree
    bnodes, levels = VO.bnodes, VO.levels

    ### Move left means decrement level by 1 for all nodes in the range.

    cannotmove = False
    # can't move left if at top level 1
    if levels[ln1-1] <= 1:
        cannotmove = True
    # don't move left if the range is not at the end of subtree
    elif not ALWAYS_ALLOW_MOVE_LEFT and ln2 < len(levels) and levels[ln2]==levels[ln1-1]:
        cannotmove = True
    # move left is not allowed
    elif VO.MTYPE > 1:
        cannotmove = True
        vim.command("call voom#ErrorMsg('VOoM: operation ''Move Left'' is not available in this markup mode')")
    if cannotmove:
        vim.command("let &fdm=b_fdm")
        vim.command("call voom#OopFromBody(%s,%s,-1)" %(body,tree))
        vim.command('let l:doverif=0')
        vim.command('let l:pyOK=1')
        return

    ### change levels of Body headlines
    f = VO.changeLevBodyHead
    if f:
        for bln in bnodes[ln1-1:ln2]:
            Body[bln-1] = f(VO, Body[bln-1], -1)

    # new snLn will be set to ln1
    blnShow = bnodes[ln1-1]

    ### change levels of VO.levels (same as for Tree)
    nLevels = levels[ln1-1:ln2]
    nLevels = [(lev-1) for lev in nLevels]
    levels[ln1-1:ln2] = nLevels

    if VO.hook_doBodyAfterOop:
        if ln2 < len(bnodes): blnum2 = bnodes[ln2]-1
        else: blnum2 = len(Body)
        VO.hook_doBodyAfterOop(VO, 'left', -1, blnShow, ln1, blnum2, ln2, None, None)

    ### ---go back to Tree---
    vim.command("let &fdm=b_fdm")
    vim.command("call voom#OopFromBody(%s,%s,%s)" %(body,tree,blnShow))

    ### change levels of Tree lines (same as for VO.levels)
    tlines = Tree[ln1-1:ln2]
    tlines = setLevTreeLines(tlines, levels, ln1-1)
    Tree[ln1-1:ln2] = tlines

    ### set snLn to ln1
    snLn = VO.snLn
    if not snLn==ln1:
        Tree[snLn-1] = ' ' + Tree[snLn-1][1:]
        snLn = ln1
        Tree[snLn-1] = '=' + Tree[snLn-1][1:]
        VO.snLn = snLn

    # do this last to tell vim script that there were no errors
    vim.command('let l:blnShow=%s' %blnShow)
    vim.command('let l:pyOK=1')


def voom_OopMark(): # {{{2
    body, tree = int(vim.eval('l:body')), int(vim.eval('l:tree'))
    ln1, ln2 = int(vim.eval('l:ln1')), int(vim.eval('l:ln2'))
    VO = VOOMS[body]
    assert VO.tree == tree
    Body, Tree = VO.Body, VO.Tree
    bnodes, levels = VO.bnodes, VO.levels
    marker_re = VO.marker_re

    for i in xrange(ln1-1,ln2):
        # insert 'x' in Tree line
        tline = Tree[i]
        if tline[1]!='x':
            Tree[i] = '%sx%s' %(tline[0], tline[2:])
            # insert 'x' in Body headline
            bln = bnodes[i]
            bline = Body[bln-1]
            end = marker_re.search(bline).end(1)
            Body[bln-1] = '%sx%s' %(bline[:end], bline[end:])

    vim.command('let l:pyOK=1')


def voom_OopUnmark(): # {{{2
    body, tree = int(vim.eval('l:body')), int(vim.eval('l:tree'))
    ln1, ln2 = int(vim.eval('l:ln1')), int(vim.eval('l:ln2'))
    VO = VOOMS[body]
    assert VO.tree == tree
    Body, Tree = VO.Body, VO.Tree
    bnodes, levels = VO.bnodes, VO.levels
    marker_re = VO.marker_re

    for i in xrange(ln1-1,ln2):
        # remove 'x' from Tree line
        tline = Tree[i]
        if tline[1]=='x':
            Tree[i] = '%s %s' %(tline[0], tline[2:])
            # remove 'x' from Body headline
            bln = bnodes[i]
            bline = Body[bln-1]
            end = marker_re.search(bline).end(1)
            # remove one 'x', not enough
            #Body[bln-1] = '%s%s' %(bline[:end], bline[end+1:])
            # remove all consecutive 'x' chars
            Body[bln-1] = '%s%s' %(bline[:end], bline[end:].lstrip('x'))

    vim.command('let l:pyOK=1')


def voom_OopMarkStartup(): # {{{2
    body, tree = int(vim.eval('l:body')), int(vim.eval('l:tree'))
    ln = int(vim.eval('l:ln'))
    VO = VOOMS[body]
    assert VO.tree == tree
    Body, Tree = VO.Body, VO.Tree
    bnodes, levels = VO.bnodes, VO.levels
    marker_re = VO.marker_re

    if ln==1:
        bln_selected = 0
    else:
        bln_selected = bnodes[ln-1]
    # remove '=' from all other Body headlines
    # also, strip 'x' and 'o' after removed '='
    for bln in bnodes[1:]:
        if bln==bln_selected: continue
        bline = Body[bln-1]
        end = marker_re.search(bline).end()
        bline2 = bline[end:]
        if not bline2: continue
        if bline2[0]=='=':
            Body[bln-1] = '%s%s' %(bline[:end], bline[end:].lstrip('=xo'))
        elif bline2[0]=='o' and bline2[1:] and bline2[1]=='=':
            Body[bln-1] = '%s%s' %(bline[:end+1], bline[end+1:].lstrip('=xo'))

    if ln==1:
        vim.command('let l:pyOK=1')
        return

    # insert '=' in current Body headline, but only if it's not there already
    bline = Body[bln_selected-1]
    end = marker_re.search(bline).end()
    bline2 = bline[end:]
    if not bline2:
        Body[bln_selected-1] = '%s=' %bline
        vim.command('let l:pyOK=1')
        return
    if bline2[0]=='=':
        vim.command('let l:pyOK=1')
        return
    elif bline2[0]=='o' and bline2[1:] and bline2[1]=='=':
        vim.command('let l:pyOK=1')
        return
    elif bline2[0]=='o':
        end+=1
    Body[bln_selected-1] = '%s=%s' %(bline[:end], bline[end:])

    vim.command('let l:pyOK=1')


#--- Tree Folding Operations --- {{{2
# Opened/Closed Tree buffer folds are equivalent to Expanded/Contracted nodes.
# By default, folds are closed.
# Opened folds are marked by 'o' in Body headlines (after 'x', before '=').
#
# To determine which folds are currently closed/opened, we open all closed
# folds one by one, from top to bottom, starting from top level visible folds.
# This produces list of closed folds.
#
# To restore folding according to a list of closed folds:
#   open all folds;
#   close folds from bottom to top.
#
# Conventions:
#   cFolds --lnums of closed folds
#   oFolds --lnums of opened folds
#   ln, ln1, ln2  --Tree line number
#
# NOTE: Cursor position and window view are not restored here.
# See also:  VOoM**voom_notes.txt#id_20110120011733


def voom_OopFolding(action): #{{{3
    body, tree = int(vim.eval('l:body')), int(vim.eval('l:tree'))
    VO = VOOMS[body]
    assert VO.tree == tree
    # check and adjust range lnums
    # don't worry about invalid range lnums: Vim checks that
    if not action=='cleanup':
        ln1, ln2 = int(vim.eval('a:ln1')), int(vim.eval('a:ln2'))
        if ln2<ln1: ln1,ln2=ln2,ln1 # probably redundant
        if ln2==1:
            vim.command('let l:pyOK=1')
            return
        #if ln1==1: ln1=2
        if ln1==ln2:
            ln2 = ln2 + nodeSubnodes(VO, ln2)
            if ln1==ln2:
                vim.command('let l:pyOK=1')
                return

    if action=='save':
        cFolds = foldingGet(ln1, ln2)
        foldingWrite(VO, ln1, ln2, cFolds)
    elif action=='restore':
        cFolds = foldingRead(VO, ln1, ln2)
        foldingCreate(ln1, ln2, cFolds)
    elif action=='cleanup':
        foldingCleanup(VO)

    vim.command('let l:pyOK=1')


def foldingGet(ln1, ln2): #{{{3
    """Get all closed folds in line range ln1-ln2, including subfolds.
    If line ln2 is visible and is folded, its subfolds are included.
    Executed in Tree buffer.
    """
    cFolds = []
    lnum = ln1
    # go through top level folded lines (visible closed folds)
    while lnum < ln2+1:
        # line lnum is first line of a closed fold
        if int(vim.eval('foldclosed(%s)' %lnum))==lnum:
            cFolds.append(lnum)
            # line after this fold and subfolds
            foldend = int(vim.eval('foldclosedend(%s)' %lnum))+1
            lnum0 = lnum
            lnum = foldend
            vim.command('keepj normal! %sGzo' %lnum0)
            # open every folded line in this fold
            for ln in xrange(lnum0+1, foldend):
                # line ln is first line of a closed fold
                if int(vim.eval('foldclosed(%s)' %ln))==ln:
                    cFolds.append(ln)
                    vim.command('keepj normal! %sGzo' %ln)
        else:
            lnum+=1

    cFolds.reverse()
    # close back opened folds
    for ln in cFolds:
        vim.command('keepj normal! %sGzc' %ln)
    return cFolds


def foldingCreate(ln1, ln2, cFolds): #{{{3
    """Create folds in range ln1-ln2 from a list of closed folds in that range.
    The list must be reverse sorted. Must not contain nodes without children.
    Executed in Tree buffer.
    """
    #cFolds.sort()
    #cFolds.reverse()
    #vim.command('%s,%sfoldopen!' %(ln1,ln2))
    # see  VOoM**voom_notes.txt#id_20110120011733
    vim.command(r'try | %s,%sfoldopen! | catch /^Vim\%%((\a\+)\)\=:E490/ | endtry'
            %(ln1,ln2))
    for ln in cFolds:
        vim.command('keepj normal! %sGzc' %ln)


def foldingFlip(VO, ln1, ln2, folds): #{{{3
    """Convert list of opened/closed folds in range ln1-ln2 into list of
    closed/opened folds.
    """
    # Important: this also eliminates lnums of nodes without children,
    # so we don't get Vim E490 (no fold found) error on :foldclose.
    folds = {}.fromkeys(folds)
    folds_flipped = []
    for ln in xrange(ln1,ln2+1):
        if nodeHasChildren(VO, ln) and not ln in folds:
            folds_flipped.append(ln)
    folds_flipped.reverse()
    return folds_flipped


def foldingRead(VO, ln1, ln2): #{{{3
    """Read "o" marks in Body headlines."""
    cFolds = []
    marker_re = VO.marker_re
    bnodes = VO.bnodes
    Body = VO.Body

    for ln in xrange(ln1,ln2+1):
        if not nodeHasChildren(VO, ln):
            continue
        bline = Body[bnodes[ln-1]-1]
        end = marker_re.search(bline).end()
        if end<len(bline) and bline[end]=='o':
            continue
        else:
            cFolds.append(ln)

    cFolds.reverse()
    return cFolds


def foldingWrite(VO, ln1, ln2, cFolds): #{{{3
    """Write "o" marks in Body headlines."""
    cFolds = {}.fromkeys(cFolds)
    marker_re = VO.marker_re
    bnodes = VO.bnodes
    Body = VO.Body

    for ln in xrange(ln1,ln2+1):
        if not nodeHasChildren(VO, ln):
            continue
        bln = bnodes[ln-1]
        bline = Body[bln-1]
        end = marker_re.search(bline).end()
        isClosed = ln in cFolds
        # headline is marked with 'o'
        if end<len(bline) and bline[end]=='o':
            # remove 'o' mark
            if isClosed:
                Body[bln-1] = '%s%s' %(bline[:end], bline[end:].lstrip('ox'))
        # headline is not marked with 'o'
        else:
            # add 'o' mark
            if not isClosed:
                if end==len(bline):
                    Body[bln-1] = '%so' %bline
                elif bline[end] != 'o':
                    Body[bln-1] = '%so%s' %(bline[:end], bline[end:])


def foldingCleanup(VO): #{{{3
    """Remove "o" marks from  from nodes without children."""
    marker_re = VO.marker_re
    bnodes = VO.bnodes
    Body = VO.Body

    for ln in xrange(2,len(bnodes)+1):
        if nodeHasChildren(VO, ln): continue
        bln = bnodes[ln-1]
        bline = Body[bln-1]
        end = marker_re.search(bline).end()
        if end<len(bline) and bline[end]=='o':
            Body[bln-1] = '%s%s' %(bline[:end], bline[end:].lstrip('ox'))


#--- Sort Operations --- {{{2
# 1) Sort siblings of the current node.
# - Get list of siblings of the current node (as Tree lnums).
#   Two nodes are siblings if they have the same parent and the same level.
# - Construct list of corresponding Tree headlines. Decorate with indexes and
#   Tree lnums. Sort by headline text.
# - Construct new Body region from nodes in sorted order. Replace the region.
#   IMPORTANT: this does not change outline data (Tree, VO.levels, VO.bnodes)
#   for nodes with smaller levels or for nodes outside of the siblings region.
#   Thus, recursive sort is possible.
#
# 2) Deep (recursive) sort: sort siblings of the current node and siblings in
# all subnodes. Sort as above for all groups of siblings in the affected
# region, starting from the most deeply nested.
# - Construct list of groups of all siblings: top to bottom, decorate each
#   siblings group with level and parent lnum.
# - Reverse sort the list by levels.
# - Do sort for each group of siblings in the list: from right to left and from
#   bottom to top.
#
# 3) We modify only the Body buffer. We then do global outline update to redraw
# the Tree and to update outline data. Performing targeted update as in other
# outline operations is too tedious.


def voom_OopSort(): #{{{3
    # Returning before setting l:blnShow means no changes were made.
    ### parse options {{{
    oDeep = False
    D = {'oIgnorecase':0, 'oBytes':0, 'oEnc':0, 'oReverse':0, 'oFlip':0, 'oShuffle':0}
    options = vim.eval('a:qargs')
    options = options.strip().split()
    for o in options:
        if o=='deep': oDeep = True
        elif o=='i':       D['oIgnorecase'] = 1
        elif o=='r':       D['oReverse']    = 1 # sort in reverse order
        elif o=='flip':    D['oFlip']       = 1 # reverse without sorting
        elif o=='shuffle': D['oShuffle']    = 1
        elif o=='bytes':       D['oBytes']    = 1
        else:
            vim.command("call voom#ErrorMsg('VOoM (sort): invalid option: %s')" %o.replace("'","''"))
            vim.command("call voom#WarningMsg('VOoM (sort): valid options are: deep, i (ignore-case), r (reverse-sort), flip, shuffle, bytes')")
            vim.command('let l:pyOK=1')
            return

    if (D['oReverse'] + D['oFlip'] + D['oShuffle']) > 1:
        vim.command("call voom#ErrorMsg('VOoM (sort): these options cannot be combined: r, flip, shuffle')")
        vim.command('let l:pyOK=1')
        return

    if D['oShuffle']:
        global shuffle
        if shuffle is None: from random import shuffle

    D['oEnc'] = get_vim_encoding()
    ###### }}}

    ### get other Vim data, compute 'siblings' {{{
    body, tree = int(vim.eval('l:body')), int(vim.eval('l:tree'))
    ln1, ln2 = int(vim.eval('l:ln1')), int(vim.eval('l:ln2'))
    VO = VOOMS[body]
    assert VO.tree == tree
    Body, Tree = VO.Body, VO.Tree
    bnodes, levels = VO.bnodes, VO.levels

    if ln1==ln2:
        # Tree lnums of all siblings of the current node
        siblings = nodeSiblings(VO,ln1)
    else:
        # Tree lnums of all siblings in the range
        siblings = rangeSiblings(VO,ln1,ln2)
        if not siblings:
            vim.command("call voom#ErrorMsg('VOoM (sort): invalid Tree selection')")
            vim.command('let l:pyOK=1')
            return
    ###### }}}
    #print('ln1=%s ln2=%s siblings=%s' % (ln1, ln2, siblings))

    ### do sorting
    # progress flags: (got >1 siblings, order changed after sort)
    flag1,flag2 = 0,0
    if not oDeep:
        flag1,flag2 = sortSiblings(VO, siblings, **D)
    else:
        siblings_groups = getSiblingsGroups(VO,siblings)
        for group in siblings_groups:
            m, n = sortSiblings(VO, group, **D)
            flag1+=m; flag2+=n

    if flag1==0:
        vim.command("call voom#WarningMsg('VOoM (sort): nothing to sort')")
        vim.command('let l:pyOK=1')
        return
    elif flag2==0:
        vim.command("call voom#WarningMsg('VOoM (sort): already sorted')")
        vim.command('let l:pyOK=1')
        return

    # Show first sibling. Tracking the current node and bnode is too hard.
    lnum1 = siblings[0]
    lnum2 = siblings[-1] + nodeSubnodes(VO,siblings[-1])
    blnShow = bnodes[lnum1-1]
    vim.command('let [l:blnShow,l:lnum1,l:lnum2]=[%s,%s,%s]' %(blnShow, lnum1, lnum2))
    vim.command('let l:pyOK=1')


def sortSiblings(VO, siblings, oIgnorecase, oBytes, oEnc, oReverse, oFlip, oShuffle): #{{{3
    """Sort sibling nodes. 'siblings' is list of Tree lnums in ascending order.
    This only modifies Body buffer. Outline data are not updated.
    Return progress flags (flag1,flag2), see voom_OopSort().
    """
    sibs = siblings
    if len(sibs) < 2:
        return (0,0)
    Body, Tree = VO.Body, VO.Tree
    bnodes, levels = VO.bnodes, VO.levels
    z, Z = len(sibs), len(bnodes)

    sibs_dec = [] # list of siblings for sorting
    if oFlip or oShuffle: # flip or shuffle: Tree headlines don't matter
        # make list of siblings for sorting
        #       [(0, index, lnum), ...]
        for i in xrange(z):
            sib = sibs[i]
            sibs_dec.append((0, i, sib))
        if oFlip:
            sibs_dec.reverse()
        elif oShuffle:
            shuffle(sibs_dec)
    else: # sort, reverse sort: according to Tree headlines
        # make list of siblings for sorting, decorate with headline text
        #       [(Tree headline text, index, lnum), ...]
        for i in xrange(z):
            sib = sibs[i]
            head = Tree[sib-1].split('|',1)[1]
            if IS_PY2:
                if not oBytes:
                    head = unicode(head, oEnc, 'replace')
            else:
                if oBytes:
                    head = bytes(head, oEnc, 'replace')
            if oIgnorecase:
                head = head.lower()
            sibs_dec.append((head, i, sib))
        if oReverse: # reverse sort
            sibs_dec.sort(key=lambda x: x[0], reverse=True)
        else: # sort
            sibs_dec.sort()

    sibs_sorted = [i[2] for i in sibs_dec]
    #print(sibs_dec); print(sibs_sorted)
    # don't sort if already sorted (can happen with shuffle)
    if sibs==sibs_sorted:
        return (1,0)

    ### blnum1, blnum2: first and last Body lnums of the affected region
    blnum1 = bnodes[sibs[0]-1]
    n = sibs[-1] + nodeSubnodes(VO,sibs[-1])
    if n < Z:
        blnum2 = bnodes[n]-1
    else:
        blnum2 = len(Body)

    ### construct new Body region
    blines = []
    for i in xrange(z):
        sib = sibs[i]
        j = sibs_dec[i][1] # index into sibs that points to new sib
        sib_new = sibs[j]

        # get Body region for sib_new branch
        bln1 = bnodes[sib_new-1]
        if j+1 < z:
            sib_next = sibs[j+1]
            bln2 = bnodes[sib_next-1]-1
        else:
            node_last = sib_new + nodeSubnodes(VO,sib_new)
            if node_last < Z:
                bln2 = bnodes[node_last]-1
            else:
                bln2 = len(Body)

        blines.extend(Body[bln1-1:bln2])

    ### replace Body region with the new, sorted region
    body_len = len(Body)
    Body[blnum1-1:blnum2] = blines
    assert body_len == len(Body)

    return (1,1)


#---EXECUTE SCRIPT----------------------------{{{1
#

def voom_GetVoomRange(withSubnodes=0): #{{{2
    body = int(vim.eval('l:body'))
    VO = VOOMS[body]
    lnum = int(vim.eval('a:lnum'))
    if vim.eval('l:bufType')=='Body':
        lnum = bisect.bisect_right(VO.bnodes, lnum)
    bln1, bln2 = nodesBodyRange(VO, lnum, lnum, withSubnodes)
    vim.command("let [l:bln1,l:bln2]=[%s,%s]" %(bln1,bln2))


def voom_GetBufRange(): #{{{2
    body = int(vim.eval('l:body'))
    ln1, ln2 = int(vim.eval('a:ln1')), int(vim.eval('a:ln2'))
    VO = VOOMS[body]
    bln1, bln2 = nodesBodyRange(VO, ln1, ln2)
    vim.command("let [l:bln1,l:bln2]=[%s,%s]" %(bln1,bln2))


def voom_Exec(): #{{{2
    if vim.eval('l:bufType')=='Tree':
        Buf = VOOMS[int(vim.eval('l:body'))].Body
    else:
        Buf = vim.current.buffer
    bln1, bln2 = int(vim.eval('l:bln1')), int(vim.eval('l:bln2'))
    blines = Buf[bln1-1:bln2]
    # specifiy script encoding (Vim internal encoding) on the first line
    enc = '# -*- coding: %s -*-' %get_vim_encoding()
    # prepend extra \n's to make traceback lnums match buffer lnums
    # TODO: find less silly way to adjust traceback lnums
    _code_ = '%s\n%s%s\n' %(enc, '\n'*(bln1-2), '\n'.join(blines))
    _globs_ = {'vim': vim, '_VOoM2657': sys.modules['voom_vimplugin2657.voom_vim']}
    try:
        exec(_code_, _globs_)
    #except Exception: # does not catch vim.error
    except:
        #traceback.print_exc()  # writes to sys.stderr
        printTraceback(bln1,bln2)

    print('---end of Python script (%s-%s)---' %(bln1,bln2))

# id_20101214100357
# NOTES on printing Python tracebacks and Vim errors.
#
# When there is no PyLog, we want Python traceback echoed as Vim error message.
# Writing to sys.stderr accomplishes that:
#   :py sys.stderr.write('oopsy-doopsy')
# Drawback: writing to default sys.stderr (no PyLog) triggers Vim error.
# Thus, without PyLog there are two useless lines on top with Vim error:
#   Error detected while processing function voom#Exec:
#   line 63:
#
# Vim code:
#
# 1) PyLog is enabled. Must execute this inside try/catch/entry.
# Otherwise, something weird happens when Vim error occurs, most likely
# Vim error echoing interferes with PyLog scrolling.
# The only downside is that only v:exception is printed, no details
# about Vim error location (v:throwpoint is useless).
#
# 2) PyLog is not enabled. Do not execute this inside try/catch/endtry.
# Python traceback is not printed if we do.
#


def printTraceback(bln1,bln2): #{{{2
    """Print traceback from exception caught during Voomexec."""
    out = None
    # like traceback.format_exc(), traceback.print_exc()
    try:
        etype, value, tb = sys.exc_info()
        out = traceback.format_exception(etype, value, tb)
        #out = traceback.format_exception(etype, value, tb.tb_next)
    finally:
        etype = value = tb = None
    if not out:
        sys.stderr.write('ERROR: Voomexec failed to format Python traceback')
        return
    info = '  ...exception executing script (%s-%s)...\n' %(bln1,bln2)
    if bln1==1:
        info += '  ...subtract 1 from traceback lnums to get buffer lnums...\n'
    out[1:2] = [info]
    #out[1:1] = [info]
    sys.stderr.write(''.join(out))


#---LOG BUFFER--------------------------------{{{1
#
class LogBufferClass: #{{{2
    """A file-like object for replacing sys.stdout and sys.stdin with a Vim buffer."""

    def __init__(self): #{{{3
        self.buffer = vim.current.buffer
        self.logbnr = vim.eval('bufnr("")')
        self.buffer[0] = 'Python %s Log buffer ...' % PY_VERSION
        self.join = False
        if IS_PY2:
            self.encoding = get_vim_encoding()
            self.type_u = type(u" ")


    def write(self,s): #{{{3
        """Append string to buffer, scroll Log windows in all tabs."""
        # Messages are terminated by sending '\n' (null string? ^@).
        # Thus "print '\n'" sends '\n' twice.
        # The message itself can contain '\n's.
        # One line can be sent in many strings which don't always end with \n.
        # This is certainly true for Python errors and for 'print a, b, ...' .
        #
        # Can't have '\n' in appended list items, so always use splitlines().
        # A trailing \n is lost after splitlines(), but not for '\n\n' etc.
        #
        # Vim 7.2, 7.3 (Python 2 only): cannot append unicode strings. This produces an error:
        #   :py vim.current.buffer.append(u'test')
        # Vim 7.4, Python 2 and 3: no such problem, the above works.
        # Vim 7.4, Python 3: type(s)==type(u" ") is usually true, s.encode() results in error.

        #print(self.buffer.name)

        if not s: return
        # Nasty things happen when printing to unloaded PyLog buffer.
        # This also catches printing to noexisting buffer, as in pydoc help() glitch.
        if vim.eval("bufloaded(%s)" %(self.logbnr))=='0':
            vim.command("call voom#ErrorMsg('VOoM (PyLog): PyLog buffer %s is unloaded or doesn''t exist')" %self.logbnr)
            vim.command("call voom#ErrorMsg('VOoM (PyLog): unable to write string:')")
            vim.command("echom '%s'" %(repr(s).replace("'", "''")) )
            vim.command("call voom#ErrorMsg('VOoM (PyLog): please try executing command :Voomlog to fix')")
            return
        try:
            if IS_PY2 and type(s) == self.type_u: # needed for Vim 7.2, 7.3
                s = s.encode(self.encoding)
            # Join with previous message if it had no ending newline.
            if self.join:
                s = self.buffer[-1] + s
                del self.buffer[-1]
            self.join = not s[-1]=='\n'
            self.buffer.append(s.splitlines())
        except:
            # list of all exception lines, no newlines in items
            exc_lines = traceback.format_exc().splitlines()
            self.buffer.append('')
            self.buffer.append('VOoM: exception writing to PyLog buffer:')
            self.buffer.append(repr(s))
            self.buffer.append(exc_lines)
            self.buffer.append('')

        vim.command('call voom#LogScroll()')


#---misc--------------------------------------{{{1

def get_vim_encoding(): #{{{2
    """Return Vim internal encoding."""
    # When &enc is any Unicode Vim allegedly uses utf-8 internally.
    # See |encoding|, mbyte.c, values are from |encoding-values|
    enc = vim.eval('&enc')
    if enc in ('utf-8','ucs-2','ucs-2le','utf-16','utf-16le','ucs-4','ucs-4le'):
        return 'utf-8'
    return enc


# modelines {{{1
# vim:fdm=marker:fdl=0:
# vim:foldtext=getline(v\:foldstart).'...'.(v\:foldend-v\:foldstart):
