#@+leo-ver=5-thin
#@+node:EKR.20040517075715.10: * @file vim.py
#@+<< docstring >>
#@+node:ekr.20050226184411: ** << docstring >>
'''
#@@language rest

Enables two-way communication with gVim (recommended) or Vim.

Commands
--------

``vim-open-file``
    Opens the nearest ancestor @file or @clean node in vim. Leo will update
    the file in the outline when you save the file in vim.

``vim-open-node``
    Opens the selected node in vim. Leo will update the node in the outline
    when you save the file in vim.

Installation
------------

Set the ``vim_cmd`` and ``vim_exe`` settings as shown below.

Alternatively, you can put gvim.exe is on your PATH.

Settings
--------

``@string vim_cmd``
    The command to execute to start gvim. Something like::

        <path-to-gvim>/gvim --servername LEO

``@string vim_exe``
    The path to the gvim executable.

``vim_plugin_positions_cursor``
    True: Leo will put Vim cursor at same location as Leo cursor in file.

``vim_plugin_uses_tab_feature``
    True: Leo will put the node or file in a Vim tab card.

'''
#@-<< docstring >>
#@+<< version history >>
#@+node:ekr.20050226184411.1: ** << version history >>
#@@killcolor
#@+at
# 
# Contributed by Andrea Galimberti.
# Edited by Felix Breuer.
# 
# 1.5 EKR:
#     - Added new sections.
#     - Move most comments into docstring.
#     - Added useDoubleClick variable.
#     - Added init function.
#     - Init _vim_cmd depending on sys.platform.
# 1.6 EKR:
#     - Use keywords to get c, not g.top().
#     - Don't use during unit testing: prefer xemacs instead.
#     - Added _vim_exe
#     - Use "os.spawnv" instead of os.system.
#     - Simplified the search of g.app.openWithFiles.
#     - Fixed bug in open_in_vim: hanged v.bodyString to v.bodyString()
# 1.7 EKR: Excellent new code by Jim Sizelove solves weird message on first open of vim.
# 1.8 EKR: Set subprocess = None if import fails.
# 1.9 EKR:
#     - Document how install subproces, and use g.importExtension to import subprocess.
#     - Import subprocess with g.importExtension.
# 1.10 EKR:
#     - Support 'vim_cmd' and 'vim_exe' settings.
#     - These override the default _vim_cmd and _vim_exe settings.
# 1.11 EKR: Emergency default for window is now the default location: c:\Program Files\vim\vim63
# 1.12 EKR:
#     - Added emergency default for 'darwin'.
#     - Corrected the call to openWith.  It must now use data=data due to a new event param.
# 1.13 EKR: The docstring now states that the open_with plugin must be enabled for this to work.
# 1.14 EKR: Emphasized that the open_with plugin must be enabled.
# 1.15 EKR: Don't open @url nodes in vim if @bool vim_plugin_opens_url_nodes setting is False.
# 1.16 TL: open_in_vim modifications
#     - support file open in gVim at same line number as Leo cursor location
#     - support file open in a gVim tab (see also mod_tempfname.py)
# 1.17 EKR: Give a location message to help with settings.
# 1.18 VMV:
#     - Use gvim on Linux too, emergency default on Windows doesn't have explicit path
#     - Works when subprocess.Popen(shell=True)
# 2.0 EKR: Use *only* the vim-open-node command.  Do not pollute click handlers.
#@-<< version history >>
#@+<< documentation from Jim Sizelove >>
#@+node:ekr.20050909102921: ** << documentation from Jim Sizelove >>
#@+at
#@@language rest
#@@wrap
# 
# I was trying to get Leo to work more effectively with Vim, my editor of choice. To do so, I made several changes to Leo which (I believe) make it work better.
# 
# After much exploring and trying various things, I made a change to the os.spawnv section of the openWith function in leoCommands.py. This added line seems to prevent the "weird error message on first open of Vim." (vim.py, line 32) when opening Vim with os.spawnv.
# 
# os.spawnv needs the command it is calling as the first argument in the args list in addition, so the command actually shows twice in the total args to os.spawnv.  For example::
# 
# 
#     os.spawnv(os.P_NOWAIT, "C:/Program Files/Vim/vim63/gvim.exe",
#         ["gvim.exe", "--servername", "LEO", "--remote", "foo.txt"])
# 
# 
# If the call is made without the command-name as the first item in the list of args, like so::
# 
# 
#     os.spawnv(os.P_NOWAIT, "C:/Program Files/Vim/vim63/gvim.exe",
#         ["--servername", "LEO", "--remote", "foo.txt"])
# 
# 
# an error message pops up::
# 
#     E247: no registered server named "GVIM": Send failed.  Trying to execute locally
# 
# This message means that gVim is not looking for a server named "LEO", which presumably the user has already opened with the command "gvim --servername LEO". Instead it is looking for a server named "GVIM", and not finding it, opens the files "foo.txt" and "LEO" (notice that it didn't catch the "--servername" argument and thinks that "LEO" is the name of a new file to create) in two buffers in a local copy of gVim. Now, if the command is::
# 
#     os.spawnv(
#         os.P_NOWAIT, "C:/Program Files/Vim/vim63/gvim.exe",
#         ["gvim.exe", "--servername", "LEO", "--remote", "foo.txt"])
# 
# Everything works great, as long as the user doesn't close the gVim window. If the user has closed the gVim window, then tries to open a node in Vim, they will see this error message::
# 
#     E247: no registered server named "LEO": Send failed.
# 
# Trying to execute locally If you use the ``--remote-silent`` argument, gVim will start the LEO server without the error message.
# 
# You can see which servers gVim has running by typing the following at the command prompt::
# 
#     vim --serverlist
# 
# The rest of my changes have to do with using the subprocess module instead of the os.system, and various os.spawn* calls. I find subprocess easier to understand, and it is fairly simple to use for the most common kinds of process calls, but is capable of all the variations you may need. It is designed to replace all the os.system, os.spawn, and popen calls. It is available in Python 2.4.
# 
# So I added some lines to use subprocess in the OpenWith plugin and the Vim plugin.
# 
# I also have added a table in the "create_open_with_menu" function that makes use of the various editors I have used at times.  Most of those editors are called with subprocess.Popen.
#@-<< documentation from Jim Sizelove >>
#@+<< imports >>
#@+node:ekr.20050226184411.2: ** << imports >>
import leo.core.leoGlobals as g
import os
import subprocess
import sys
#@-<< imports >>
# This command is used to communicate with the vim server. If you use gvim
# you can leave the command as is, you do not need to change it to "gvim ..."
# New in version 1.10 of this plugin: these are emergency defaults only.
# They are typically overridden by the corresponding 'vim_cmd' and 'vim_exe' settings in
# leoSettings.leo or individual .leo files.
if sys.platform == 'win32':
    # Works on XP if you have gvim on PATH
    _vim_cmd = "gvim --servername LEO"
    _vim_exe = "gvim"
elif sys.platform == 'darwin':
    _vim_cmd = "/Applications/gvim.app/Contents/MacOS/gvim --servername LEO"
    _vim_exe = "gvim"
else:
    _vim_cmd = "gvim --servername LEO"
    _vim_exe = "gvim"
# Global message flags.
contextmenu_message_given = False
locationMessageGiven = False
#@+others
#@+node:ekr.20050226184624: ** init
def init():
    '''Return True if the plugin has loaded successfully.'''
    ok = not g.app.unitTesting # Don't conflict with xemacs plugin.
    if ok:
        # print ('vim.py enabled')
        # Register the handlers...
        # event = 'open2'
        # g.registerHandler(event,on_open_window)
        # Enable the os.system call if you want to
        # start a (g)vim server when Leo starts.
        if 0: os.system(_vim_cmd)
        g.plugin_signon(__name__)
    return ok
#@+node:ekr.20150326150910.1: ** g.command('vim-open-file')
@g.command('vim-open-file')
def vim_open_file_command(event):
    '''vim.py: Open the entire file in (g)vim.'''
    c = event.get('c')
    if c:
        VimCommander(c, entire_file=True)
#@+node:ekr.20120315101404.9745: ** g.command('vim-open-node')
@g.command('vim-open-node')
def vim_open_node_command(event):
    '''vim.py: open the selected node in (g)vim.'''
    c = event.get('c')
    if c:
        VimCommander(c, entire_file=False)
#@+node:ekr.20150326153420.1: ** class VimCommander
class VimCommander:
    '''A class implementing the vim plugin.'''
    #@+others
    #@+node:ekr.20150326155343.1: *3*  vim.ctor
    def __init__(self, c, entire_file):
        '''Ctor for the VimCommander class.'''
        self.c = c
        self.entire_file = entire_file
        # compute settings.
        getBool, getString = c.config.getBool, c.config.getString
        self.open_url_nodes = getBool('vim_plugin_opens_url_nodes')
        self.position_cursor = getBool('vim_plugin_positions_cursor')
        self.trace = getBool('vim_plugin_trace')
        self.uses_tab = getBool('vim_plugin_uses_tab_feature')
        self.vim_cmd = getString('vim_cmd') or _vim_cmd
        self.vim_exe = getString('vim_exe') or _vim_exe
        # Give messages.
        global locationMessageGiven
        if self.trace and not locationMessageGiven:
            locationMessageGiven = True
            print('vim_cmd: %s' % self.vim_cmd)
            print('vim_exe: %s' % self.vim_exe)
        self.open_in_vim()
    #@+node:ekr.20150326183613.1: *3* vim.check_args
    def check_args(self):
        '''Return True of basic checks pass.'''
        p = self.c.p
        contextMenu = self.load_context_menu()
        if not contextMenu:
            return False
        ### What is this?
        # if p.h.find('file-ref') == 1: # Must be at 2nd position
        #     return False
        if not self.open_url_nodes and p.h.startswith('@url'):
            return False
        else:
            return True
    #@+node:ekr.20150326183310.1: *3* vim.error
    def error(self, s):
        '''Report an error.'''
        g.es_print(s, color='red')
    #@+node:ekr.20150326173414.1: *3* vim.find_root
    def find_root(self, p):
        '''Return the nearest ancestor @auto or @clean node.'''
        p0 = p.copy()
        while p:
            if self.entire_file and p.isAnyAtFileNode():
                return p
            elif p.isAtAutoNode() or p.isAtCleanNode():
                return p
            else:
                p.moveToParent()
        self.error('no parent @auto or @clean node: %s' % p0.h)
        return None
    #@+node:ekr.20150326180515.1: *3* vim.find_path_for_node
    def find_path_for_node(self, p):
        '''Search the open-files list for a file corresponding to p.'''
        efc = g.app.externalFilesController
        path = efc.find_path_for_node(p)
        return path
    #@+node:ekr.20150326173301.1: *3* vim.forget_path
    def forget_path(self, path):
        '''
        Stop handling the path:
        - Remove the path from the list of open-with files.
        - Send a command to vim telling it to close the path.
        '''
        assert path
        # Don't do this: it prevents efc from reopening paths.
        # efc = g.app.externalFilesController
        # if efc: efc.forget_path(path)
        os.remove(path)
        cmd = self.vim_cmd + "--remote-send '<C-\\><C-N>:bd " + path + "<CR>'"
        if self.trace: g.trace('os.system(%s)' % cmd)
        os.system(cmd)
    #@+node:ekr.20150326181247.1: *3* vim.get_cursor_arg
    def get_cursor_arg(self):
        '''Compute the cursor argument for vim.'''
        if self.position_cursor:
            wrapper = self.c.frame.body.wrapper
            s = wrapper.getAllText()
            ins = wrapper.getInsertPoint()
            row, col = g.convertPythonIndexToRowCol(s, ins)
            return "+" + str(row + 1)
        else:
            return ''
    #@+node:ekr.20150326154203.1: *3* vim.load_context_menu
    def load_context_menu(self):
        '''Load the contextmenu plugin.'''
        global contextmenu_message_given
        contextMenu = g.loadOnePlugin('contextmenu.py', verbose=True)
        if not contextMenu and not contextmenu_message_given:
            contextmenu_message_given = True
            self.error('can not load contextmenu.py')
        return contextMenu
    #@+node:ekr.20150326180928.1: *3* vim.open_file (calls c.openWith)
    def open_file(self, root):
        '''Open the the file in vim using c.openWith.'''
        trace = False and not g.unitTesting
        c = self.c
        # Common arguments.
        if trace: g.trace(self.entire_file, root.h)
        cursor_arg = self.get_cursor_arg()
        tab_arg = "-tab" if self.uses_tab else ""
        remote_arg = "--remote" + tab_arg + "-silent"
        if self.entire_file:
            # vim-open-file.
            assert root.isAnyAtFileNode(), root
            args = [self.vim_exe, "--servername", "LEO", remote_arg] # No cursor arg.
            dir_ = g.setDefaultDirectory(c, root)
            fn = c.os_path_finalize_join(dir_, root.anyAtFileNodeName())
            c_arg = '%s %s' % (' '.join(args), fn)
            command = 'subprocess.Popen(%s,shell=True)' % c_arg
            if trace: g.trace(command)
            try:
                subprocess.Popen(c_arg, shell=True)
            except OSError:
                g.es_print(command)
                g.es_exception()
        else:
            # vim-open-node
            root.v._vim_old_body = root.v.b
                # Not used in existing code, but it may be used elsewhere.
            args = [self.vim_exe, "--servername", "LEO", remote_arg, cursor_arg]
            if trace: g.trace('c.openWith(%s)' % args)
            d = {'args': args,
                 'ext': None,
                 'kind': 'subprocess.Popen',
                 'p': root.copy(),
            }
            c.openWith(d=d)
    #@+node:ekr.20120315101404.9746: *3* vim.open_in_vim (entry: called from ctor)
    def open_in_vim(self):
        '''Open p in vim, or the entire enclosing file if entire_file is True.'''
        p = self.c.p
        if not self.check_args():
            return
        root = self.find_root(p) if self.entire_file else p
        if not root:
            return
        path = self.find_path_for_node(root)
        if path and self.should_open_old_file(path, root):
            cmd = self.vim_cmd + "--remote-send '<C-\\><C-N>:e " + path + "<CR>'"
            if self.trace: g.trace('os.system(%s)' % cmd)
            os.system(cmd)
        else:
            # Open a new temp file.
            if path: self.forget_path(path)
            self.open_file(root)
    #@+node:ekr.20150326173000.1: *3* vim.should_open_old_file
    def should_open_old_file(self, path, root):
        '''Return True if we should open the old temp file.'''
        v = root.v
        return (
            path and g.os_path_exists(path) and
            hasattr(v.b, '_vim_old_body') and v.b == v._vim_old_body
        )
    #@+node:ekr.20150326175258.1: *3* vim.write_root
    def write_root(self, root):
        '''Return the concatenation of all bodies in p's tree.'''
        result = []
        for p in root.self_and_subtree():
            s = p.b
            result.append(s if s.endswith('\n') else s.rstrip() + '\n')
        return ''.join(result)
        # return ''.join([p.b if p.b endswith('\n') else p.b.rstrip()+'\n'
            # for p in root.self_and_subtree()])
    #@-others
#@-others
#@@language python
#@@tabwidth -4
#@-leo
