view autoload/unreal.vim @ 9:b5040cfea052

Tweak how configs and targets are parsed and handled
author Ludovic Chabant <ludovic@chabant.com>
date Tue, 29 Aug 2023 13:06:44 -0700
parents 5cd58b3fd93d
children 06af9916ba7c
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(silent) abort
    call unreal#find_branch_dir(a:silent)

    if !empty(g:unreal_branch_dir)
        call unreal#find_project()
    endif
endfunction

function! unreal#find_branch_dir(silent) 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
        if a:silent
            call unreal#trace("No UE branch found")
        else
            call unreal#throw("No UE branch found!")
        endif
    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 l:clean_projname = trim(a:projname)
    if !has_key(g:unreal_branch_projects, l:clean_projname)
        call unreal#throw("No project '".l:clean_projname."' in the current branch. Branch projects are: ".string(keys(g:unreal_branch_projects)))
    endif

    let g:unreal_project = l:clean_projname

    let l:cached_proj_file = unreal#get_cache_path("LastProject.txt", 1) " Auto-create cache dir.
    call writefile([l:clean_projname], l:cached_proj_file)

    call unreal#trace("Set UE project: ".l:clean_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:argdir = 
                \fnamemodify(a:solution, ':p:h').s:dirsep.'.vimcrosoft'
    let l:argfile = 
                \l:argdir.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
        if !isdirectory(l:argdir)
            call mkdir(l:argdir)
        endif

        let l:arglines = [
                    \l:version_line,
                    \"-DUNREAL_CODE_ANALYZER"
                    \]
        call writefile(l:arglines, l:argfile)
    endif
endfunction

" }}}

" Configuration and Platform {{{

let s:unreal_configs = []
let s:unreal_configs_map = {}

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
                let l:key = l:state.l:target
                call add(s:unreal_configs, l:key)
                let s:unreal_configs_map[l:key] = [l:state, l:target]
            endfor
        endfor
    endif
endfunction

function! s:parse_config_state_and_target(config) abort
    let l:config = trim(a:config)

    for l:key in keys(s:unreal_configs_map)
        if l:config == l:key
            let [l:config_state, l:config_target] = s:unreal_configs_map[l:key]
            break
        endif
    endfor

    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, trim(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", a:bang)
endfunction

function! unreal#rebuild(bang, ...) abort
    let g:__unreal_makeprg_script = "Rebuild"
    let g:__unreal_makeprg_args = call('unreal#get_ubt_args', a:000)
    call unreal#run_make("ubuild", a:bang)
endfunction

function! unreal#clean(bang, ...) abort
    let g:__unreal_makeprg_script = "Clean"
    let g:__unreal_makeprg_args = call('unreal#get_ubt_args', a:000)
    call unreal#run_make("ubuild", a:bang)
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(1)
    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

" }}}