view 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 source

" unreal.vim - Work with the Unreal Engine in Vim

" Utilities {{{

let s:basedir = expand('<sfile>:p:h:h')

function! unreal#throw(message)
    throw "unreal: ".a:message
endfunction

function! unreal#error(message)
    let v:errmsg = "unreal: ".a:message
    echoerr v:errmsg
endfunction

function! unreal#warning(message)
    echohl WarningMsg
    echom "unreal: ".a:message
    echohl None
endfunction

function! unreal#info(message)
    echom "unreal: ".a:message
endfunction

function! unreal#trace(message)
    if g:unreal_trace
        echom "unreal: ".a:message
    endif
endfunction

if has('win32') || has('win64')
    let s:iswin = 1
    let s:dirsep = "\\"
    let s:scriptext = ".bat"
else
    let s:iswin = 0
    let s:dirsep = "/"
    let s:scriptext = ".sh"
endif

" }}}

" Modules {{{

function! unreal#call_modules(funcname, ...) abort
    for module in g:unreal_modules
        let l:fullfuncname = module.'#'.a:funcname
        if exists('*'.l:fullfuncname)
            call unreal#trace("Calling module function: ".l:fullfuncname)
            call call(l:fullfuncname, a:000)
        else
            call unreal#trace("Skipping ".l:fullfuncname.": doesn't exist.")
        endif
    endfor
endfunction

" }}}

" {{{ 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#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

    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_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_branch_dir_marker, 0, 1)
        if !empty(l:markers)
            call unreal#trace("Found marker file: ".l:markers[0])
            return l:cur
        endif
        let l:prev = l:cur
        let l:cur = fnamemodify(l:cur, ':h')
    endwhile
    return ""
endfunction

function! unreal#set_branch_dir(branch_dir, ...) abort
    " Strip any end slashes on the directory path.
    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)

    " 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: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.
                if g:vimcrosoft_current_sln != l:sln_files[0]
                    execute "VimcrosoftSetSln ".fnameescape(l:sln_files[0])
                endif
                " Make sure we have our extra compiler args ready.
                call unreal#generate_vimcrosoft_extra_args(l:sln_files[0])
            endif
        else
            execute "VimcrosoftUnsetSln"
        endif
    endif

    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

    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
            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

function! unreal#generate_vimcrosoft_extra_args(solution) abort
    let l:argfile = 
                \fnamemodify(a:solution, ':p:h').s:dirsep.
                \'.vimcrosoft'.s:dirsep.
                \fnamemodify(a:solution, ':t').'.flags'

    let l:do_regen = 0
    let l:version_line = "# version ".string(s:extra_args_version)
    try
        call unreal#trace("Checking for extra clang args file: ".l:argfile)
        let l:lines = readfile(l:argfile)
        if len(l:lines) < 1
            call unreal#trace("Extra clang args file is empty... regenerating")
            let l:do_regen = 1
        elseif trim(l:lines[0]) != l:version_line
            call unreal#trace("Extra clang args file is outdated... regenerating")
            let l:do_regen = 1
        endif
    catch
        call unreal#trace("Extra clang args file doesn't exist... regenerating")
        let l:do_regen = 1
    endtry
    if l:do_regen
        let l:arglines = [
                    \l:version_line,
                    \"-DUNREAL_CODE_ANALYZER"
                    \]
        call writefile(l:arglines, l:argfile)
    endif
endfunction

" }}}

" 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)

    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
    if index(g:unreal_platforms, a:platform) < 0
        call unreal#throw("Invalid Unreal platform: ".a:platform)
    endif
    let g:unreal_project_platform = a:platform
endfunction

" }}}

" 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

    " 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 {{{

function! s:add_unique_suggestion_trailing_space(suggestions)
    " If there's only one answer, add a space so we can start typing the
    " next argument right away.
    if len(a:suggestions) == 1
        let a:suggestions[0] = a:suggestions[0] . ' '
    endif
    return a:suggestions
endfunction

function! s:filter_suggestions(arglead, suggestions)
    let l:argpat = tolower(a:arglead)
    let l:suggestions = filter(a:suggestions,
                \{idx, val -> val =~? l:argpat})
    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)
    call s:cache_unreal_configs()
    return s:filter_suggestions(a:ArgLead, copy(s:unreal_configs))
endfunction

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.
    if len(l:bits) <= 1
        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
        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
    return s:filter_suggestions(a:ArgLead, l:suggestions)
endfunction

" }}}

" Build System {{{

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
        if l:bang
            Make!
        else
            Make
        endif
    else
        if l:bang
            make!
        else
            make
        endif
    endif
endfunction

" }}}

" Unreal Scripts {{{

let s:builds_in_progress = []

function! unreal#get_script_path(scriptname, ...) abort
    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

" }}}

" Initialization {{{

function! unreal#init() abort
    if g:unreal_auto_find_project
        call unreal#find_branch_dir_and_project()
    endif
endfunction

" }}}

" Statusline Functions {{{

function! unreal#statusline(...) abort
    if empty(g:unreal_branch_dir)
        return ''
    endif
    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

" }}}