436 lines
12 KiB
Python
436 lines
12 KiB
Python
# vimspector - A multi-language debugging system for Vim
|
|
# Copyright 2018 Ben Jackson
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
|
|
import logging
|
|
import os
|
|
import contextlib
|
|
import vim
|
|
import json
|
|
import string
|
|
|
|
_log_handler = logging.FileHandler( os.path.expanduser( '~/.vimspector.log' ),
|
|
mode = 'w' )
|
|
_log_handler.setFormatter(
|
|
logging.Formatter( '%(asctime)s - %(levelname)s - %(message)s' ) )
|
|
|
|
|
|
def SetUpLogging( logger ):
|
|
logger.setLevel( logging.DEBUG )
|
|
if _log_handler not in logger.handlers:
|
|
logger.addHandler( _log_handler )
|
|
|
|
|
|
_logger = logging.getLogger( __name__ )
|
|
SetUpLogging( _logger )
|
|
|
|
|
|
def BufferNumberForFile( file_name ):
|
|
return int( vim.eval( 'bufnr( "{0}", 1 )'.format( file_name ) ) )
|
|
|
|
|
|
def BufferForFile( file_name ):
|
|
return vim.buffers[ BufferNumberForFile( file_name ) ]
|
|
|
|
|
|
def OpenFileInCurrentWindow( file_name ):
|
|
buffer_number = BufferNumberForFile( file_name )
|
|
try:
|
|
vim.command( 'bu {0}'.format( buffer_number ) )
|
|
except vim.error as e:
|
|
if 'E325' not in str( e ):
|
|
raise
|
|
|
|
return vim.buffers[ buffer_number ]
|
|
|
|
|
|
def SetUpCommandBuffer( cmd, name ):
|
|
bufs = vim.bindeval(
|
|
'vimspector#internal#job#StartCommandWithLog( {}, "{}" )'.format(
|
|
json.dumps( cmd ),
|
|
name ) )
|
|
|
|
if bufs is None:
|
|
raise RuntimeError( "Unable to start job {}: {}".format( cmd, name ) )
|
|
elif not all( b > 0 for b in bufs ):
|
|
raise RuntimeError( "Unable to get all streams for job {}: {}".format(
|
|
name,
|
|
cmd ) )
|
|
|
|
return [ vim.buffers[ b ] for b in bufs ]
|
|
|
|
|
|
def CleanUpCommand( name ):
|
|
return vim.eval( 'vimspector#internal#job#CleanUpCommand( "{}" )'.format(
|
|
name ) )
|
|
|
|
|
|
def SetUpScratchBuffer( buf, name ):
|
|
buf.options[ 'buftype' ] = 'nofile'
|
|
buf.options[ 'swapfile' ] = False
|
|
buf.options[ 'modifiable' ] = False
|
|
buf.options[ 'modified' ] = False
|
|
buf.options[ 'readonly' ] = True
|
|
buf.options[ 'buflisted' ] = False
|
|
buf.options[ 'bufhidden' ] = 'wipe'
|
|
buf.name = name
|
|
|
|
|
|
def SetUpHiddenBuffer( buf, name ):
|
|
buf.options[ 'buftype' ] = 'nofile'
|
|
buf.options[ 'swapfile' ] = False
|
|
buf.options[ 'modifiable' ] = False
|
|
buf.options[ 'modified' ] = False
|
|
buf.options[ 'readonly' ] = True
|
|
buf.options[ 'buflisted' ] = False
|
|
buf.options[ 'bufhidden' ] = 'hide'
|
|
buf.name = name
|
|
|
|
|
|
def SetUpPromptBuffer( buf, name, prompt, callback, hidden=False ):
|
|
# This feature is _super_ new, so only enable when available
|
|
if not int( vim.eval( "exists( '*prompt_setprompt' )" ) ):
|
|
return SetUpScratchBuffer( buf, name )
|
|
|
|
buf.options[ 'buftype' ] = 'prompt'
|
|
buf.options[ 'swapfile' ] = False
|
|
buf.options[ 'modifiable' ] = True
|
|
buf.options[ 'modified' ] = False
|
|
buf.options[ 'readonly' ] = False
|
|
buf.options[ 'buflisted' ] = False
|
|
buf.options[ 'bufhidden' ] = 'wipe' if not hidden else 'hide'
|
|
buf.name = name
|
|
|
|
vim.eval( "prompt_setprompt( {0}, '{1}' )".format( buf.number,
|
|
Escape( prompt ) ) )
|
|
vim.eval( "prompt_setcallback( {0}, function( '{1}' ) )".format(
|
|
buf.number,
|
|
Escape( callback ) ) )
|
|
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def ModifiableScratchBuffer( buf ):
|
|
if buf.options[ 'modifiable' ]:
|
|
yield
|
|
return
|
|
|
|
buf.options[ 'modifiable' ] = True
|
|
buf.options[ 'readonly' ] = False
|
|
try:
|
|
yield
|
|
finally:
|
|
buf.options[ 'modifiable' ] = False
|
|
buf.options[ 'readonly' ] = True
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def RestoreCursorPosition():
|
|
current_pos = vim.current.window.cursor
|
|
try:
|
|
yield
|
|
finally:
|
|
vim.current.window.cursor = (
|
|
min( current_pos[ 0 ], len( vim.current.buffer ) ),
|
|
current_pos[ 1 ] )
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def RestoreCurrentWindow():
|
|
# TODO: Don't trigger autocommands when shifting windows
|
|
old_tabpage = vim.current.tabpage
|
|
old_window = vim.current.window
|
|
try:
|
|
yield
|
|
finally:
|
|
vim.current.tabpage = old_tabpage
|
|
vim.current.window = old_window
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def RestoreCurrentBuffer( window ):
|
|
# TODO: Don't trigger autoccommands when shifting buffers
|
|
old_buffer = window.buffer
|
|
try:
|
|
yield
|
|
finally:
|
|
with RestoreCurrentWindow():
|
|
vim.current.window = window
|
|
vim.current.buffer = old_buffer
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def LetCurrentWindow( window ):
|
|
with RestoreCurrentWindow():
|
|
JumpToWindow( window )
|
|
yield
|
|
|
|
|
|
def JumpToWindow( window ):
|
|
vim.current.tabpage = window.tabpage
|
|
vim.current.window = window
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def TemporaryVimOptions( opts ):
|
|
old_value = {}
|
|
try:
|
|
for option, value in opts.items():
|
|
old_value[ option ] = vim.options[ option ]
|
|
vim.options[ option ] = value
|
|
|
|
yield
|
|
finally:
|
|
for option, value in old_value.items():
|
|
vim.options[ option ] = value
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def TemporaryVimOption( opt, value ):
|
|
old_value = vim.options[ opt ]
|
|
vim.options[ opt ] = value
|
|
try:
|
|
yield
|
|
finally:
|
|
vim.options[ opt ] = old_value
|
|
|
|
|
|
def PathToConfigFile( file_name, from_directory = None ):
|
|
if not from_directory:
|
|
p = os.getcwd()
|
|
else:
|
|
p = os.path.abspath( os.path.realpath( from_directory ) )
|
|
|
|
while True:
|
|
candidate = os.path.join( p, file_name )
|
|
if os.path.exists( candidate ):
|
|
return candidate
|
|
|
|
parent = os.path.dirname( p )
|
|
if parent == p:
|
|
return None
|
|
p = parent
|
|
|
|
|
|
def Escape( msg ):
|
|
return msg.replace( "'", "''" )
|
|
|
|
|
|
def UserMessage( msg, persist=False ):
|
|
if persist:
|
|
_logger.warning( 'User Msg: ' + msg )
|
|
else:
|
|
_logger.info( 'User Msg: ' + msg )
|
|
|
|
vim.command( 'redraw' )
|
|
cmd = 'echom' if persist else 'echo'
|
|
for line in msg.split( '\n' ):
|
|
vim.command( "{0} '{1}'".format( cmd, Escape( line ) ) )
|
|
vim.command( 'redraw' )
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def InputSave():
|
|
vim.eval( 'inputsave()' )
|
|
try:
|
|
yield
|
|
finally:
|
|
vim.eval( 'inputrestore()' )
|
|
|
|
|
|
def SelectFromList( prompt, options ):
|
|
with InputSave():
|
|
display_options = [ prompt ]
|
|
display_options.extend( [ '{0}: {1}'.format( i + 1, v )
|
|
for i, v in enumerate( options ) ] )
|
|
try:
|
|
selection = int( vim.eval(
|
|
'inputlist( ' + json.dumps( display_options ) + ' )' ) ) - 1
|
|
if selection < 0 or selection >= len( options ):
|
|
return None
|
|
return options[ selection ]
|
|
except KeyboardInterrupt:
|
|
return None
|
|
|
|
|
|
def AskForInput( prompt, default_value = None ):
|
|
if default_value is None:
|
|
default_option = ''
|
|
else:
|
|
default_option = ", '{}'".format( Escape( default_value ) )
|
|
|
|
with InputSave():
|
|
try:
|
|
return vim.eval( "input( '{}' {} )".format( Escape( prompt ),
|
|
default_option ) )
|
|
except KeyboardInterrupt:
|
|
return ''
|
|
|
|
|
|
def AppendToBuffer( buf, line_or_lines, modified=False ):
|
|
try:
|
|
# After clearing the buffer (using buf[:] = None) there is always a single
|
|
# empty line in the buffer object and no "is empty" method.
|
|
if len( buf ) > 1 or buf[ 0 ]:
|
|
line = len( buf ) + 1
|
|
buf.append( line_or_lines )
|
|
elif isinstance( line_or_lines, str ):
|
|
line = 1
|
|
buf[ -1 ] = line_or_lines
|
|
else:
|
|
line = 1
|
|
buf[ : ] = line_or_lines
|
|
except Exception:
|
|
# There seem to be a lot of Vim bugs that lead to E315, whose help says that
|
|
# this is an internal error. Ignore the error, but write a trace to the log.
|
|
_logger.exception(
|
|
'Internal error while updating buffer %s (%s)', buf.name, buf.number )
|
|
finally:
|
|
if not modified:
|
|
buf.options[ 'modified' ] = False
|
|
|
|
# Return the first Vim line number (1-based) that we just set.
|
|
return line
|
|
|
|
|
|
|
|
def ClearBuffer( buf ):
|
|
buf[ : ] = None
|
|
|
|
|
|
def SetBufferContents( buf, lines, modified=False ):
|
|
try:
|
|
if not isinstance( lines, list ):
|
|
lines = lines.splitlines()
|
|
|
|
buf[:] = lines
|
|
finally:
|
|
buf.options[ 'modified' ] = modified
|
|
|
|
|
|
def IsCurrent( window, buf ):
|
|
return vim.current.window == window and vim.current.window.buffer == buf
|
|
|
|
|
|
# TODO: Should we just run the substitution on the whole JSON string instead?
|
|
# That woul dallow expansion in bool and number values, such as ports etc. ?
|
|
def ExpandReferencesInDict( obj, mapping, user_choices ):
|
|
def expand_refs_in_string( orig_s ):
|
|
s = os.path.expanduser( orig_s )
|
|
s = os.path.expandvars( s )
|
|
|
|
# Parse any variables passed in in mapping, and ask for any that weren't,
|
|
# storing the result in mapping
|
|
bug_catcher = 0
|
|
while bug_catcher < 100:
|
|
++bug_catcher
|
|
|
|
try:
|
|
s = string.Template( s ).substitute( mapping )
|
|
break
|
|
except KeyError as e:
|
|
# HACK: This is seemingly the only way to get the key. str( e ) returns
|
|
# the key surrounded by '' for unknowable reasons.
|
|
key = e.args[ 0 ]
|
|
default_value = user_choices.get( key, None )
|
|
mapping[ key ] = AskForInput( 'Enter value for {}: '.format( key ),
|
|
default_value )
|
|
user_choices[ key ] = mapping[ key ]
|
|
_logger.debug( "Value for %s not set in %s (from %s): set to %s",
|
|
key,
|
|
s,
|
|
orig_s,
|
|
mapping[ key ] )
|
|
except ValueError as e:
|
|
UserMessage( 'Invalid $ in string {}: {}'.format( s, e ),
|
|
persist = True )
|
|
break
|
|
|
|
return s
|
|
|
|
def expand_refs_in_object( obj ):
|
|
if isinstance( obj, dict ):
|
|
ExpandReferencesInDict( obj, mapping, user_choices )
|
|
elif isinstance( obj, list ):
|
|
for i, _ in enumerate( obj ):
|
|
# FIXME: We are assuming that it is a list of string, but could be a
|
|
# list of list of a list of dict, etc.
|
|
obj[ i ] = expand_refs_in_object( obj[ i ] )
|
|
elif isinstance( obj, str ):
|
|
obj = expand_refs_in_string( obj )
|
|
|
|
return obj
|
|
|
|
for k in obj.keys():
|
|
obj[ k ] = expand_refs_in_object( obj[ k ] )
|
|
|
|
|
|
def ParseVariables( variables_list, mapping, user_choices ):
|
|
new_variables = {}
|
|
new_mapping = mapping.copy()
|
|
|
|
if not isinstance( variables_list, list ):
|
|
variables_list = [ variables_list ]
|
|
|
|
for variables in variables_list:
|
|
new_mapping.update( new_variables )
|
|
for n, v in variables.items():
|
|
if isinstance( v, dict ):
|
|
if 'shell' in v:
|
|
import subprocess
|
|
import shlex
|
|
|
|
new_v = v.copy()
|
|
# Bit of a hack. Allows environment variables to be used.
|
|
ExpandReferencesInDict( new_v, new_mapping, user_choices )
|
|
|
|
env = os.environ.copy()
|
|
env.update( new_v.get( 'env' ) or {} )
|
|
cmd = new_v[ 'shell' ]
|
|
if not isinstance( cmd, list ):
|
|
cmd = shlex.split( cmd )
|
|
|
|
new_variables[ n ] = subprocess.check_output(
|
|
cmd,
|
|
cwd = new_v.get( 'cwd' ) or os.getcwd(),
|
|
env = env ).decode( 'utf-8' ).strip()
|
|
|
|
_logger.debug( "Set new_variables[ %s ] to '%s' from %s from %s",
|
|
n,
|
|
new_variables[ n ],
|
|
new_v,
|
|
v )
|
|
else:
|
|
raise ValueError(
|
|
"Unsupported variable defn {}: Missing 'shell'".format( n ) )
|
|
else:
|
|
new_variables[ n ] = v
|
|
|
|
return new_variables
|
|
|
|
|
|
def DisplayBaloon( is_term, display ):
|
|
if not is_term:
|
|
display = '\n'.join( display )
|
|
|
|
vim.eval( "balloon_show( {0} )".format(
|
|
json.dumps( display ) ) )
|
|
|
|
|
|
def GetBufferFilepath( buf ):
|
|
if not buf.name:
|
|
return ''
|
|
|
|
return os.path.normpath( buf.name )
|