diff autoload/unreal.vim @ 2:9235d8341a18

Refactor the build system invocation commands. Now we have proper knowledge of the projects inside a codebase ("branch"). The plugin should correctly parse configuration names, find the correct module to build based on the configuration, and so on. Also, added support for generating the clang compilation database.
author Ludovic Chabant <ludovic@chabant.com>
date Fri, 22 Jan 2021 16:38:18 -0800
parents 43d0e448edce
children 613f13dc42f7
line wrap: on
line diff
--- a/autoload/unreal.vim	Fri Sep 25 09:44:49 2020 -0700
+++ b/autoload/unreal.vim	Fri Jan 22 16:38:18 2021 -0800
@@ -2,6 +2,8 @@
 
 " Utilities {{{
 
+let s:basedir = expand('<sfile>:p:h:h')
+
 function! unreal#throw(message)
     throw "unreal: ".a:message
 endfunction
@@ -55,27 +57,57 @@
 
 " }}}
 
-" Project Management {{{
+" {{{ Scripts and Cache Files
+
+let s:scriptsdir = s:basedir.'\scripts'
+
+function! unreal#get_vim_script_path(scriptname) abort
+    return s:scriptsdir.s:dirsep.a:scriptname.s:scriptext
+endfunction
 
-function! unreal#find_project_dir() abort
-    if !empty(g:unreal_project_dir_finder)
-        return call(g:unreal_project_dir_finder)
+function! unreal#get_cache_path(name, ...) abort
+    if empty(g:unreal_branch_dir)
+        call unreal#throw("No UE branch defined")
+    endif
+    let l:cache_dir = g:unreal_branch_dir.s:dirsep.".vimunreal"
+    let l:path = l:cache_dir.s:dirsep.a:name
+    if a:0 && a:1 && !isdirectory(l:cache_dir)
+        call mkdir(l:cache_dir)
+    endif
+    return l:path
+endfunction
+
+" }}}
+
+" Branch and Project Management {{{
+
+function! unreal#find_branch_dir_and_project() abort
+    call unreal#find_branch_dir()
+
+    if !empty(g:unreal_branch_dir)
+        call unreal#find_project()
+    endif
+endfunction
+
+function! unreal#find_branch_dir() abort
+    if !empty(g:unreal_branch_dir_finder)
+        let l:branch_dir = call(g:unreal_branch_dir_finder)
+    else
+        let l:branch_dir = unreal#default_branch_dir_finder(getcwd())
     endif
 
-    let l:path = getcwd()
-    try
-        let l:proj_dir = unreal#default_project_dir_finder(l:path)
-    catch /^unreal:/
-        let l:proj_dir = ''
-    endtry
-    call unreal#set_project_dir(l:proj_dir)
+    if !empty(l:branch_dir)
+        call unreal#set_branch_dir(l:branch_dir, 1)  " Set branch silently.
+    else
+        call unreal#throw("No UE branch found!")
+    endif
 endfunction
 
-function! unreal#default_project_dir_finder(path) abort
+function! unreal#default_branch_dir_finder(path) abort
     let l:cur = a:path
     let l:prev = ""
     while l:cur != l:prev
-        let l:markers = globpath(l:cur, g:unreal_project_dir_marker, 0, 1)
+        let l:markers = globpath(l:cur, g:unreal_branch_dir_marker, 0, 1)
         if !empty(l:markers)
             call unreal#trace("Found marker file: ".l:markers[0])
             return l:cur
@@ -83,18 +115,30 @@
         let l:prev = l:cur
         let l:cur = fnamemodify(l:cur, ':h')
     endwhile
-    call unreal#throw("No UE project markers found.")
+    return ""
 endfunction
 
-function! unreal#set_project_dir(project_dir, ...) abort
+function! unreal#set_branch_dir(branch_dir, ...) abort
     " Strip any end slashes on the directory path.
-    let g:unreal_project_dir = fnamemodify(a:project_dir, ':s?[/\\]$??')
+    let l:prev_dir = g:unreal_branch_dir
+    let g:unreal_branch_dir = fnamemodify(a:branch_dir, ':s?[/\\]$??')
+    let l:branch_was_set = !empty(g:unreal_branch_dir)
+
+    " Update our projects infos.
+    let g:unreal_branch_projects = unreal#get_branch_projects(g:unreal_branch_dir)
 
-    let l:proj_was_set = !empty(g:unreal_project_dir)
+    " Notify our modules.
+    if l:branch_was_set
+        call unreal#call_modules('on_branch_changed', g:unreal_branch_dir)
+    else
+        call unreal#call_modules('on_branch_cleared')
+    endif
 
+    " Auto-set the Vimcrosoft solution if that plugin is installed.
+    " TODO: move this into a module.
     if exists(":VimcrosoftSetSln")
-        if l:proj_was_set
-            let l:sln_files = glob(g:unreal_project_dir.s:dirsep."*.sln", 0, 1)
+        if l:branch_was_set
+            let l:sln_files = glob(g:unreal_branch_dir.s:dirsep."*.sln", 0, 1)
             if !empty(l:sln_files)
                 " Vimcrosoft might have auto-found the same solution, already, 
                 " in which case we don't have to set it.
@@ -109,20 +153,121 @@
         endif
     endif
 
-    if l:proj_was_set
-        call unreal#call_modules('on_project_changed', g:unreal_project_dir)
-    else
-        call unreal#call_modules('on_project_cleared')
+    let l:silent = a:0 && a:1
+    if !l:silent
+        if l:branch_was_set
+            echom "UE branch set to: ".g:unreal_branch_dir
+        else
+            echom "UE branch cleared"
+        endif
+    endif
+endfunction
+
+function! unreal#find_project() abort
+    if empty(g:unreal_branch_dir)
+        call unreal#throw("No UE branch set!")
+    endif
+
+    if len(g:unreal_branch_projects) == 0
+        call unreal#throw("No UE projects found in branch: ".g:unreal_branch_dir)
+    endif
+
+    let l:proj = ""
+    let l:cached_proj_file = unreal#get_cache_path("LastProject.txt")
+    try
+        let l:cached_proj = readfile(l:cached_proj_file, '', 1)
+    catch
+        let l:cached_proj = []
+    endtry
+    if len(l:cached_proj) > 0 && !empty(l:cached_proj[0])
+        if has_key(g:unreal_branch_projects, l:cached_proj[0])
+            let l:proj = l:cached_proj[0]
+            call unreal#trace("Found previously set project: ".l:proj)
+        endif
+    endif
+
+    if l:proj == ""
+        let l:projnames = sort(keys(g:unreal_branch_projects))
+        if len(l:projnames) > 0
+            let l:proj = l:projnames[0]
+            call unreal#trace("Picking first project in branch: ".l:proj)
+        endif
     endif
 
-    let l:silent = a:0 && a:1
-    if !l:silent
-        if l:proj_was_set
-            echom "UE Project set to: ".g:unreal_project_dir
+    if l:proj == ""
+        call unreal#throw("No UE projects found in branch: ".g:unreal_branch_dir)
+    else
+        call unreal#set_project(l:proj)
+    endif
+endfunction
+
+function! unreal#set_project(projname) abort
+    let g:unreal_project = a:projname
+
+    let l:cached_proj_file = unreal#get_cache_path("LastProject.txt", 1) " Auto-create cache dir.
+    call writefile([a:projname], l:cached_proj_file)
+
+    call unreal#trace("Set UE project: ".a:projname)
+endfunction
+
+function! unreal#get_branch_projects(branch_dir)
+    if empty(a:branch_dir)
+        return {}
+    endif
+
+    " Reset the known projects.
+    let l:projs = {}
+    call unreal#trace("Finding projects in branch: ".a:branch_dir)
+    
+    " Find project files in the branch directory.
+    let l:dirs = readdir(a:branch_dir)
+    for l:dir in l:dirs
+        let l:dirpath = a:branch_dir.s:dirsep.l:dir.s:dirsep
+        let l:uprojfiles = glob(l:dirpath."*.uproject", 0, 1)
+        if len(l:uprojfiles) > 0
+            let l:lines = readfile(l:uprojfiles[0])
+            let l:jsonraw = join(l:lines, "\n")
+            let l:json = json_decode(l:jsonraw)
+            let l:json["Path"] = l:uprojfiles[0]
+            let l:projname = fnamemodify(l:uprojfiles[0], ':t:r')
+            let l:projs[l:projname] = l:json
+            call unreal#trace("Found project: ".l:projname)
+        endif
+    endfor
+
+    return l:projs
+endfunction
+
+function! unreal#get_project_info(proppath) abort
+    if empty(g:unreal_project) || empty(g:unreal_branch_projects)
+        call unreal#throw("No project(s) set!")
+    endif
+
+    let l:proj = g:unreal_branch_projects[g:unreal_project]
+
+    let l:cur = l:proj
+    let l:propnames = split(a:proppath, '.')
+    for l:propname in l:propnames
+        if type(l:cur) == type([])
+            let l:cur = l:cur[str2nr(l:propname)]
         else
-            echom "UE Project cleared"
+            let l:cur = l:cur[l:propname]
         endif
+    endfor
+endfunction
+
+function! unreal#find_project_module_of_type(project, module_type) abort
+    if empty(a:project) || empty(g:unreal_branch_projects)
+        call unreal#throw("No project(s) set!")
     endif
+
+    let l:proj = g:unreal_branch_projects[a:project]
+    for l:module in l:proj["Modules"]
+        if get(l:module, "Type", "") == a:module_type
+            return copy(l:module)
+        endif
+    endfor
+    return {}
 endfunction
 
 let s:extra_args_version = 1
@@ -160,10 +305,46 @@
 
 " }}}
 
-" Commands {{{
+" Configuration and Platform {{{
+
+let s:unreal_configs = []
+
+function! s:cache_unreal_configs() abort
+    if len(s:unreal_configs) == 0
+        for l:state in g:unreal_config_states
+            for l:target in g:unreal_config_targets
+                call add(s:unreal_configs, l:state.l:target)
+            endfor
+        endfor
+    endif
+endfunction
+
+function! s:parse_config_state_and_target(config) abort
+    let l:alen = len(a:config)
 
-function! unreal#generate_project_files() abort
-    call unreal#run_make("ugenprojfiles")
+    let l:config_target = ""
+    for l:target in g:unreal_config_targets
+        let l:tlen = len(l:target)
+        if l:alen > l:tlen && a:config[l:alen - l:tlen : ] == l:target
+            let l:config_target = l:target
+            break
+        endif
+    endfor
+
+    let l:config_state = a:config[0 : l:alen - t:tlen - 1]
+    
+    if index(g:unreal_config_states, l:config_state) >= 0 ||
+                \index(g:unreal_config_targets, l:config_target) >= 0
+        return [l:config_state, l:config_target]
+    else
+        call unreal#throw("Invalid config state or target: ".l:config_state.l:config_target)
+    endif
+endfunction
+
+function! unreal#set_config(config) abort
+    let [l:config_state, l:config_target] = s:parse_config_state_and_target(a:config)
+    let g:unreal_config_state = l:config_state
+    let g:unreal_config_target = l:config_target
 endfunction
 
 function! unreal#set_platform(platform) abort
@@ -173,15 +354,141 @@
     let g:unreal_project_platform = a:platform
 endfunction
 
-function! unreal#build(...) abort
-    let l:opts = copy(g:unreal_auto_build_options)
-    if a:0
-        let l:opts = a:000 + l:opts
+" }}}
+
+" Build {{{
+
+function! unreal#get_ubt_args(...) abort
+    " Start with modules we should always build.
+    let l:mod_names = keys(g:unreal_auto_build_modules)
+    let l:mod_args = copy(g:unreal_auto_build_modules)
+
+    " Function arguments are: 
+    " <Project> <Platform> <Config> [<...MainModuleOptions>] [<...GlobalOptions>] <?NoGlobalModules>
+    let l:project = g:unreal_project
+    if a:0 >= 1 && !empty(a:1)
+        let l:project = a:1
+    endif
+
+    let l:platform = g:unreal_platform
+    if a:0 >= 2 && !empty(a:2)
+        let l:platform = a:2
+    endif
+
+    let [l:config_state, l:config_target] = [g:unreal_config_state, g:unreal_config_target]
+    if a:0 >= 3 && !empty(a:3)
+        let [l:config_state, l:config_target] = s:parse_config_state_and_target(a:3)
+    endif
+
+    let l:mod_opts = []
+    if a:0 >= 4
+        if type(a:4) == type([])
+            let l:mod_opts = a:4
+        else
+            let l:mod_opts = [a:4]
+        endif
+    endif
+
+    let l:global_opts = copy(g:unreal_auto_build_options)
+    if a:0 >= 5
+        if type(a:5) == type([])
+            call extend(l:global_opts, a:5)
+        else
+            call extend(l:global_opts, [a:5])
+        endif
+    endif
+
+    if a:0 >= 6 && a:6
+        let l:mod_names = []
+    endif
+
+    " Find the appropriate module for our project.
+    if l:config_target == "Editor"
+        let l:module = unreal#find_project_module_of_type(l:project, "Editor")
+    else
+        let l:module = unreal#find_project_module_of_type(l:project, "Runtime")
+    endif
+    if empty(l:module)
+        call unreal#throw("Can't find module for target '".l:config_target."' in project: ".l:project)
     endif
-    let g:unreal_temp_makeprg_args__ = l:opts
+
+    " Add the module's arguments to the list.
+    call insert(l:mod_names, l:module["Name"], 0)
+    let l:mod_args[l:module["Name"]] = l:mod_opts
+
+    " Build the argument list for our modules.
+    let l:ubt_cmdline = []
+    for l:mod_name in l:mod_names
+        let l:mod_cmdline = '-Target="'.
+                    \l:mod_name.' '.
+                    \l:platform.' '.
+                    \l:config_state
+        let l:mod_arg = l:mod_args[l:mod_name]
+        if !empty(l:mod_arg)
+            let l:mod_cmdline .= ' '.join(l:mod_arg, ' ')
+        endif
+        let l:mod_cmdline .= '"'
+
+        call add(l:ubt_cmdline, l:mod_cmdline)
+    endfor
+
+    " Add any global options.
+    call extend(l:ubt_cmdline, l:global_opts)
+
+    return l:ubt_cmdline
+endfunction
+
+function! unreal#build(bang, ...) abort
+    let g:__unreal_makeprg_script = "Build"
+    let g:__unreal_makeprg_args = call('unreal#get_ubt_args', a:000)
+    call unreal#run_make("ubuild", bang)
+endfunction
+
+function! unreal#rebuild(...) abort
+    let g:__unreal_makeprg_script = "Rebuild"
+    let g:__unreal_makeprg_args = call('unreal#get_ubt_args', a:000)
     call unreal#run_make("ubuild")
 endfunction
 
+function! unreal#clean(...) abort
+    let g:__unreal_makeprg_script = "Clean"
+    let g:__unreal_makeprg_args = call('unreal#get_ubt_args', a:000)
+    call unreal#run_make("ubuild")
+endfunction
+
+function! unreal#generate_compilation_database() abort
+    let g:__unreal_makeprg_script = "Build"
+    let g:__unreal_makeprg_args = unreal#get_ubt_args('', '', '', [], ['-allmodules', '-Mode=GenerateClangDatabase'], 1)
+    call unreal#run_make("ubuild")
+endfunction
+
+function! unreal#generate_project_files() abort
+    if !g:unreal_auto_generate_compilation_database
+        call unreal#run_make("ugenprojfiles")
+    else
+        " Generate a response file that will run both the project generation
+        " and the compilation database generation one after the other. Then we
+        " pass that to our little script wrapper.
+        let l:genscriptpath = shellescape(
+                    \unreal#get_script_path("Engine/Build/BatchFiles/GenerateProjectFiles"))
+        let l:buildscriptpath = shellescape(
+                    \unreal#get_script_path("Engine/Build/BatchFiles/Build"))
+        let l:buildscriptargs = 
+                    \unreal#get_ubt_args('', '', '', [], ['-allmodules', '-Mode=GenerateClangDatabase'], 1)
+
+        let l:rsplines = [
+                    \l:genscriptpath,
+                    \l:buildscriptpath.' '.join(l:buildscriptargs, ' ')
+                    \]
+        let l:rsppath = tempname()
+        call unreal#trace("Writing response file: ".l:rsppath)
+        call writefile(l:rsplines, l:rsppath)
+
+        let g:__unreal_makeprg_args = l:rsppath
+        call unreal#run_make("uscriptwrapper")
+    endif
+endfunction
+
 " }}}
 
 " Completion Functions {{{
@@ -202,23 +509,29 @@
     return s:add_unique_suggestion_trailing_space(l:suggestions)
 endfunction
 
+function! unreal#complete_projects(ArgLead, CmdLine, CursorPos)
+    return s:filter_suggestions(a:ArgLead, keys(g:unreal_branch_projects))
+endfunction
+
 function! unreal#complete_platforms(ArgLead, CmdLine, CursorPos)
     return s:filter_suggestions(a:ArgLead, copy(g:unreal_platforms))
 endfunction
 
 function! unreal#complete_configs(ArgLead, CmdLine, CursorPos)
-    return s:filter_suggestions(a:ArgLead, copy(g:unreal_configurations))
+    call s:cache_unreal_configs()
+    return s:filter_suggestions(a:ArgLead, copy(s:unreal_configs))
 endfunction
 
-function! unreal#complete_build_targets(ArgLead, CmdLine, CursorPos)
+function! unreal#complete_build_args(ArgLead, CmdLine, CursorPos)
     let l:bits = split(a:CmdLine.'_', ' ')
-    let l:bits = l:bits[1:]  " Remove the `UnrealBuild` command from the line
+    let l:bits = l:bits[1:]  " Remove the `UnrealBuild` command from the line.
     if len(l:bits) <= 1
-        let l:suggestions = vimcrosoft#get_sln_project_names()
+        let l:suggestions = keys(g:unreal_branch_projects)
     elseif len(l:bits) == 2
         let l:suggestions = copy(g:unreal_platforms)
     elseif len(l:bits) == 3
-        let l:suggestions = copy(g:unreal_configurations)
+        call s:cache_unreal_configs()
+        let l:suggestions = s:unreal_configs
     elseif len(l:bits) >= 4
         let l:suggestions = copy(g:unreal_build_options)
     endif
@@ -229,12 +542,26 @@
 
 " Build System {{{
 
-function! unreal#run_make(compilername) abort
+function! unreal#run_make(compilername, ...) abort
+    let l:bang = 0
+    if a:0 && a:1
+        let l:bang = 1
+    endif
+
     execute "compiler ".a:compilername
+
     if exists(':Make')  " Support for vim-dispatch
-        Make
+        if l:bang
+            Make!
+        else
+            Make
+        endif
     else
-        make
+        if l:bang
+            make!
+        else
+            make
+        endif
     endif
 endfunction
 
@@ -245,7 +572,12 @@
 let s:builds_in_progress = []
 
 function! unreal#get_script_path(scriptname, ...) abort
-    return g:unreal_project_dir.s:dirsep.a:scriptname.s:scriptext
+    if s:iswin
+        let l:name = substitute(a:scriptname, '/', "\\", 'g')
+    else
+        let l:name = a:scriptname
+    endif
+    return g:unreal_branch_dir.s:dirsep.l:name.s:scriptext
 endfunction
 
 " }}}
@@ -254,7 +586,7 @@
 
 function! unreal#init() abort
     if g:unreal_auto_find_project
-        call unreal#find_project_dir()
+        call unreal#find_branch_dir_and_project()
     endif
 endfunction
 
@@ -263,12 +595,13 @@
 " Statusline Functions {{{
 
 function! unreal#statusline(...) abort
-    if empty(g:unreal_project_dir)
+    if empty(g:unreal_branch_dir)
         return ''
     endif
-
-    let l:line = 'UE:'.g:unreal_project_dir
-    return l:line
+    if empty(g:unreal_project)
+        return 'UE:'.g:unreal_branch_dir.':<no project>'
+    endif
+    return 'UE:'.g:unreal_branch_dir.':'.g:unreal_project.'('.g:unreal_config_state.g:unreal_config_target.'|'.g:unreal_platform.')'
 endfunction
 
 " }}}