changeset 0:5d2c0db51914

Initial commit
author Ludovic Chabant <ludovic@chabant.com>
date Tue, 17 Sep 2019 13:24:24 -0700
parents
children 426cb9c33353
files README.md autoload/vimcrosoft.vim autoload/vimcrosoft/fzf.vim autoload/vimcrosoft/youcompleteme.vim compiler/vimcrosoftsln.vim doc/vimcrosoft.vim plugin/vimcrosoft.vim scripts/__init__.py scripts/build_sln_cache.py scripts/get_proj_config.py scripts/list_sln_configs.py scripts/list_sln_files.py scripts/list_sln_projects.py scripts/logutil.py scripts/vimutil.py scripts/vsutil.py scripts/ycm_extra_conf.py
diffstat 16 files changed, 1803 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README.md	Tue Sep 17 13:24:24 2019 -0700
@@ -0,0 +1,10 @@
+
+# Vim-crosoft
+
+Vimcrosoft is a Vim plugin for working with Microsoft Visual Studio solutions.
+It makes it possible to build solutions and projects from Vim, and integrates
+with various other plugins, such as letting you list solution files in FZF, or
+get code completion with YouCompleteMe.
+
+It's currently a work in progress in alpha state.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/autoload/vimcrosoft.vim	Tue Sep 17 13:24:24 2019 -0700
@@ -0,0 +1,432 @@
+" vimcrosoft.vim - A wrapper for Visual Studio solutions
+
+" Utilities {{{
+
+let s:basedir = expand('<sfile>:p:h:h')
+
+function! vimcrosoft#throw(message)
+    throw "vimcrosoft: ".a:message
+endfunction
+
+function! vimcrosoft#error(message)
+    let v:errmsg = "vimcrosoft: ".a:message
+    echoerr v:errmsg
+endfunction
+
+function! vimcrosoft#warning(message)
+    echohl WarningMsg
+    echom "vimcrosoft: ".a:message
+    echohl None
+endfunction
+
+function! vimcrosoft#trace(message)
+    if g:vimcrosoft_trace
+        echom "vimcrosoft: ".a:message
+    endif
+endfunction
+
+function! vimcrosoft#ensure_msbuild_found() abort
+    if !empty(g:vimcrosoft_msbuild_path)
+        return 1
+    endif
+
+    let l:programfilesdir = get(environ(), 'ProgramFiles(x86)')
+    let l:vswhere = l:programfilesdir.'\Microsoft Visual Studio\Installer\vswhere.exe'
+    if !executable(l:vswhere)
+        call vimcrosoft#error("Can't find `vswhere` -- you must set `g:vimcrosoft_msbuild_path` yourself.")
+        return 0
+    endif
+
+    let l:vswhere_cmdline = '"'.l:vswhere.'" '.
+                \'-prerelease -latest -products * '.
+                \'-requires Microsoft.Component.MSBuild '.
+                \'-property installationPath'
+    call vimcrosoft#trace("Trying to find MSBuild, running: ".l:vswhere_cmdline)
+    let l:installdirs = split(system(l:vswhere_cmdline), '\n')
+    call vimcrosoft#trace("Got: ".string(l:installdirs))
+    for installdir in l:installdirs
+        let l:msbuild = installdir.'\MSBuild\Current\Bin\MSBuild.exe'
+        if executable(l:msbuild)
+            let g:vimcrosoft_msbuild_path = l:msbuild
+            return 1
+        endif
+		let l:msbuild = installdir.'\MSBuild\15.0\Bin\MSBuild.exe'
+		if executable(l:msbuild)
+            let g:vimcrosoft_msbuild_path = l:msbuild
+            return 1
+        endif
+    endfor
+
+    call vimcrosoft#error("Couldn't find MSBuild anywhere in:\n".
+                \string(l:installdirs))
+    return 0
+endfunction
+
+" }}}
+
+" Cache Files {{{
+
+function! vimcrosoft#get_sln_cache_dir(...) abort
+    if empty(g:vimcrosoft_current_sln)
+        if a:0 && a:1
+            call vimcrosoft#throw("No solution is currently set.")
+        endif
+        return ''
+    endif
+    let l:cache_dir = fnamemodify(g:vimcrosoft_current_sln, ':h')
+    let l:cache_dir .= '\.vimcrosoft'
+    return l:cache_dir
+endfunction
+
+function! vimcrosoft#get_sln_cache_file(filename) abort
+    let l:cache_dir = vimcrosoft#get_sln_cache_dir()
+    if empty(l:cache_dir)
+        return ''
+    else
+        return l:cache_dir.'\'.a:filename
+    endif
+endfunction
+
+" }}}
+
+" Configuration Files {{{
+
+let s:config_format = 2
+
+function! vimcrosoft#save_config() abort
+    if empty(g:vimcrosoft_current_sln)
+        return
+    endif
+
+    call vimcrosoft#trace("Saving config for: ".g:vimcrosoft_current_sln)
+    let l:lines = [
+                \'format='.string(s:config_format),
+                \g:vimcrosoft_current_sln,
+                \g:vimcrosoft_current_config,
+                \g:vimcrosoft_current_platform,
+                \g:vimcrosoft_active_project]
+    let l:configfile = vimcrosoft#get_sln_cache_file('config.txt')
+    call writefile(l:lines, l:configfile)
+endfunction
+
+function! vimcrosoft#load_config() abort
+    if empty(g:vimcrosoft_current_sln)
+        return
+    endif
+
+    let l:configfile = vimcrosoft#get_sln_cache_file('config.txt')
+    if !filereadable(l:configfile)
+        return
+    endif
+
+    let l:lines = readfile(l:configfile)
+    let l:format_line = l:lines[0]
+    if l:format_line == 'format='.string(s:config_format)
+        let g:vimcrosoft_current_sln = l:lines[1]
+        let g:vimcrosoft_current_config = l:lines[2]
+        let g:vimcrosoft_current_platform = l:lines[3]
+        let g:vimcrosoft_active_project = l:lines[4]
+    else
+        call vimcrosoft#warning("Solution configuration format has changed ".
+                    \"since you last opened this solution in Vim. ".
+                    \"You previous configuration/platform has NOT been ".
+                    \"restored.")
+    endif
+endfunction
+
+" }}}
+
+" {{{ Scripts
+
+let s:scriptsdir = s:basedir.'\scripts'
+
+function! vimcrosoft#get_script_path(scriptname) abort
+    return s:scriptsdir.'\'.a:scriptname
+endfunction
+
+function! vimcrosoft#exec_script_job(scriptname, ...) abort
+    let l:scriptpath = vimcrosoft#get_script_path(a:scriptname)
+    let l:cmd = ['python', l:scriptpath] + a:000
+    return job_start(l:cmd)
+endfunction
+
+let s:scriptsdir_added_to_sys = 0
+let s:scripts_imported = []
+
+function! s:install_scriptsdir() abort
+    if !s:scriptsdir_added_to_sys
+        execute 'python3 import sys'
+        execute 'python3 sys.path.append("'.escape(s:scriptsdir, "\\").'")'
+        execute 'python3 import vimutil'
+        let s:scriptsdir_added_to_sys = 1
+    endif
+endfunction
+
+function! vimcrosoft#exec_script_now(scriptname, ...) abort
+    if g:vimcrosoft_use_external_python
+        let l:cmd = 'python '.shellescape(vimcrosoft#get_script_path(a:scriptname.'.py'))
+        " TODO: shellescape arguments?
+        let l:cmd .= ' '.join(a:000, " ")
+        let l:output = system(l:cmd)
+    else
+        call s:install_scriptsdir()
+        if index(s:scripts_imported, a:scriptname) < 0
+            execute 'python3 import '.a:scriptname
+            call add(s:scripts_imported, a:scriptname)
+        endif
+        let l:line = 'vimutil.runscript('.a:scriptname.'.main'
+        if a:0 > 0
+            let l:args = copy(a:000)
+            call map(l:args, {idx, val -> escape(val, '\"')})
+            let l:line .= ', "'.join(l:args, '", "').'"'
+        endif
+        let l:line .= ')'
+        call vimcrosoft#trace("Executing: ".l:line)
+        let l:output = py3eval(l:line)
+    endif
+    return l:output
+endfunction
+
+" }}}
+
+" Module Management {{{
+
+let s:modulesdir = s:basedir.'\autoload\vimcrosoft'
+let s:modules = glob(s:modulesdir.'\*.vim', 0, 1)
+
+function! vimcrosoft#call_modules(funcname, ...) abort
+    for modpath in s:modules
+        let l:modname = fnamemodify(modpath, ':t:r')
+        let l:fullfuncname = 'vimcrosoft#'.l:modname.'#'.a:funcname
+        if exists("*".l:fullfuncname)
+            call vimcrosoft#trace("Module ".l:modname.": calling ".a:funcname)
+            call call(l:fullfuncname, a:000)
+        else
+            call vimcrosoft#trace("Skipping ".l:fullfuncname.": doesn't exist.")
+        endif
+    endfor
+endfunction
+
+" }}}
+
+" Solution Management {{{
+
+function! vimcrosoft#set_sln(slnpath, ...) abort
+    let g:vimcrosoft_current_sln = a:slnpath
+
+    let l:sln_was_set = !empty(a:slnpath)
+    if l:sln_was_set
+        let g:vimcrosoft_current_sln_cache = vimcrosoft#get_sln_cache_file("slncache.bin")
+        call vimcrosoft#call_modules('on_sln_changed', a:slnpath)
+    else
+        let g:vimcrosoft_current_sln_cache = ''
+        call vimcrosoft#call_modules('on_sln_cleared')
+    endif
+
+    let l:silent = a:0 && a:1
+    if !l:silent
+        call vimcrosoft#save_config()
+
+        if l:sln_was_set
+            echom "Current solution: ".a:slnpath
+        else
+            echom "No current solution anymore"
+        endif
+    endif
+endfunction
+
+function! vimcrosoft#auto_find_sln(...) abort
+    let l:path = getcwd()
+    try
+        let l:slnpath = vimcrosoft#find_sln(l:path)
+    catch /^vimcrosoft:/
+        let l:slnpath = ''
+    endtry
+    let l:silent = a:0 && a:1
+    call vimcrosoft#set_sln(l:slnpath, l:silent)
+endfunction
+
+function! vimcrosoft#find_sln(curpath) abort
+    if g:vimcrosoft_sln_finder != ''
+        return call(g:vimcrosoft_sln_finder, [a:curpath])
+    endif
+    return vimcrosoft#default_sln_finder(a:curpath)
+endfunction
+
+function! vimcrosoft#default_sln_finder(path) abort
+    let l:cur = a:path
+    let l:prev = ""
+    while l:cur != l:prev
+        let l:slnfiles = globpath(l:cur, '*.sln', 0, 1)
+        if !empty(l:slnfiles)
+            call vimcrosoft#trace("Found solution file: ".l:slnfiles[0])
+            return l:slnfiles[0]
+        endif
+        let l:prev = l:cur
+        let l:cur = fnamemodify(l:cur, ':h')
+    endwhile
+    call vimcrosoft#throw("No solution file found.")
+endfunction
+
+function! vimcrosoft#set_active_project(projname, ...) abort
+    " Strip trailing spaces in the project name.
+    let l:projname = substitute(a:projname, '\v\s+$', '', 'g')
+
+    let g:vimcrosoft_active_project = l:projname
+    call vimcrosoft#call_modules('on_active_project_changed', l:projname)
+
+    let l:silent = a:0 && a:1
+    if !l:silent
+        call vimcrosoft#save_config()
+        echom "Active project changed"
+    endif
+endfunction
+
+function! vimcrosoft#build_sln(target) abort
+    let l:args = []
+    if !empty(a:target)
+        call add(l:args, '/t:'.a:target)
+    endif
+    call vimcrosoft#run_make(l:args)
+endfunction
+
+function! vimcrosoft#build_project(projname, target, only) abort
+    let l:projname = !empty(a:projname) ? a:projname : g:vimcrosoft_active_project
+    if empty(l:projname)
+        call vimcrosoft#error("No project name given, and no active project set.")
+        return
+    endif
+
+    " Strip trailing spaces in the project name.
+    let l:projname = substitute(l:projname, '\v\s+$', '', 'g')
+    let l:target = '/t:'.tr(l:projname, '.', '_')
+    if !empty(a:target)
+        let l:target .= ':'.a:target
+    endif
+
+    let l:args = []
+    call add(l:args, l:target)
+    if a:only
+        call add(l:args, '/p:BuildProjectReferences=false')
+    endif
+    call vimcrosoft#run_make(l:args)
+endfunction
+
+function! vimcrosoft#run_make(customargs) abort
+    if !vimcrosoft#ensure_msbuild_found()
+        return
+    endif
+
+    " Add some common arguments for MSBuild.
+    let l:fullargs = copy(a:customargs)
+    call add(l:fullargs, '"/p:Configuration='.g:vimcrosoft_current_config.'"')
+    call add(l:fullargs, '"/p:Platform='.g:vimcrosoft_current_platform.'"')
+    " Add the solution file itself.
+    call add(l:fullargs, '"'.g:vimcrosoft_current_sln.'"')
+
+    " Setup the backdoor args list for our compiler to pick-up, and run
+    " the make process.
+    let g:vimcrosoft_temp_compiler_args__ = l:fullargs
+    compiler vimcrosoftsln
+    if !empty(g:vimcrosoft_make_command)
+        execute g:vimcrosoft_make_command
+    elseif exists(":Make")  " Support for vim-dispatch.
+        Make
+    else
+        make
+    endif
+endfunction
+
+function! vimcrosoft#set_config_platform(configplatform)
+    let l:bits = split(a:configplatform, '|')
+    if len(l:bits) != 2
+        call vimcrosoft#throw("Expected a value of the form: Config|Platform")
+    endif
+
+    let g:vimcrosoft_current_config = l:bits[0]
+    let g:vimcrosoft_current_platform = l:bits[1]
+    call vimcrosoft#call_modules('on_config_platform_changed', 
+                \g:vimcrosoft_current_config, g:vimcrosoft_current_platform)
+
+    call vimcrosoft#save_config()
+endfunction
+
+function! vimcrosoft#get_sln_project_names() abort
+    if empty(g:vimcrosoft_current_sln)
+        return []
+    endif
+    let l:output = vimcrosoft#exec_script_now("list_sln_projects",
+                \g:vimcrosoft_current_sln,
+                \'-c', g:vimcrosoft_current_sln_cache,
+                \'--full-names')
+    return split(l:output, "\n")
+endfunction
+
+function! vimcrosoft#get_sln_config_platforms() abort
+    if empty(g:vimcrosoft_current_sln)
+        return []
+    endif
+    let l:output = vimcrosoft#exec_script_now("list_sln_configs",
+                \g:vimcrosoft_current_sln,
+                \'-c', g:vimcrosoft_current_sln_cache)
+    return split(l:output, "\n")
+endfunction
+
+" }}}
+
+" {{{ Commands Auto-completion
+
+function! vimcrosoft#complete_current_sln_projects(ArgLead, CmdLine, CursorPos)
+    let l:proj_names = vimcrosoft#get_sln_project_names()
+    let l:argpat = '^'.substitute(a:ArgLead, '\', '', 'g')
+    let l:projnames = filter(l:proj_names,
+                \{idx, val -> val =~? l:argpat})
+    return l:projnames
+endfunction
+
+function! vimcrosoft#complete_current_sln_config_platforms(ArgLead, CmdLine, CursorPos)
+    let l:cfgplats = vimcrosoft#get_sln_config_platforms()
+    let l:argpat = '^'.substitute(a:ArgLead, '\', '', 'g')
+    let l:cfgplatnames = filter(l:cfgplats,
+                \{idx, val -> val =~? l:argpat})
+    return l:cfgplatnames
+endfunction
+
+" }}}
+
+" {{{ Statusline Functions
+
+function! vimcrosoft#statusline(...)
+    if empty(g:vimcrosoft_current_sln)
+        return ''
+    endif
+
+    let l:line = fnamemodify(g:vimcrosoft_current_sln, ':t')
+    if !empty(g:vimcrosoft_active_project)
+        let l:line .= '('.g:vimcrosoft_active_project.')'
+    endif
+    let l:line .= ' ['.
+                \g:vimcrosoft_current_config.'|'.
+                \g:vimcrosoft_current_platform.']'
+    return l:line
+endfunction
+
+" }}}
+
+" {{{ Initialization
+
+function! vimcrosoft#init() abort
+    call vimcrosoft#trace("Loading modules...")
+    for modpath in s:modules
+        execute 'source '.fnameescape(modpath)
+    endfor
+
+    call vimcrosoft#call_modules('init')
+
+    if g:vimcrosoft_auto_find_sln
+        call vimcrosoft#auto_find_sln(1)
+        call vimcrosoft#load_config()
+    endif
+endfunction
+
+" }}}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/autoload/vimcrosoft/fzf.vim	Tue Sep 17 13:24:24 2019 -0700
@@ -0,0 +1,19 @@
+
+function! vimcrosoft#fzf#init() abort
+endfunction
+
+function! vimcrosoft#fzf#on_sln_changed(slnpath) abort
+    let $FZF_DEFAULT_COMMAND = s:build_file_list_command(a:slnpath)
+endfunction
+
+function! vimcrosoft#fzf#on_sln_cleared() abort
+    unlet $FZF_DEFAULT_COMMAND
+endfunction
+
+function! s:build_file_list_command(slnpath) abort
+    let l:scriptpath = vimcrosoft#get_script_path('list_sln_files.py')
+    return 'python '.shellescape(l:scriptpath).
+                \' '.shellescape(a:slnpath)
+                \.' --cache '.shellescape(g:vimcrosoft_current_sln_cache)
+endfunction
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/autoload/vimcrosoft/youcompleteme.vim	Tue Sep 17 13:24:24 2019 -0700
@@ -0,0 +1,24 @@
+
+function! vimcrosoft#youcompleteme#init() abort
+endfunction
+
+function! vimcrosoft#youcompleteme#on_sln_changed(slnpath) abort
+    let g:ycm_global_ycm_extra_conf = vimcrosoft#get_script_path('ycm_extra_conf.py')
+    let g:ycm_extra_conf_vim_data = [
+                \'g:vimcrosoft_current_sln',
+                \'g:vimcrosoft_current_sln_cache',
+                \'g:vimcrosoft_current_config',
+                \'g:vimcrosoft_current_platform'
+                \]
+endfunction
+
+function! vimcrosoft#youcompleteme#on_sln_cleared() abort
+    let g:ycm_global_ycm_extra_conf = ''
+    let g:ycm_extra_conf_vim_data = []
+endfunction
+
+function! vimcrosoft#youcompleteme#on_config_platform_changed(config, platform) abort
+    if exists(":YcmCompleter")
+        YcmCompleter ClearCompilationFlagCache 
+    endif
+endfunction
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/compiler/vimcrosoftsln.vim	Tue Sep 17 13:24:24 2019 -0700
@@ -0,0 +1,28 @@
+" Compiler file for Visual Studio
+" Compiler: Visual Studio
+" Maintainer: Ludovic Chabant <https://ludovic.chabant.com>
+
+if exists("current_compiler")
+    finish
+endif
+let current_compiler = "vimcrosoftsln"
+
+let s:keepcpo = &cpo
+
+let s:prgargs = ''
+if !empty(g:vimcrosoft_temp_compiler_args__)
+    let s:tmpargs = map(
+                \copy(g:vimcrosoft_temp_compiler_args__),
+                \{idx, val -> escape(val, ' \"')})
+    let s:prgargs = '\ '.join(s:tmpargs, '\ ')
+endif
+
+let s:prgcmdline = fnameescape('"'.g:vimcrosoft_msbuild_path.'"').s:prgargs
+call vimcrosoft#trace("Setting makeprg to: ".s:prgcmdline)
+execute "CompilerSet makeprg=".s:prgcmdline
+
+CompilerSet errorformat&
+
+let &cpo = s:keepcpo
+unlet s:keepcpo
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/vimcrosoft.vim	Tue Sep 17 13:24:24 2019 -0700
@@ -0,0 +1,169 @@
+*vimcrosoft.txt*  Work with Visual Studio solutions in Vim
+
+
+$$\    $$\$$$$$$\$$\      $$\ $$$$$$\ $$$$$$$\  $$$$$$\  $$$$$$\  $$$$$$\ $$$$$$$$\$$$$$$$$\ 
+$$ |   $$ \_$$  _$$$\    $$$ $$  __$$\$$  __$$\$$  __$$\$$  __$$\$$  __$$\$$  _____\__$$  __|
+$$ |   $$ | $$ | $$$$\  $$$$ $$ /  \__$$ |  $$ $$ /  $$ $$ /  \__$$ /  $$ $$ |        $$ |   
+\$$\  $$  | $$ | $$\$$\$$ $$ $$ |     $$$$$$$  $$ |  $$ \$$$$$$\ $$ |  $$ $$$$$\      $$ |   
+ \$$\$$  /  $$ | $$ \$$$  $$ $$ |     $$  __$$<$$ |  $$ |\____$$\$$ |  $$ $$  __|     $$ |   
+  \$$$  /   $$ | $$ |\$  /$$ $$ |  $$\$$ |  $$ $$ |  $$ $$\   $$ $$ |  $$ $$ |        $$ |   
+   \$  /  $$$$$$\$$ | \_/ $$ \$$$$$$  $$ |  $$ |$$$$$$  \$$$$$$  |$$$$$$  $$ |        $$ |   
+    \_/   \______\__|     \__|\______/\__|  \__|\______/ \______/ \______/\__|        \__|   
+
+
+                                   VIM-CROSOFT
+
+                                                                  *vimcrosoft*
+
+==============================================================================
+Configuration                                       *vimcrosoft-configuration*
+
+                                                          *g:vimcrosoft_trace*
+g:vimcrosoft_trace
+                        Enables debugging information.
+                        Default: `0`
+
+                                                  *g:vimcrosoft_auto_find_sln*
+g:vimcrosoft_auto_find_sln
+                        Try and find a solution for the current working
+                        directory on Vim startup. This effectively executes
+                        |:VimcrosoftAutoFindSln| upon startup.
+                        Default: `0`
+
+                                                     *g:vimcrosoft_sln_finder*
+g:vimcrosoft_sln_finder
+                        The name of a function to call to find a solution file
+                        from a given current path. If not set, Vimcrosoft will
+                        use its own default finder, which just walks up the
+                        directory tree until it finds any `*.sln` files.
+                        Default: `""`
+
+                                                   *g:vimcrosoft_msbuild_path*
+g:vimcrosoft_msbuild_path
+                        By default, Vimcrosoft automatically finds where
+                        MSBuild is installed. If that fails, you can specify
+                        the path to the MSBuild executable directly.
+                        Default: `""`
+
+                                                   *g:vimcrosoft_make_command*
+g:vimcrosoft_make_command
+                        The command to run when starting builds. If empty,
+                        Vimcrosoft will use |:make|, unless the vim-dispatch
+                        plugin is detected, in which case it will use |:Make|.
+                        If the option is not empty, it will use whatever you
+                        specified.
+                        Default: `""`
+
+==============================================================================
+Commands                                                 *vimcrosoft-commands*
+
+                                                *vimcrosoft-solution-commands*
+Here's a list of solution-related commands:
+
+                                                      *:VimcrosoftAutoFindSln*
+:VimcrosoftAutoFindSln
+                        Finds a solution file (`*.sln`) in the current working
+                        directory, or any of its parent directories, and sets
+                        it as the current solution file (see 
+                        |:VimcrosoftSetSln|). If any solution files are found,
+                        the first one is used.
+
+                                                           *:VimcrosoftSetSln*
+:VimcrosoftSetSln <file>
+                        Sets the currently active solution file. All
+                        vim-crosoft commands will relate to this solution.
+
+                                                         *:VimcrosoftUnsetSln*
+:VimcrosoftUnsetSln
+                        Unsets the currently active solution file.
+
+                                                *:VimcrosoftSetConfigPlatform*
+:VimcrosoftSetConfigPlatform <configplatform>
+                        Sets the currently active configuration and platform
+                        for the active solution. The argument is a combo of
+                        configuration and platform, in the form of
+                        `Configuration|Platform`.
+
+                                                         *:VimcrosoftBuildSln*
+:VimcrosoftBuildSln
+                        Starts a build on the current solution, using the
+                        current configuration and platform.
+
+                                                       *:VimcrosoftRebuildSln*
+:VimcrosoftRebuildSln
+                        Rebuilds the current solution, using the current
+                        configuration and platform.
+
+                                                         *:VimcrosoftCleanSln*
+:VimcrosoftCleanSln
+                        Cleans the current solution for the current
+                        configuration and platform.
+
+
+                                                 *vimcrosoft-project-commands*
+Here are some project-related commands:
+
+                                                     *:VimcrosoftBuildProject*
+:VimcrosoftBuildProject
+                        Builds the active project for the current
+                        configuration and platform. MSBuild will typically
+                        build all its dependencies first.
+
+                                                 *:VimcrosoftBuildProjectOnly*
+:VimcrosoftBuildProjectOnly
+                        Builds the active project for the current
+                        configuration and platform, but skips building any
+                        dependencies.
+
+                                                   *:VimcrosoftRebuildProject*
+:VimcrosoftRebuildProject
+                        Rebuilds the active project for the current
+                        configuration and platform.
+
+                                                     *:VimcrosoftCleanProject*
+:VimcrosoftCleanProject
+                        Cleans the active project for the current
+                        configuration and platform.
+
+
+                                          *vimcrosoft-active-project-commands*
+Vimcrosoft lets you specify an "active project" that makes it quicker to
+build/clean/etc.
+
+                                                 *:VimcrosoftSetActiveProject*
+:VimcrosoftSetActiveProject
+                        Sets the active project for the current solution. This
+                        enables a few "shortcut" commands that operate on it
+                        directly.
+
+                                               *:VimcrosoftBuildActiveProject*
+:VimcrosoftBuildActiveProject
+                        Builds the active project for the current
+                        configuration and platform. MSBuild will typically
+                        build all its dependencies first.
+
+                                           *:VimcrosoftBuildActiveProjectOnly*
+:VimcrosoftBuildActiveProjectOnly
+                        Builds the active project for the current
+                        configuration and platform, but skips building any
+                        dependencies.
+
+                                             *:VimcrosoftRebuildActiveProject*
+:VimcrosoftRebuildActiveProject
+                        Rebuilds the active project for the current
+                        configuration and platform.
+
+                                               *:VimcrosoftCleanActiveProject*
+:VimcrosoftCleanActiveProject
+                        Cleans the active project for the current
+                        configuration and platform.
+
+
+==============================================================================
+Statusline                                             *vimcrosoft-statusline*
+
+You can show some vimcrosoft-related information in your 'statusline' by
+calling the `vimcrosoft#statusline()` function.
+
+
+" vim:tw=78:et:ft=help:norl:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugin/vimcrosoft.vim	Tue Sep 17 13:24:24 2019 -0700
@@ -0,0 +1,74 @@
+" vimcrosoft.vim - A wrapper for Visual Studio solutions
+" Maintainer:    Ludovic Chabant <https://ludovic.chabant.com>
+
+" Globals {{{
+
+if (&cp || get(g:, 'vimcrosoft_dont_load', 0))
+    finish
+endif
+
+let g:vimcrosoft_trace = get(g:, 'vimcrosoft_trace', 0)
+
+let g:vimcrosoft_current_sln = get(g:, 'vimcrosoft_current_sln', '')
+let g:vimcrosoft_current_config = get(g:, 'vimcrosoft_current_config', '')
+let g:vimcrosoft_current_platform = get(g:, 'vimcrosoft_current_platform', '')
+let g:vimcrosoft_active_project = get(g:, 'vimcrosoft_active_project', '')
+
+let g:vimcrosoft_auto_find_sln = get(g:, 'vimcrosoft_auto_find_sln', 1)
+let g:vimcrosoft_sln_finder = get(g:, 'vimcrosoft_sln_finder', '')
+
+let g:vimcrosoft_current_sln_cache = ''
+
+let g:vimcrosoft_msbuild_path = get(g:, 'vimcrosoft_msbuild_path', '')
+let g:vimcrosoft_use_external_python = get(g:, 'vimcrosoft_use_external_python', 0)
+let g:vimcrosoft_make_command = get(g:, 'vimcrosoft_make_command', '')
+
+" }}}
+
+" Commands {{{
+
+command! VimcrosoftAutoFindSln :call vimcrosoft#auto_find_sln()
+command! -nargs=1 -complete=file VimcrosoftSetSln :call vimcrosoft#set_sln(<f-args>)
+command! VimcrosoftUnsetSln :call Vimcrosoft#set_sln("")
+
+command! -nargs=1 
+            \ -complete=customlist,vimcrosoft#complete_current_sln_config_platforms 
+            \ VimcrosoftSetConfigPlatform 
+            \ :call vimcrosoft#set_config_platform(<f-args>)
+
+command! VimcrosoftBuildSln :call vimcrosoft#build_sln('Build')
+command! VimcrosoftRebuildSln :call vimcrosoft#build_sln('Rebuild')
+command! VimcrosoftCleanSln :call vimcrosoft#build_sln('Clean')
+
+command! -nargs=1 
+            \ -complete=customlist,vimcrosoft#complete_current_sln_projects 
+            \ VimcrosoftSetActiveProject 
+            \ :call vimcrosoft#set_active_project(<f-args>)
+command! VimcrosoftBuildActiveProject :call vimcrosoft#build_project('', '', 0)
+command! VimcrosoftBuildActiveProjectOnly :call vimcrosoft#build_project('', '', 1)
+command! VimcrosoftRebuildActiveProject :call vimcrosoft#build_project('', 'Rebuild', 0)
+command! VimcrosoftCleanActiveProject :call vimcrosoft#build_project('', 'Clean', 0)
+
+command! -nargs=1 
+            \ -complete=customlist,vimcrosoft#complete_current_sln_projects 
+            \ VimcrosoftBuildProject 
+            \ :call vimcrosoft#build_project(<f-args>, '', 0)
+command! -nargs=1 
+            \ -complete=customlist,vimcrosoft#complete_current_sln_projects 
+            \ VimcrosoftBuildProjectOnly 
+            \ :call vimcrosoft#build_project(<f-args>, '', 1)
+command! -nargs=1 
+            \ -complete=customlist,vimcrosoft#complete_current_sln_projects 
+            \ VimcrosoftRebuildProject 
+            \ :call vimcrosoft#build_project(<f-args>, 'Rebuild', 1)
+command! -nargs=1 -complete=customlist,vimcrosoft#complete_current_sln_projects 
+            \ VimcrosoftCleanProject 
+            \ :call vimcrosoft#build_project(<f-args>, 'Clean', 1)
+
+" }}}
+
+" Initialization {{{
+
+call vimcrosoft#init()
+
+" }}}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/build_sln_cache.py	Tue Sep 17 13:24:24 2019 -0700
@@ -0,0 +1,30 @@
+import os.path
+import logging
+import argparse
+from vsutil import SolutionCache
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument('solution',
+                        help="The path to the solution file")
+    parser.add_argument('cache',
+                        help="The path to the cache file")
+    parser.add_argument('-v', '--verbose',
+                        action='store_true')
+    args = parser.parse_args()
+
+    loglevel = logging.INFO
+    if args.verbose:
+        loglevel = logging.DEBUG
+    logging.basicConfig(level=loglevel)
+    logger = logging.getLogger()
+
+    cache, loaded = SolutionCache.load_or_rebuild(args.solution, args.cache)
+    if not loaded:
+        total_items = sum([len(i) for i in cache.index.values()])
+        logger.debug(f"Built cache with {total_items} items.")
+
+
+if __name__ == '__main__':
+    main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/get_proj_config.py	Tue Sep 17 13:24:24 2019 -0700
@@ -0,0 +1,52 @@
+import argparse
+import logging
+import re
+from vsutil import SolutionCache
+
+
+re = _re_proj_cfg_suffix = re.compile(r'\.(ActiveCfg|Build\.0)$')
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument('solution',
+                        help="The path to the Visual Studio solution file.")
+    parser.add_argument('cache',
+                        help="The path to the solution cache.")
+    parser.add_argument('project',
+                        help="The name of the project to check for.")
+    parser.add_argument('slnconfig',
+                        help="The solution configuration.")
+    parser.add_argument('-v', '--verbose',
+                        action='store_true',
+                        help="Show verbose information.")
+    args = parser.parse_args()
+
+    loglevel = logging.INFO
+    if args.verbose:
+        loglevel = logging.DEBUG
+    logging.basicConfig(level=loglevel)
+    logger = logging.getLogger()
+
+    cache, loaded = SolutionCache.load_or_rebuild(args.solution, args.cache)
+    logger.debug("Cache was %s" % ("valid" if loaded else "not valid"))
+
+    proj = cache.slnobj.find_project_by_name(args.project)
+    if proj is None:
+        raise Exception("No such project: %s" % args.project)
+    projguid = '{%s}' % proj.guid
+
+    slnconfig = args.slnconfig.replace(' ', '_')
+    sec = cache.slnobj.globalsection('ProjectConfigurationPlatforms')
+    for e in sec.entries:
+        if e.name.startswith(projguid):
+            if slnconfig == e.value.replace(' ', '_'):
+                projcfg = e.name[len(projguid) + 1:]
+                projcfg = _re_proj_cfg_suffix.sub('', projcfg)
+                print(projcfg)
+                break
+
+
+if __name__ == '__main__':
+    main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/list_sln_configs.py	Tue Sep 17 13:24:24 2019 -0700
@@ -0,0 +1,31 @@
+import argparse
+import logging
+from logutil import setup_logging
+from vsutil import SolutionCache
+
+
+logger = logging.getLogger(__name__)
+
+
+def main(args=None):
+    parser = argparse.ArgumentParser()
+    parser.add_argument('solution',
+                        help="The path to the Visual Studio solution file.")
+    parser.add_argument('-c', '--cache',
+                        help="The path to the solution cache.")
+    parser.add_argument('-v', '--verbose',
+                        action='store_true',
+                        help="Show verbose information.")
+    args = parser.parse_args(args)
+    setup_logging(args.verbose)
+
+    cache, _ = SolutionCache.load_or_rebuild(args.solution, args.cache)
+    sec = cache.slnobj.globalsection('SolutionConfigurationPlatforms')
+    for e in sec.entries:
+        config, platform = e.name.split('|')
+        if config != "Invalid" and platform != "Invalid":
+            print(e.name)
+
+
+if __name__ == '__main__':
+    main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/list_sln_files.py	Tue Sep 17 13:24:24 2019 -0700
@@ -0,0 +1,41 @@
+import argparse
+import logging
+import os.path
+from logutil import setup_logging
+from vsutil import SolutionCache
+
+
+logger = logging.getLogger(__name__)
+
+
+def main(args=None):
+    parser = argparse.ArgumentParser()
+    parser.add_argument('solution',
+                        help="The path to the Visual Studio solution file.")
+    parser.add_argument('-p', '--project',
+                        help="Only list files in the named project.")
+    parser.add_argument('-c', '--cache',
+                        help="Use a cache file to store the file list.")
+    parser.add_argument('-v', '--verbose',
+                        action='store_true',
+                        help="Show verbose information.")
+    args = parser.parse_args(args)
+    setup_logging(args.verbose)
+
+    cache, _ = SolutionCache.load_or_rebuild(args.solution, args.cache)
+    slnobj = cache.slnobj
+
+    projs = slnobj.projects
+    if args.project:
+        projs = [slnobj.find_project_by_name(args.project)]
+    projs = list(filter(lambda p: not p.is_folder, projs))
+
+    for p in projs:
+        ig = p.defaultitemgroup()
+        for i in ig.get_source_items():
+            file_path = os.path.abspath(os.path.join(p.absdirpath, i.include))
+            print(file_path)
+
+
+if __name__ == '__main__':
+    main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/list_sln_projects.py	Tue Sep 17 13:24:24 2019 -0700
@@ -0,0 +1,58 @@
+import argparse
+import logging
+from logutil import setup_logging
+from vsutil import SolutionCache
+
+
+logger = logging.getLogger(__name__)
+
+
+def main(args=None):
+    parser = argparse.ArgumentParser()
+    parser.add_argument('solution',
+                        help="The path to the Visual Studio solution file.")
+    parser.add_argument('-c', '--cache',
+                        help="The path to the solution cache.")
+    parser.add_argument('-f', '--full-names',
+                        action='store_true',
+                        help="Print full names for nested projects.")
+    parser.add_argument('-v', '--verbose',
+                        action='store_true',
+                        help="Show verbose information.")
+    args = parser.parse_args(args)
+    setup_logging(args.verbose)
+
+    cache, _ = SolutionCache.load_or_rebuild(args.solution, args.cache)
+    slnobj = cache.slnobj
+    logger.debug("Found {0} projects:".format(len(slnobj.projects)))
+
+    non_folder_projs = list(filter(lambda p: not p.is_folder,
+                                   slnobj.projects))
+    if args.full_names:
+        parenting = {}
+        nesting_sec = slnobj.globalsection("NestedProjects")
+        if nesting_sec:
+            for entry in nesting_sec.entries:
+                child_guid, parent_guid = (entry.name.strip('{}'),
+                                           entry.value.strip('{}'))
+                child = slnobj.find_project_by_guid(child_guid, False)
+                parent = slnobj.find_project_by_guid(parent_guid, False)
+                parenting[child] = parent
+
+        for p in non_folder_projs:
+            full_name = p.name
+            cur_p = p
+            while True:
+                cur_p = parenting.get(cur_p)
+                if not cur_p:
+                    break
+                full_name = cur_p.name + "\\" + full_name
+            print(full_name)
+    else:
+        for p in non_folder_projs:
+            print(p.name)
+
+
+if __name__ == '__main__':
+    main()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/logutil.py	Tue Sep 17 13:24:24 2019 -0700
@@ -0,0 +1,9 @@
+import logging
+
+
+def setup_logging(verbose):
+    loglevel = logging.INFO
+    if verbose:
+        loglevel = logging.DEBUG
+    logging.basicConfig(level=loglevel)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/vimutil.py	Tue Sep 17 13:24:24 2019 -0700
@@ -0,0 +1,19 @@
+import io
+import sys
+
+
+def runscript(scriptfunc, *args):
+    """ Executes a given function with the given args. This is because
+        the convention is that all python scripts here should have a `main`
+        function that takes a custom list of args to override the default
+        behaviour of using `sys.args`.
+    """
+    prevout = sys.stdout
+    captured = io.StringIO()
+    sys.stdout = captured
+    try:
+        scriptfunc(args)
+    finally:
+        sys.stdout = prevout
+    return captured.getvalue()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/vsutil.py	Tue Sep 17 13:24:24 2019 -0700
@@ -0,0 +1,592 @@
+import copy
+import logging
+import os.path
+import pickle
+import re
+import xml.etree.ElementTree as etree
+
+
+# Known VS project types.
+PROJ_TYPE_FOLDER = '2150E333-8FDC-42A3-9474-1A3956D46DE8'
+PROJ_TYPE_NMAKE = '8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942'
+PROJ_TYPE_CSHARP = 'FAE04EC0-301F-11D3-BF4B-00C04F79EFBC'
+
+PROJ_TYPE_NAMES = {
+    PROJ_TYPE_FOLDER: 'folder',
+    PROJ_TYPE_NMAKE: 'nmake',
+    PROJ_TYPE_CSHARP: 'csharp'
+}
+
+# Known VS item types.
+ITEM_TYPE_CPP_SRC = 'ClCompile'
+ITEM_TYPE_CPP_HDR = 'ClInclude'
+
+ITEM_TYPE_CS_REF = 'Reference'
+ITEM_TYPE_CS_PROJREF = 'ProjectReference'
+ITEM_TYPE_CS_SRC = 'Compile'
+
+ITEM_TYPE_NONE = 'None'
+
+ITEM_TYPE_SOURCE_FILES = (ITEM_TYPE_CPP_SRC, ITEM_TYPE_CPP_HDR,
+                          ITEM_TYPE_CS_SRC)
+
+
+# Known VS properties.
+PROP_CONFIGURATION_TYPE = 'ConfigurationType'
+PROP_NMAKE_PREPROCESSOR_DEFINITIONS = 'NMakePreprocessorDefinitions'
+PROP_NMAKE_INCLUDE_SEARCH_PATH = 'NMakeIncludeSearchPath'
+
+
+logger = logging.getLogger(__name__)
+
+
+def _strip_ns(tag):
+    """ Remove the XML namespace from a tag name. """
+    if tag[0] == '{':
+        i = tag.index('}')
+        return tag[i+1:]
+    return tag
+
+
+re_msbuild_var = re.compile(r'\$\((?P<var>[\w\d_]+)\)')
+
+
+def _resolve_value(val, env):
+    """ Expands MSBuild property values given a build environment. """
+    def _repl_vars(m):
+        varname = m.group('var')
+        varval = env.get(varname, '')
+        return varval
+
+    if not val:
+        return val
+    return re_msbuild_var.sub(_repl_vars, val)
+
+
+def _evaluate_condition(cond, env):
+    """ Expands MSBuild property values in a condition and evaluates it. """
+    left, right = _resolve_value(cond, env).split('==')
+    return left == right
+
+
+class VSBaseGroup:
+    """ Base class for VS project stuff that has conditional stuff inside.
+
+        For instance, a property group called 'Blah' might have some common
+        (always valid) stuff, but a bunch of other stuff that should only
+        be considered when the solution configuration is Debug, Release, or
+        whatever else. In that case, each 'conditional' (i.e. values for Debug,
+        values for Release, etc.) is listed and tracked separately until
+        we are asked to 'resolve' ourselves based on a given build environment.
+    """
+    def __init__(self, label):
+        self.label = label
+        self.conditionals = {}
+
+    def get_conditional(self, condition):
+        """ Adds a conditional sub-group. """
+        return self.conditionals.get(condition)
+
+    def get_or_create_conditional(self, condition):
+        """ Gets or creates a new conditional sub-group. """
+        c = self.get_conditional(condition)
+        if not c:
+            c = self.__class__(self.label)
+            self.conditionals[condition] = c
+        return c
+
+    def resolve(self, env):
+        """ Resolves this group by evaluating each conditional sub-group
+            based on the given build environment. Returns a 'flattened'
+            version of ourselves.
+        """
+        c = self.__class__(self.label)
+        c._collapse_child(self, env)
+
+        for cond, child in self.conditionals.items():
+            if _evaluate_condition(cond, env):
+                c._collapse_child(child, env)
+
+        return c
+
+
+class VSProjectItem:
+    """ A VS project item, like a source code file. """
+    def __init__(self, include, itemtype=None):
+        self.include = include
+        self.itemtype = itemtype
+        self.metadata = {}
+
+    def resolve(self, env):
+        c = VSProjectItem(_resolve_value(self.include), self.itemtype)
+        c.metadata = {k: _resolve_value(v, env)
+                      for k, v in self.metadata.items()}
+        return c
+
+    def __str__(self):
+        return "(%s)%s" % (self.itemtype, self.include)
+
+
+class VSProjectItemGroup(VSBaseGroup):
+    """ A VS project item group, like a list of source code files,
+        or a list of resources.
+    """
+    def __init__(self, label):
+        super().__init__(label)
+        self.items = []
+
+    def get_source_items(self):
+        for i in self.items:
+            if i.itemtype in ITEM_TYPE_SOURCE_FILES:
+                yield i
+
+    def _collapse_child(self, child, env):
+        self.items += [i.resolve(env) for i in child.items]
+
+
+class VSProjectProperty:
+    """ A VS project property, like an include path or compiler flag. """
+    def __init__(self, name, value):
+        self.name = name
+        self.value = value
+
+    def resolve(self, env):
+        c = VSProjectProperty(self.name, _resolve_value(self.value, env))
+        return c
+
+    def __str__(self):
+        return "%s=%s" % (self.name, self.value)
+
+
+class VSProjectPropertyGroup(VSBaseGroup):
+    """ A VS project property group, such as compiler macros or flags. """
+    def __init__(self, label):
+        super().__init__(label)
+        self.properties = []
+
+    def get(self, propname):
+        try:
+            return self[propname]
+        except IndexError:
+            return None
+
+    def __getitem__(self, propname):
+        for p in self.properties:
+            if p.name == propname:
+                return p.value
+        raise IndexError()
+
+    def _collapse_child(self, child, env):
+        self.properties += [p.resolve(env) for p in child.properties]
+
+
+class VSProject:
+    """ A VS project. """
+    def __init__(self, projtype, name, path, guid):
+        self.type = projtype
+        self.name = name
+        self.path = path
+        self.guid = guid
+        self._itemgroups = None
+        self._propgroups = None
+        self._sln = None
+
+    @property
+    def is_folder(self):
+        """ Returns whether this project is actually just a solution
+            folder, used as a container for other projects.
+        """
+        return self.type == PROJ_TYPE_FOLDER
+
+    @property
+    def abspath(self):
+        abspath = self.path
+        if self._sln and self._sln.path:
+            abspath = os.path.join(self._sln.dirpath, self.path)
+        return abspath
+
+    @property
+    def absdirpath(self):
+        return os.path.dirname(self.abspath)
+
+    @property
+    def itemgroups(self):
+        self._ensure_loaded()
+        return self._itemgroups.values()
+
+    @property
+    def propertygroups(self):
+        self._ensure_loaded()
+        return self._propgroups.values()
+
+    def itemgroup(self, label, resolved_with=None):
+        self._ensure_loaded()
+        ig = self._itemgroups.get(label)
+        if resolved_with is not None and ig is not None:
+            logger.debug("Resolving item group '%s'." % ig.label)
+            ig = ig.resolve(resolved_with)
+        return ig
+
+    def defaultitemgroup(self, resolved_with=None):
+        return self.itemgroup(None, resolved_with=resolved_with)
+
+    def propertygroup(self, label, resolved_with=None):
+        self._ensure_loaded()
+        pg = self._propgroups.get(label)
+        if resolved_with is not None and pg is not None:
+            logger.debug("Resolving property group '%s'." % pg.label)
+            pg = pg.resolve(resolved_with)
+        return pg
+
+    def defaultpropertygroup(self, resolved_with=None):
+        return self.propertygroup(None, resolved_with=resolved_with)
+
+    def get_abs_item_include(self, item):
+        return os.path.abspath(os.path.join(self.absdirpath, item.include))
+
+    def resolve(self, env):
+        self._ensure_loaded()
+
+        propgroups = list(self._propgroups)
+        itemgroups = list(self._itemgroups)
+        self._propgroups[:] = []
+        self._itemgroups[:] = []
+
+        for pg in propgroups:
+            rpg = pg.resolve(env)
+            self._propgroups.append(rpg)
+
+        for ig in itemgroups:
+            rig = ig.resolve(env)
+            self._itemgroups.append(rig)
+
+    def _ensure_loaded(self):
+        if self._itemgroups is None or self._propgroups is None:
+            self._load()
+
+    def _load(self):
+        if not self.path:
+            raise Exception("The current project has no path.")
+        if self.is_folder:
+            logger.debug(f"Skipping folder project {self.name}")
+            self._itemgroups = {}
+            self._propgroups = {}
+            return
+
+        ns = {'ms': 'http://schemas.microsoft.com/developer/msbuild/2003'}
+
+        abspath = self.abspath
+        logger.debug(f"Loading project {self.name} ({self.path}) from: {abspath}")
+        tree = etree.parse(abspath)
+        root = tree.getroot()
+        if _strip_ns(root.tag) != 'Project':
+            raise Exception(f"Expected root node 'Project', got '{root.tag}'")
+
+        self._itemgroups = {}
+        for itemgroupnode in root.iterfind('ms:ItemGroup', ns):
+            label = itemgroupnode.attrib.get('Label')
+            itemgroup = self._itemgroups.get(label)
+            if not itemgroup:
+                itemgroup = VSProjectItemGroup(label)
+                self._itemgroups[label] = itemgroup
+                logger.debug(f"Adding itemgroup '{label}'")
+
+            condition = itemgroupnode.attrib.get('Condition')
+            if condition:
+                itemgroup = itemgroup.get_or_create_conditional(condition)
+
+            for itemnode in itemgroupnode:
+                incval = itemnode.attrib.get('Include')
+                item = VSProjectItem(incval, _strip_ns(itemnode.tag))
+                itemgroup.items.append(item)
+                for metanode in itemnode:
+                    item.metadata[_strip_ns(metanode.tag)] = metanode.text
+
+        self._propgroups = {}
+        for propgroupnode in root.iterfind('ms:PropertyGroup', ns):
+            label = propgroupnode.attrib.get('Label')
+            propgroup = self._propgroups.get(label)
+            if not propgroup:
+                propgroup = VSProjectPropertyGroup(label)
+                self._propgroups[label] = propgroup
+                logger.debug(f"Adding propertygroup '{label}'")
+
+            condition = propgroupnode.attrib.get('Condition')
+            if condition:
+                propgroup = propgroup.get_or_create_conditional(condition)
+
+            for propnode in propgroupnode:
+                propgroup.properties.append(VSProjectProperty(
+                    _strip_ns(propnode.tag),
+                    propnode.text))
+
+
+class MissingVSProjectError(Exception):
+    pass
+
+
+class VSGlobalSectionEntry:
+    """ An entry in a VS solution's global section. """
+    def __init__(self, name, value):
+        self.name = name
+        self.value = value
+
+
+class VSGlobalSection:
+    """ A global section in a VS solution. """
+    def __init__(self, name):
+        self.name = name
+        self.entries = []
+
+
+class VSSolution:
+    """ A VS solution. """
+    def __init__(self, path=None):
+        self.path = path
+        self.projects = []
+        self.sections = []
+
+    @property
+    def dirpath(self):
+        return os.path.dirname(self.path)
+
+    def find_project_by_name(self, name, missing_ok=True):
+        for p in self.projects:
+            if p.name == name:
+                return p
+        if missing_ok:
+            return None
+        return MissingVSProjectError(f"Can't find project with name: {name}")
+
+    def find_project_by_path(self, path, missing_ok=True):
+        for p in self.projects:
+            if p.abspath == path:
+                return p
+        if missing_ok:
+            return None
+        raise MissingVSProjectError(f"Can't find project with path: {path}")
+
+    def find_project_by_guid(self, guid, missing_ok=True):
+        for p in self.projects:
+            if p.guid == guid:
+                return p
+        if missing_ok:
+            return None
+        raise MissingVSProjectError(f"Can't find project for guid: {guid}")
+
+    def globalsection(self, name):
+        for sec in self.sections:
+            if sec.name == name:
+                return sec
+        return None
+
+    def find_project_configuration(self, proj_guid, sln_config):
+        configs = self.globalsection('ProjectConfigurationPlatforms')
+        if not configs:
+            return None
+
+        entry_name = '{%s}.%s.Build.0' % (proj_guid, sln_config)
+        for entry in configs.entries:
+            if entry.name == entry_name:
+                return entry.value
+        return None
+
+
+_re_sln_project_decl_start = re.compile(
+    r'^Project\("\{(?P<type>[A-Z0-9\-]+)\}"\) \= '
+    r'"(?P<name>[^"]+)", "(?P<path>[^"]+)", "\{(?P<guid>[A-Z0-9\-]+)\}"$')
+_re_sln_project_decl_end = re.compile(
+    r'^EndProject$')
+
+_re_sln_global_start = re.compile(r'^Global$')
+_re_sln_global_end = re.compile(r'^EndGlobal$')
+_re_sln_global_section_start = re.compile(
+    r'^\s*GlobalSection\((?P<name>\w+)\) \= (?P<step>\w+)$')
+_re_sln_global_section_end = re.compile(r'^\s*EndGlobalSection$')
+
+
+def parse_sln_file(slnpath):
+    """ Parses a solution file, returns a solution object.
+        The projects are not loaded (they will be lazily loaded upon
+        first access to their items/properties/etc.).
+    """
+    logging.debug(f"Reading {slnpath}")
+    slnobj = VSSolution(slnpath)
+    with open(slnpath, 'r') as fp:
+        lines = fp.readlines()
+        _parse_sln_file_text(slnobj, lines)
+    return slnobj
+
+
+def _parse_sln_file_text(slnobj, lines):
+    until = None
+    in_global = False
+    in_global_section = None
+
+    for i, line in enumerate(lines):
+        if until:
+            # We need to parse something until a given token, so let's
+            # do that and ignore everything else.
+            m = until.search(line)
+            if m:
+                until = None
+            continue
+
+        if in_global:
+            # We're in the 'global' part of the solution. It should contain
+            # a bunch of 'global sections' that we need to parse individually.
+            if in_global_section:
+                # Keep parsing the current section until we reach the end.
+                m = _re_sln_global_section_end.search(line)
+                if m:
+                    in_global_section = None
+                    continue
+
+                ename, evalue = line.strip().split('=')
+                in_global_section.entries.append(VSGlobalSectionEntry(
+                    ename.strip(),
+                    evalue.strip()))
+                continue
+
+            m = _re_sln_global_section_start.search(line)
+            if m:
+                # Found the start of a new section.
+                in_global_section = VSGlobalSection(m.group('name'))
+                logging.debug(f"   Adding global section {in_global_section.name}")
+                slnobj.sections.append(in_global_section)
+                continue
+
+            m = _re_sln_global_end.search(line)
+            if m:
+                # Found the end of the 'global' part.
+                in_global = False
+                continue
+
+        # We're not in a specific part of the solution, so do high-level
+        # parsing. First, ignore root-level comments.
+        if not line or line[0] == '#':
+            continue
+
+        m = _re_sln_project_decl_start.search(line)
+        if m:
+            # Found the start of a project declaration.
+            try:
+                p = VSProject(
+                    m.group('type'), m.group('name'), m.group('path'),
+                    m.group('guid'))
+            except:
+                raise Exception(f"Error line {i}: unexpected project syntax.")
+            logging.debug(f"  Adding project {p.name}")
+            slnobj.projects.append(p)
+            p._sln = slnobj
+
+            until = _re_sln_project_decl_end
+            continue
+
+        m = _re_sln_global_start.search(line)
+        if m:
+            # Reached the start of the 'global' part, where global sections
+            # are defined.
+            in_global = True
+            continue
+
+        # Ignore the rest (like visual studio version flags).
+        continue
+
+
+class SolutionCache:
+    """ A class that contains a VS solution object, along with pre-indexed
+        lists of items. It's meant to be saved on disk.
+    """
+    VERSION = 3
+
+    def __init__(self, slnobj):
+        self.slnobj = slnobj
+        self.index = None
+
+    def build_cache(self):
+        self.index = {}
+        for proj in self.slnobj.projects:
+            if proj.is_folder:
+                continue
+            itemgroup = proj.defaultitemgroup()
+            if not itemgroup:
+                continue
+
+            item_cache = set()
+            self.index[proj.abspath] = item_cache
+
+            for item in itemgroup.get_source_items():
+                item_path = proj.get_abs_item_include(item).lower()
+                item_cache.add(item_path)
+
+    def save(self, path):
+        pathdir = os.path.dirname(path)
+        if not os.path.exists(pathdir):
+            os.makedirs(pathdir)
+        with open(path, 'wb') as fp:
+            pickle.dump(self, fp)
+
+    @staticmethod
+    def load_or_rebuild(slnpath, cachepath):
+        if cachepath:
+            res = _try_load_from_cache(slnpath, cachepath)
+            if res is not None:
+                return res
+
+        slnobj = parse_sln_file(slnpath)
+        cache = SolutionCache(slnobj)
+
+        if cachepath:
+            logger.debug(f"Regenerating cache: {cachepath}")
+            cache.build_cache()
+            cache.save(cachepath)
+
+        return (cache, False)
+
+
+def _try_load_from_cache(slnpath, cachepath):
+    try:
+        sln_dt = os.path.getmtime(slnpath)
+        cache_dt = os.path.getmtime(cachepath)
+    except OSError:
+        logger.debug("Can't read solution or cache files.")
+        return None
+
+    # If the solution file is newer, bail out.
+    if sln_dt >= cache_dt:
+        logger.debug("Solution is newer than cache.")
+        return None
+
+    # Our cache is at least valid for the solution stuff. Some of our
+    # projects might be out of date, but at least there can't be any
+    # added or removed projects from the solution (otherwise the solution
+    # file would have been touched). Let's load the cache.
+    with open(cachepath, 'rb') as fp:
+        cache = pickle.load(fp)
+
+    # Check that the cache version is up-to-date with this code.
+    loaded_ver = getattr(cache, 'VERSION')
+    if loaded_ver != SolutionCache.VERSION:
+        logger.debug(f"Cache was saved with older format: {cachepath}")
+        return None
+
+    slnobj = cache.slnobj
+
+    # Check that none of the project files in the solution are newer
+    # than this cache.
+    proj_dts = []
+    for p in slnobj.projects:
+        if not p.is_folder:
+            try:
+                proj_dts.append(os.path.getmtime(p.abspath))
+            except OSError:
+                logger.debug(f"Found missing project: {p.abspath}")
+                return None
+
+    if all([cache_dt > pdt for pdt in proj_dts]):
+        logger.debug(f"Cache is up to date: {cachepath}")
+        return (cache, True)
+
+    logger.debug("Cache has outdated projects.")
+    return None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/ycm_extra_conf.py	Tue Sep 17 13:24:24 2019 -0700
@@ -0,0 +1,215 @@
+import argparse
+import logging
+import os.path
+import sys
+
+
+if True: # 'vim' in sys.modules:
+    sys.path.append(os.path.dirname(__file__))
+
+
+from logutil import setup_logging
+from vsutil import SolutionCache
+
+
+logger = logging.getLogger(__name__)
+
+
+def _build_cflags(filename, solution, buildenv=None, slncache=None):
+    # Load the solution.
+    if not solution:
+        raise Exception(
+            "No solution path was provided in the client data!")
+
+    cache, loaded = SolutionCache.load_or_rebuild(solution, slncache)
+    if not loaded:
+        cache.build_cache()
+
+    # Find the current file in the solution.
+    filename_lower = filename.lower()
+    projpath = None
+    for pp, pi in cache.index.items():
+        if filename_lower in pi:
+            projpath = pp
+            break
+    else:
+        raise Exception("File doesn't belong to the solution: %s" % filename)
+
+    # Find the project that our file belongs to.
+    proj = cache.slnobj.find_project_by_path(projpath)
+    if not proj:
+        raise Exception("Can't find project in solution: %s" % projpath)
+    logger.debug("Found project %s: %s" % (proj.name, proj.abspath))
+
+    # Get the provided config/platform combo, which represent a solution
+    # configuration, and find the corresponding project configuration.
+    # For instance, a solution configuration of "Debug|Win64" could map
+    # to a "MyDebug|AnyCPU" configuration on a specific project.
+    sln_config_platform = '%s|%s' % (buildenv['Configuration'],
+                                     buildenv['Platform'])
+    proj_config_platform = cache.slnobj.find_project_configuration(
+        proj.guid, sln_config_platform)
+    if not proj_config_platform:
+        raise Exception("Can't find project configuration and platform for "
+                        "solution configuration and platform: %s" %
+                        sln_config_platform)
+
+    # Make a build environment for the project, and figure out what
+    # kind of project it is.
+    proj_config, proj_platform = proj_config_platform.split('|')
+
+    proj_buildenv = buildenv.copy()
+    proj_buildenv['Configuration'] = proj_config
+    proj_buildenv['Platform'] = proj_platform
+
+    cfggroup = proj.propertygroup('Configuration', proj_buildenv)
+    cfgtype = cfggroup.get('ConfigurationType')
+    if not cfgtype:
+        raise Exception("Can't find configuration type. Did you specify a "
+                        "configuration name and platform? Got: %s" %
+                        proj_buildenv)
+    logger.debug("Found configuration type: %s" % cfgtype)
+
+    # Let's prepare a list of standard stuff for C++.
+    preproc = []
+    incpaths = []
+    projdir = os.path.dirname(proj.abspath)
+
+    if cfgtype == 'Makefile':
+        # It's a 'Makefile' project, which means we know as little about
+        # compiler flags as whatever information was given to VS. As
+        # such, if the solution setup doesn't give enough info, VS
+        # intellisense won't work, and neither will YouCompleteMe.
+        defaultpropgroup = proj.defaultpropertygroup(proj_buildenv)
+
+        nmake_preproc = defaultpropgroup.get('NMakePreprocessorDefinitions')
+        preproc += nmake_preproc.strip(';').split(';')
+
+        nmake_incpaths = defaultpropgroup.get('NMakeIncludeSearchPath')
+        incpaths += [os.path.abspath(os.path.join(projdir, p))
+                     for p in nmake_incpaths.strip(';').split(';')]
+
+    else:
+        # We should definitely support standard VC++ projects here but
+        # I don't need it yet :)
+        raise Exception("Don't know how to handle configuration type: %s" %
+                        cfgtype)
+
+    # Build the clang/YCM flags with what we found.
+    flags = ['-x', 'c++']  # TODO: check language type from project file.
+
+    for symbol in preproc:
+        flags.append('-D%s' % symbol)
+    for path in incpaths:
+        if path.startswith("C:\\Program Files"):
+            flags.append('-isystem')
+        else:
+            flags.append('-I')
+        flags.append(path)
+
+    return {'flags': flags}
+
+
+def _build_env_from_vim(client_data):
+    buildenv = {}
+    buildenv['Configuration'] = client_data.get('g:vimcrosoft_current_config', '')
+    buildenv['Platform'] = client_data.get('g:vimcrosoft_current_platform', '')
+    return buildenv
+
+
+def Settings(**kwargs):
+    language = kwargs.get('language')
+    filename = kwargs.get('filename')
+
+    client_data = kwargs.get('client_data', {})
+    from_cli = kwargs.get('from_cli', False)
+    if from_cli:
+        solution = client_data.get('solution')
+        slncache = client_data.get('slncache')
+        buildenv = client_data.get('env', {})
+    else:
+        solution = client_data.get('g:vimcrosoft_current_sln')
+        slncache = client_data.get('g:vimcrosoft_current_sln_cache')
+        buildenv = _build_env_from_vim(client_data)
+
+    flags = None
+
+    if language == 'cfamily':
+        try:
+            flags = _build_cflags(filename, solution,
+                                  buildenv=buildenv, slncache=slncache)
+        except Exception as exc:
+            if from_cli:
+                raise
+            flags = {'error': str(exc)}
+    else:
+        flags = {'error': f"Unknown language: {language}"}
+
+    with open("D:\\P4\\DevEditor\\debug.txt", 'w') as fp:
+        fp.write("kwargs:")
+        fp.write(str(kwargs))
+        fp.write("client_data:")
+        fp.write(str(list(kwargs['client_data'].items())))
+        fp.write("flags:")
+        fp.write(str(flags))
+    return flags
+
+
+languages = {
+    'cfamily': ['h', 'c', 'hpp', 'cpp', 'inl']
+}
+
+
+def _get_language(filename):
+    _, ext = os.path.splitext(filename)
+    ext = ext.lstrip('.')
+    for lang, exts in languages.items():
+        if ext in exts:
+            return lang
+    return None
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument('solution',
+                        help="The solution file")
+    parser.add_argument('filename',
+                        help="The filename for which to get flags")
+    parser.add_argument('-p', '--property',
+                        action="append",
+                        help="Specifies a build property")
+    parser.add_argument('-c', '--cache',
+                        help="The solution cache to use")
+    parser.add_argument('-v', '--verbose',
+                        action='store_true',
+                        help="Show debugging information")
+    args = parser.parse_args()
+    setup_logging(args.verbose)
+
+    lang = _get_language(args.filename)
+    logger.debug(f"Got language {lang} for {args.filename}")
+
+    build_env = {}
+    if args.property:
+        for p in args.property:
+            pname, pval = p.split('=', 1)
+            build_env[pname] = pval
+    logger.debug(f"Got build environment: {build_env}")
+    client_data = {'solution': args.solution,
+                   'slncache': args.cache,
+                   'env': build_env}
+
+    params = {'from_cli': True,
+              'language': lang,
+              'filename': args.filename,
+              'client_data': client_data
+              }
+    flags = Settings(**params)
+    logger.info("Flags:")
+    import pprint
+    pp = pprint.PrettyPrinter(indent=2)
+    pp.pprint(flags)
+
+
+if __name__ == '__main__':
+    main()