diff --git a/autoload/vimspector.vim b/autoload/vimspector.vim index 35ecd03..f19bd78 100644 --- a/autoload/vimspector.vim +++ b/autoload/vimspector.vim @@ -296,7 +296,7 @@ function! vimspector#GetConfigurations() abort return endif let configurations = py3eval( - \ 'list( _vimspector_session.GetConfigurations( {} )[ 1 ].keys() )' + \ 'list( _vimspector_session.GetConfigurations().keys() )' \ . ' if _vimspector_session else []' ) return configurations endfunction diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index ea72651..c2deb08 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import glob import json import logging import os @@ -32,15 +31,11 @@ from vimspector import ( breakpoints, variables, settings, terminal, - installer ) -from vimspector.vendor.json_minify import minify + launch ) # We cache this once, and don't allow it to change (FIXME?) VIMSPECTOR_HOME = utils.GetVimspectorBase() -# cache of what the user entered for any option we ask them -USER_CHOICES = {} - class DebugSession( object ): def __init__( self, api_prefix ): @@ -80,24 +75,11 @@ class DebugSession( object ): self._server_capabilities = {} self.ClearTemporaryBreakpoints() - def GetConfigurations( self, adapters ): + def GetConfigurations( self ): current_file = utils.GetBufferFilepath( vim.current.buffer ) filetypes = utils.GetBufferFiletypes( vim.current.buffer ) - configurations = {} + return launch.GetConfigurations( {}, current_file, filetypes )[ 1 ] - for launch_config_file in PathsToAllConfigFiles( VIMSPECTOR_HOME, - current_file, - filetypes ): - self._logger.debug( f'Reading configurations from: {launch_config_file}' ) - if not launch_config_file or not os.path.exists( launch_config_file ): - continue - - with open( launch_config_file, 'r' ) as f: - database = json.loads( minify( f.read() ) ) - configurations.update( database.get( 'configurations' ) or {} ) - adapters.update( database.get( 'adapters' ) or {} ) - - return launch_config_file, configurations def Start( self, launch_variables = None ): # We mutate launch_variables, so don't mutate the default argument. @@ -112,161 +94,48 @@ class DebugSession( object ): self._launch_config = None current_file = utils.GetBufferFilepath( vim.current.buffer ) - adapters = {} - launch_config_file, configurations = self.GetConfigurations( adapters ) - - if not configurations: - utils.UserMessage( 'Unable to find any debug configurations. ' - 'You need to tell vimspector how to launch your ' - 'application.' ) - return - - glob.glob( install.GetGadgetDir( VIMSPECTOR_HOME ) ) - for gadget_config_file in PathsToAllGadgetConfigs( VIMSPECTOR_HOME, - current_file ): - self._logger.debug( f'Reading gadget config: {gadget_config_file}' ) - if not gadget_config_file or not os.path.exists( gadget_config_file ): - continue - - with open( gadget_config_file, 'r' ) as f: - a = json.loads( minify( f.read() ) ).get( 'adapters' ) or {} - adapters.update( a ) - - if 'configuration' in launch_variables: - configuration_name = launch_variables.pop( 'configuration' ) - elif ( len( configurations ) == 1 and - next( iter( configurations.values() ) ).get( "autoselect", True ) ): - configuration_name = next( iter( configurations.keys() ) ) - else: - # Find a single configuration with 'default' True and autoselect not False - defaults = { n: c for n, c in configurations.items() - if c.get( 'default', False ) is True - and c.get( 'autoselect', True ) is not False } - - if len( defaults ) == 1: - configuration_name = next( iter( defaults.keys() ) ) - else: - configuration_name = utils.SelectFromList( - 'Which launch configuration?', - sorted( configurations.keys() ) ) - - if not configuration_name or configuration_name not in configurations: - return + filetypes = utils.GetBufferFiletypes( vim.current.buffer ) + adapters = launch.GetAdapters( current_file ) + launch_config_file, configurations = launch.GetConfigurations( adapters, + current_file, + filetypes ) if launch_config_file: self._workspace_root = os.path.dirname( launch_config_file ) else: self._workspace_root = os.path.dirname( current_file ) - configuration = configurations[ configuration_name ] - adapter = configuration.get( 'adapter' ) - if isinstance( adapter, str ): - adapter_dict = adapters.get( adapter ) - if adapter_dict is None: - suggested_gadgets = installer.FindGadgetForAdapter( adapter ) - if suggested_gadgets: - response = utils.AskForInput( - f"The specified adapter '{adapter}' is not " - "installed. Would you like to install the following gadgets? ", - ' '.join( suggested_gadgets ) ) - if response: - new_launch_variables = dict( launch_variables ) - new_launch_variables[ 'configuration' ] = configuration_name + if not configurations: + configuration_name, configuration = launch.SuggestConfiguration( + current_file, + filetypes ) + else: + configuration_name, configuration = launch.SelectConfiguration( + launch_variables, + configurations ) - installer.RunInstaller( - self._api_prefix, - False, # Don't leave open - *shlex.split( response ), - then = lambda: self.Start( new_launch_variables ) ) - return - elif response is None: - return + if not configuration: + utils.UserMessage( 'Unable to find any debug configurations. ' + 'You need to tell vimspector how to launch your ' + 'application.' ) + return - utils.UserMessage( f"The specified adapter '{adapter}' is not " - "available. Did you forget to run " - "'install_gadget.py'?", - persist = True, - error = True ) - return - - adapter = adapter_dict - - # Additional vars as defined by VSCode: - # - # ${workspaceFolder} - the path of the folder opened in VS Code - # ${workspaceFolderBasename} - the name of the folder opened in VS Code - # without any slashes (/) - # ${file} - the current opened file - # ${relativeFile} - the current opened file relative to workspaceFolder - # ${fileBasename} - the current opened file's basename - # ${fileBasenameNoExtension} - the current opened file's basename with no - # file extension - # ${fileDirname} - the current opened file's dirname - # ${fileExtname} - the current opened file's extension - # ${cwd} - the task runner's current working directory on startup - # ${lineNumber} - the current selected line number in the active file - # ${selectedText} - the current selected text in the active file - # ${execPath} - the path to the running VS Code executable - - def relpath( p, relative_to ): - if not p: - return '' - return os.path.relpath( p, relative_to ) - - def splitext( p ): - if not p: - return [ '', '' ] - return os.path.splitext( p ) - - variables = { - 'dollar': '$', # HACK. Hote '$$' also works. - 'workspaceRoot': self._workspace_root, - 'workspaceFolder': self._workspace_root, - 'gadgetDir': install.GetGadgetDir( VIMSPECTOR_HOME ), - 'file': current_file, - } - - calculus = { - 'relativeFile': lambda: relpath( current_file, - self._workspace_root ), - 'fileBasename': lambda: os.path.basename( current_file ), - 'fileBasenameNoExtension': - lambda: splitext( os.path.basename( current_file ) )[ 0 ], - 'fileDirname': lambda: os.path.dirname( current_file ), - 'fileExtname': lambda: splitext( os.path.basename( current_file ) )[ 1 ], - # NOTE: this is the window-local cwd for the current window, *not* Vim's - # working directory. - 'cwd': os.getcwd, - 'unusedLocalPort': utils.GetUnusedLocalPort, - } - - # Pretend that vars passed to the launch command were typed in by the user - # (they may have been in theory) - USER_CHOICES.update( launch_variables ) - variables.update( launch_variables ) + adapter = launch.SelectAdapter( self._api_prefix, + self, + configuration_name, + configuration, + adapters, + launch_variables ) + if not adapter: + return try: - variables.update( - utils.ParseVariables( adapter.get( 'variables', {} ), - variables, - calculus, - USER_CHOICES ) ) - variables.update( - utils.ParseVariables( configuration.get( 'variables', {} ), - variables, - calculus, - USER_CHOICES ) ) - - - utils.ExpandReferencesInDict( configuration, - variables, - calculus, - USER_CHOICES ) - utils.ExpandReferencesInDict( adapter, - variables, - calculus, - USER_CHOICES ) + launch.ResolveConfiguration( adapter, + configuration, + launch_variables, + self._workspace_root, + current_file ) except KeyboardInterrupt: self._Reset() return @@ -398,6 +267,9 @@ class DebugSession( object ): def _Reset( self ): self._logger.info( "Debugging complete." ) + + launch.SaveConfiguration( self._configuration ) + if self._uiTab: self._logger.debug( "Clearing down UI" ) @@ -1277,29 +1149,3 @@ class DebugSession( object ): def AddFunctionBreakpoint( self, function, options ): return self._breakpoints.AddFunctionBreakpoint( function, options ) - - -def PathsToAllGadgetConfigs( vimspector_base, current_file ): - yield install.GetGadgetConfigFile( vimspector_base ) - for p in sorted( glob.glob( - os.path.join( install.GetGadgetConfigDir( vimspector_base ), - '*.json' ) ) ): - yield p - - yield utils.PathToConfigFile( '.gadgets.json', - os.path.dirname( current_file ) ) - - -def PathsToAllConfigFiles( vimspector_base, current_file, filetypes ): - for ft in filetypes + [ '_all' ]: - for p in sorted( glob.glob( - os.path.join( install.GetConfigDirForFiletype( vimspector_base, ft ), - '*.json' ) ) ): - yield p - - for ft in filetypes: - yield utils.PathToConfigFile( f'.vimspector.{ft}.json', - os.path.dirname( current_file ) ) - - yield utils.PathToConfigFile( '.vimspector.json', - os.path.dirname( current_file ) ) diff --git a/python3/vimspector/gadgets.py b/python3/vimspector/gadgets.py index 0271069..a2a603d 100644 --- a/python3/vimspector/gadgets.py +++ b/python3/vimspector/gadgets.py @@ -49,6 +49,40 @@ GADGETS = { } }, }, + 'templates': [ + { + 'description': 'gdb/lldb local debugging with vscode-cpptools', + 'filetypes': ( 'c', 'cpp', 'objc', 'rust' ), + 'configurations': [ + { + 'description': 'Launch local process', + 'launch_configuration': { + 'adapter': 'vscode-cpptools', + 'configuration': { + 'request': 'launch', + 'program': '${Binary:${fileBasenameNoExtension\\}}', + 'args': [ '*${CommandLineArguments}' ], + 'cwd': '${WorkingDir:${fileDirname\\}}', + 'externalConsole#json': '${UseExternalConsole:true\nfalse}', + 'stopAtEntry#json': '${StopAtEntry:true\nfalse}', + 'MIMode': '${Debugger:gdb\nlldb}', + } + } + }, + { + 'description': 'Attach to local process by PID', + 'launch_configuration': { + 'adapter': 'vscode-cpptools', + 'configuration': { + 'request': 'attach', + 'program': '${Binary:${fileBasenameNoExtension\\}}', + 'MIMode': '${Debugger:gdb\nlldb}', + } + } + } + ] + } + ] }, 'linux': { 'file_name': 'cpptools-linux.vsix', @@ -452,6 +486,42 @@ GADGETS = { }, 'all': { 'version': 'v1.5.3', + 'templates': [ + { + 'description': 'LLDB local debugging with CodeLLDB', + 'filetypes': ( 'c', 'cpp', 'objc', 'rust' ), + 'configurations': [ + { + 'description': 'Launch local process', + 'launch_configuration': { + 'adapter': 'CodeLLDB', + 'configuration': { + 'request': 'launch', + 'program': '${Binary:${fileBasenameNoExtension\\}}', + 'args': [ '*${CommandLineArguments}' ], + 'cwd': '${WorkingDir:${fileDirname\\}}', + 'terminal': '${Console:integrated\nexternal}', + 'stopOnEntry#json': '${StopOnEntry:true\nfalse}', + 'expressions': '${ExpressionType:native\nsimple\npython}' + } + } + }, + { + 'description': 'Attach to local process by PID', + 'launch_configuration': { + 'adapter': 'CodeLLDB', + 'configuration': { + 'request': 'attach', + 'program': '${Binary:${fileBasenameNoExtension\\}}', + 'pid#json': "${pid}", + 'stopOnEntry#json': '${StopOnEntry:true\nfalse}', + 'expressions': '${ExpressionType:native\nsimple\npython}' + } + } + } + ] + } + ] }, 'macos': { 'file_name': 'codelldb-x86_64-darwin.vsix', @@ -486,6 +556,7 @@ GADGETS = { 'name': 'CodeLLDB', 'type': 'CodeLLDB', "command": [ + # FIXME: This probably doesn't work on windows. "${gadgetDir}/CodeLLDB/adapter/codelldb", "--port", "${unusedLocalPort}" ], diff --git a/python3/vimspector/launch.py b/python3/vimspector/launch.py new file mode 100644 index 0000000..5bf4108 --- /dev/null +++ b/python3/vimspector/launch.py @@ -0,0 +1,335 @@ +# vimspector - A multi-language debugging system for Vim +# Copyright 2020 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 os +import logging +import json +import glob +import shlex +import copy + +from vimspector import install, installer, utils, gadgets +from vimspector.vendor.json_minify import minify + +_logger = logging.getLogger( __name__ ) +utils.SetUpLogging( _logger ) + +# We cache this once, and don't allow it to change (FIXME?) +VIMSPECTOR_HOME = utils.GetVimspectorBase() + +# cache of what the user entered for any option we ask them +USER_CHOICES = {} + + + +def GetAdapters( current_file ): + adapters = {} + for gadget_config_file in PathsToAllGadgetConfigs( VIMSPECTOR_HOME, + current_file ): + _logger.debug( f'Reading gadget config: {gadget_config_file}' ) + if not gadget_config_file or not os.path.exists( gadget_config_file ): + continue + + with open( gadget_config_file, 'r' ) as f: + a = json.loads( minify( f.read() ) ).get( 'adapters' ) or {} + adapters.update( a ) + + return adapters + + +def PathsToAllGadgetConfigs( vimspector_base, current_file ): + yield install.GetGadgetConfigFile( vimspector_base ) + for p in sorted( glob.glob( + os.path.join( install.GetGadgetConfigDir( vimspector_base ), + '*.json' ) ) ): + yield p + + yield utils.PathToConfigFile( '.gadgets.json', + os.path.dirname( current_file ) ) + + +def GetConfigurations( adapters, current_file, filetypes ): + configurations = {} + for launch_config_file in PathsToAllConfigFiles( VIMSPECTOR_HOME, + current_file, + filetypes ): + _logger.debug( f'Reading configurations from: {launch_config_file}' ) + if not launch_config_file or not os.path.exists( launch_config_file ): + continue + + with open( launch_config_file, 'r' ) as f: + database = json.loads( minify( f.read() ) ) + configurations.update( database.get( 'configurations' ) or {} ) + adapters.update( database.get( 'adapters' ) or {} ) + + # We return the last config file inspected which is the most specific one + # (i.e. the project-local one) + return launch_config_file, configurations + + +def PathsToAllConfigFiles( vimspector_base, current_file, filetypes ): + for ft in filetypes + [ '_all' ]: + for p in sorted( glob.glob( + os.path.join( install.GetConfigDirForFiletype( vimspector_base, ft ), + '*.json' ) ) ): + yield p + + for ft in filetypes: + yield utils.PathToConfigFile( f'.vimspector.{ft}.json', + os.path.dirname( current_file ) ) + + yield utils.PathToConfigFile( '.vimspector.json', + os.path.dirname( current_file ) ) + + +def SelectConfiguration( launch_variables, configurations ): + if 'configuration' in launch_variables: + configuration_name = launch_variables.pop( 'configuration' ) + elif ( len( configurations ) == 1 and + next( iter( configurations.values() ) ).get( "autoselect", True ) ): + configuration_name = next( iter( configurations.keys() ) ) + else: + # Find a single configuration with 'default' True and autoselect not False + defaults = { n: c for n, c in configurations.items() + if c.get( 'default', False ) is True + and c.get( 'autoselect', True ) is not False } + + if len( defaults ) == 1: + configuration_name = next( iter( defaults.keys() ) ) + else: + configuration_name = utils.SelectFromList( + 'Which launch configuration?', + sorted( configurations.keys() ) ) + + if not configuration_name or configuration_name not in configurations: + return None, None + + configuration = configurations[ configuration_name ] + + return configuration_name, configuration + + +def SuggestConfiguration( current_file, filetypes ): + nothing = None, None + templates = [] + filetypes = set( filetypes ) + + for gadget_name, gadget in gadgets.GADGETS.items(): + spec = {} + spec.update( gadget.get( 'all', {} ) ) + spec.update( gadget.get( install.GetOS(), {} ) ) + + for template in spec.get( 'templates', [] ): + if filetypes.intersection( template.get( 'filetypes', set() ) ): + # We _must_ copy the template as we end up taking bits out of it to + # assign to the resulting config. Python... references... mutabiliy.. + # fun. + templates.append( copy.deepcopy( template ) ) + + if not templates: + return nothing + + template_idx = utils.SelectFromList( + 'No debug configurations were found for this project, ' + 'Would you like to use one of the following templates?', + [ t[ 'description' ] for t in templates ], + ret = 'index' ) + + if template_idx is None: + return nothing + + template = templates[ template_idx ] + + config_index = utils.SelectFromList( + 'Which configuration?', + [ c[ 'description' ] for c in template[ 'configurations' ] ], + ret = 'index' ) + + if config_index is None: + return nothing + + configuration = template[ 'configurations' ][ config_index ] + configuration_name = utils.AskForInput( 'Give the config a name: ', + configuration[ 'description' ] ) + + configuration[ 'launch_configuration' ][ 'generated' ] = { + 'name': configuration_name, + 'path': os.path.join( os.path.dirname( current_file ), '.vimspector.json' ), + } + + return configuration_name, configuration[ 'launch_configuration' ] + + +def SaveConfiguration( configuration ): + gen = configuration.pop( 'generated', None ) + if not gen: + return + + if utils.Confirm( + f'Would you like to save the configuration named "{ gen[ "name" ] }"', + '&Yes\n&No' ) != 1: + return + + config_path = utils.AskForInput( 'Enter the path to save to: ', + gen[ 'path' ] ) + + if not config_path: + return + + os.makedirs( os.path.dirname( config_path ), exist_ok = True ) + current_contents = {} + + if os.path.exists( config_path ): + if utils.Confirm( 'File exists, merge with this configuration?\n' + '(NOTE: comments and formatting in the existing file ' + 'will be LOST!!)', + '&Yes\n&No' ) != 1: + return + + with open( config_path, 'r' ) as f: + current_contents = json.loads( minify( f.read() ) ) + + # TODO: how much of configuration is mangled at this point ? + # TODO: how about the defaulted arguments? All the refs are replaced at this + # point? + current_contents.setdefault( 'configurations', {} ) + current_contents[ 'configurations' ][ gen[ 'name' ] ] = configuration + + with open( config_path, 'w' ) as f: + json.dump( current_contents, f, indent=2 ) + + utils.UserMessage( f'Wrote { config_path }', persist = True ) + + +def SelectAdapter( api_prefix, + debug_session, + configuration_name, + configuration, + adapters, + launch_variables ): + adapter = configuration.get( 'adapter' ) + + if isinstance( adapter, str ): + adapter_dict = adapters.get( adapter ) + + if adapter_dict is None: + suggested_gadgets = installer.FindGadgetForAdapter( adapter ) + if suggested_gadgets: + response = utils.AskForInput( + f"The specified adapter '{adapter}' is not " + "installed. Would you like to install the following gadgets? ", + ' '.join( suggested_gadgets ) ) + if response: + new_launch_variables = dict( launch_variables ) + new_launch_variables[ 'configuration' ] = configuration_name + + installer.RunInstaller( + api_prefix, + False, # Don't leave open + *shlex.split( response ), + then = debug_session.Start( new_launch_variables ) ) + return None + elif response is None: + return None + + utils.UserMessage( f"The specified adapter '{adapter}' is not " + "available. Did you forget to run " + "'install_gadget.py'?", + persist = True, + error = True ) + return None + + adapter = adapter_dict + + return adapter + + +def ResolveConfiguration( adapter, + configuration, + launch_variables, + workspace_root, + current_file ): + # Additional vars as defined by VSCode: + # + # ${workspaceFolder} - the path of the folder opened in VS Code + # ${workspaceFolderBasename} - the name of the folder opened in VS Code + # without any slashes (/) + # ${file} - the current opened file + # ${relativeFile} - the current opened file relative to workspaceFolder + # ${fileBasename} - the current opened file's basename + # ${fileBasenameNoExtension} - the current opened file's basename with no + # file extension + # ${fileDirname} - the current opened file's dirname + # ${fileExtname} - the current opened file's extension + # ${cwd} - the task runner's current working directory on startup + # ${lineNumber} - the current selected line number in the active file + # ${selectedText} - the current selected text in the active file + # ${execPath} - the path to the running VS Code executable + def relpath( p, relative_to ): + if not p: + return '' + return os.path.relpath( p, relative_to ) + + def splitext( p ): + if not p: + return [ '', '' ] + return os.path.splitext( p ) + + variables = { + 'dollar': '$', # HACK. Hote '$$' also works. + 'workspaceRoot': workspace_root, + 'workspaceFolder': workspace_root, + 'gadgetDir': install.GetGadgetDir( VIMSPECTOR_HOME ), + 'file': current_file, + } + + calculus = { + 'relativeFile': lambda: relpath( current_file, workspace_root ), + 'fileBasename': lambda: os.path.basename( current_file ), + 'fileBasenameNoExtension': + lambda: splitext( os.path.basename( current_file ) )[ 0 ], + 'fileDirname': lambda: os.path.dirname( current_file ), + 'fileExtname': lambda: splitext( os.path.basename( current_file ) )[ 1 ], + # NOTE: this is the window-local cwd for the current window, *not* Vim's + # working directory. + 'cwd': os.getcwd, + 'unusedLocalPort': utils.GetUnusedLocalPort, + } + + # Pretend that vars passed to the launch command were typed in by the user + # (they may have been in theory) + USER_CHOICES.update( launch_variables ) + variables.update( launch_variables ) + + variables.update( + utils.ParseVariables( adapter.get( 'variables', {} ), + variables, + calculus, + USER_CHOICES ) ) + variables.update( + utils.ParseVariables( configuration.get( 'variables', {} ), + variables, + calculus, + USER_CHOICES ) ) + + + utils.ExpandReferencesInDict( configuration, + variables, + calculus, + USER_CHOICES ) + utils.ExpandReferencesInDict( adapter, + variables, + calculus, + USER_CHOICES ) diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index 5ab9872..0aba76b 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -333,25 +333,41 @@ def UserMessage( msg, persist=False, error=False ): @contextlib.contextmanager -def InputSave(): +def AskingForUserInput(): vim.eval( 'inputsave()' ) try: yield finally: vim.eval( 'inputrestore()' ) + # Clear the command line so that subsequent + vim.command( 'redraw' ) -def SelectFromList( prompt, options ): - with InputSave(): +def SelectFromList( prompt, + options, + ret = 'text', + default_index = None ): + with AskingForUserInput(): display_options = [ prompt ] - display_options.extend( [ '{0}: {1}'.format( i + 1, v ) - for i, v in enumerate( options ) ] ) + display_options.extend( [ + '{0}{1}: {2}'.format( '*' if i == default_index else ' ', + 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 ] + if default_index is not None: + selection = default_index + else: + return None + + if ret == 'text': + return options[ selection ] + else: + return selection except ( KeyboardInterrupt, vim.error ): return None @@ -362,7 +378,7 @@ def AskForInput( prompt, default_value = None ): else: default_option = ", '{}'".format( Escape( default_value ) ) - with InputSave(): + with AskingForUserInput(): try: return vim.eval( "input( '{}' {} )".format( Escape( prompt ), default_option ) ) @@ -370,6 +386,14 @@ def AskForInput( prompt, default_value = None ): return None +def Confirm( msg, *args ): + with AskingForUserInput(): + try: + return int( Call( 'confirm', msg, *args ) ) + except ( KeyboardInterrupt, vim.error ): + return 0 + + def AppendToBuffer( buf, line_or_lines, modified=False ): line = 1 try: @@ -512,10 +536,7 @@ def ExpandReferencesInString( orig_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 - + while True: try: s = _Substitute( s, mapping ) break @@ -531,11 +552,25 @@ def ExpandReferencesInString( orig_s, if default_value is None and e.default_value is not None: try: default_value = _Substitute( e.default_value, mapping ) - except MissingSubstitution: - default_value = e.default_value + except MissingSubstitution as f: + if f.name in calculus: + default_value = calculus[ f.name ]() + else: + default_value = e.default_value - mapping[ key ] = AskForInput( 'Enter value for {}: '.format( key ), - default_value ) + value_list = None + if default_value: + default_value_list = default_value.split( '\n' ) + if len( default_value_list ) > 1: + value_list = default_value_list + + if value_list: + mapping[ key ] = SelectFromList( f'Select value for { key }: ', + default_value_list, + default_index = 0 ) + else: + mapping[ key ] = AskForInput( 'Enter value for {}: '.format( key ), + default_value ) if mapping[ key ] is None: raise KeyboardInterrupt @@ -570,8 +605,6 @@ def CoerceType( mapping: typing.Dict[ str, typing.Any ], key: str ): mapping[ key ] = DICT_TYPES[ new_type ]( value ) -# 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, calculus, user_choices ): for k in list( obj.keys() ): obj[ k ] = ExpandReferencesInObject( obj[ k ], @@ -787,6 +820,7 @@ def WindowID( window, tab=None ): return int( Call( 'win_getid', window.number, tab.number ) ) +@memoize def UseWinBar(): # Buggy neovim doesn't render correctly when the WinBar is defined: # https://github.com/neovim/neovim/issues/12689