summaryrefslogblamecommitdiffstats
path: root/autoload/fsharp.vim
blob: 90313231db8396c242caa62570ecda96edba7ac9 (plain) (tree)











































































































































































































































































































































                                                                                                                                                    
                                                                                                          























































































































































































                                                                                                
if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'fsharp') == -1

" Vim autoload functions

if exists('g:loaded_autoload_fsharp')
    finish
endif
let g:loaded_autoload_fsharp = 1

let s:cpo_save = &cpo
set cpo&vim

function! s:prompt(msg)
    let height = &cmdheight
    if height < 2
        set cmdheight=2
    endif
    echom a:msg
    let &cmdheight = height
endfunction

function! s:PlainNotification(content)
    return { 'Content': a:content }
endfunction

function! s:TextDocumentIdentifier(path)
    let usr_ss_opt = &shellslash
    set shellslash
    let uri = fnamemodify(a:path, ":p")
    if uri[0] == "/"
        let uri = "file://" . uri
    else
        let uri = "file:///" . uri
    endif
    let &shellslash = usr_ss_opt
    return { 'Uri': uri }
endfunction

function! s:Position(line, character)
    return { 'Line': a:line, 'Character': a:character }
endfunction

function! s:TextDocumentPositionParams(documentUri, line, character)
    return {
        \ 'TextDocument': s:TextDocumentIdentifier(a:documentUri),
        \ 'Position':     s:Position(a:line, a:character)
        \ }
endfunction

function! s:DocumentationForSymbolRequest(xmlSig, assembly)
    return {
        \ 'XmlSig': a:xmlSig,
        \ 'Assembly': a:assembly
        \ }
endfunction

function! s:ProjectParms(projectUri)
    return { 'Project': s:TextDocumentIdentifier(a:projectUri) }
endfunction

function! s:WorkspacePeekRequest(directory, deep, excludedDirs)
    return {
        \ 'Directory': fnamemodify(a:directory, ":p"),
        \ 'Deep': a:deep,
        \ 'ExcludedDirs': a:excludedDirs
        \ }
endfunction

function! s:WorkspaceLoadParms(files)
    let prm = []
    for file in a:files
        call add(prm, s:TextDocumentIdentifier(file))
    endfor
    return { 'TextDocuments': prm }
endfunction

function! s:FsdnRequest(query)
    return { 'Query': a:query }
endfunction

function! s:call(method, params, cont)
    call LanguageClient#Call(a:method, a:params, a:cont)
endfunction

function! s:signature(filePath, line, character, cont)
    return s:call('fsharp/signature', s:TextDocumentPositionParams(a:filePath, a:line, a:character), a:cont)
endfunction
function! s:signatureData(filePath, line, character, cont)
    return s:call('fsharp/signatureData', s:TextDocumentPositionParams(a:filePath, a:line, a:character), a:cont)
endfunction
function! s:lineLens(projectPath, cont)
    return s:call('fsharp/lineLens', s:ProjectParms(a:projectPath), a:cont)
endfunction
function! s:compilerLocation(cont)
    return s:call('fsharp/compilerLocation', {}, a:cont)
endfunction
function! s:compile(projectPath, cont)
    return s:call('fsharp/compile', s:ProjectParms(a:projectPath), a:cont)
endfunction
function! s:workspacePeek(directory, depth, excludedDirs, cont)
    return s:call('fsharp/workspacePeek', s:WorkspacePeekRequest(a:directory, a:depth, a:excludedDirs), a:cont)
endfunction
function! s:workspaceLoad(files, cont)
    return s:call('fsharp/workspaceLoad', s:WorkspaceLoadParms(a:files), a:cont)
endfunction
function! s:project(projectPath, cont)
    return s:call('fsharp/project', s:ProjectParms(a:projectPath), a:cont)
endfunction
function! s:fsdn(signature, cont)
    return s:call('fsharp/fsdn', s:FsdnRequest(a:signature), a:cont)
endfunction
function! s:f1Help(filePath, line, character, cont)
    return s:call('fsharp/f1Help', s:TextDocumentPositionParams(a:filePath, a:line, a:character), a:cont)
endfunction
function! fsharp#documentation(filePath, line, character, cont)
    return s:call('fsharp/documentation', s:TextDocumentPositionParams(a:filePath, a:line, a:character), a:cont)
endfunction
function! s:documentationSymbol(xmlSig, assembly, cont)
    return s:call('fsharp/documentationSymbol', s:DocumentationForSymbolRequest(a:xmlSig, a:assembly), a:cont)
endfunction

" FSharpConfigDto from https://github.com/fsharp/FsAutoComplete/blob/master/src/FsAutoComplete/LspHelpers.fs
" 
" * The following options seems not working with workspace/didChangeConfiguration
"   since the initialization has already completed?
"     'AutomaticWorkspaceInit',
"     'WorkspaceModePeekDeepLevel',
"
" * Changes made to linter/unused analyzer settings seems not reflected after sending them to FSAC?
"
let s:config_keys_camel =
    \ [
    \     {'key': 'AutomaticWorkspaceInit', 'default': 1},
    \     {'key': 'WorkspaceModePeekDeepLevel', 'default': 2},
    \     {'key': 'ExcludeProjectDirectories', 'default': []},
    \     {'key': 'keywordsAutocomplete', 'default': 1},
    \     {'key': 'ExternalAutocomplete', 'default': 0},
    \     {'key': 'Linter', 'default': 1},
    \     {'key': 'UnionCaseStubGeneration', 'default': 1},
    \     {'key': 'UnionCaseStubGenerationBody'},
    \     {'key': 'RecordStubGeneration', 'default': 1},
    \     {'key': 'RecordStubGenerationBody'},
    \     {'key': 'InterfaceStubGeneration', 'default': 1},
    \     {'key': 'InterfaceStubGenerationObjectIdentifier', 'default': 'this'},
    \     {'key': 'InterfaceStubGenerationMethodBody'},
    \     {'key': 'UnusedOpensAnalyzer', 'default': 1},
    \     {'key': 'UnusedDeclarationsAnalyzer', 'default': 1},
    \     {'key': 'SimplifyNameAnalyzer', 'default': 0},
    \     {'key': 'ResolveNamespaces', 'default': 1},
    \     {'key': 'EnableReferenceCodeLens', 'default': 1},
    \     {'key': 'EnableAnalyzers', 'default': 0},
    \     {'key': 'AnalyzersPath'},
    \     {'key': 'DisableInMemoryProjectReferences', 'default': 0},
    \     {'key': 'LineLens', 'default': {'enabled': 'replaceCodeLens', 'prefix': '//'}},
    \     {'key': 'UseSdkScripts', 'default': 1},
    \     {'key': 'dotNetRoot'},
    \     {'key': 'fsiExtraParameters', 'default': []},
    \ ]
let s:config_keys = []

function! fsharp#toSnakeCase(str)
    let sn = substitute(a:str, '\(\<\u\l\+\|\l\+\)\(\u\)', '\l\1_\l\2', 'g')
    if sn == a:str | return tolower(a:str) | endif
    return sn
endfunction

function! s:buildConfigKeys()
    if len(s:config_keys) == 0
        for key_camel in s:config_keys_camel
            let key = {}
            let key.snake = fsharp#toSnakeCase(key_camel.key)
            let key.camel = key_camel.key
            if has_key(key_camel, 'default')
                let key.default = key_camel.default
            endif
            call add(s:config_keys, key)
        endfor
    endif
endfunction

function! g:fsharp#getServerConfig()
    let fsharp = {}
    call s:buildConfigKeys() 
    for key in s:config_keys
        if exists('g:fsharp#' . key.snake)
            let fsharp[key.camel] = g:fsharp#{key.snake}
        elseif exists('g:fsharp#' . key.camel)
            let fsharp[key.camel] = g:fsharp#{key.camel}
        elseif has_key(key, 'default') && g:fsharp#use_recommended_server_config
            let g:fsharp#{key.snake} = key.default
            let fsharp[key.camel] = key.default
        endif
    endfor
    return fsharp
endfunction

function! g:fsharp#updateServerConfig()
    let fsharp = fsharp#getServerConfig()
    let settings = {'settings': {'FSharp': fsharp}}
    call LanguageClient#Notify('workspace/didChangeConfiguration', settings)
endfunction

function! s:findWorkspace(dir, cont)
    let s:cont_findWorkspace = a:cont
    function! s:callback_findWorkspace(result)
        let result = a:result
        let content = json_decode(result.result.content)
        if len(content.Data.Found) < 1
            return []
        endif
        let workspace = { 'Type': 'none' }
        for found in content.Data.Found
            if workspace.Type == 'none'
                let workspace = found
            elseif found.Type == 'solution'
                if workspace.Type == 'project'
                    let workspace = found
                else
                    let curLen = len(workspace.Data.Items)
                    let newLen = len(found.Data.Items)
                    if newLen > curLen
                        let workspace = found
                    endif
                endif
            endif
        endfor
        if workspace.Type == 'solution'
            call s:cont_findWorkspace([workspace.Data.Path])
        else
            call s:cont_findWorkspace(workspace.Data.Fsprojs)
        endif
    endfunction
    call s:workspacePeek(a:dir, g:fsharp#workspace_mode_peek_deep_level, g:fsharp#exclude_project_directories, function("s:callback_findWorkspace"))
endfunction

let s:workspace = []

function! s:load(arg)
    let s:loading_workspace = a:arg
    function! s:callback_load(_)
        echo "[FSAC] Workspace loaded: " . join(s:loading_workspace, ', ')
        let s:workspace = s:workspace + s:loading_workspace
    endfunction
    call s:workspaceLoad(a:arg, function("s:callback_load"))
endfunction

function! fsharp#loadProject(...)
    let prjs = []
    for proj in a:000
        call add(prjs, fnamemodify(proj, ':p'))
    endfor
    call s:load(prjs)
endfunction

function! fsharp#loadWorkspaceAuto()
    if &ft == 'fsharp'
        call fsharp#updateServerConfig()
        if g:fsharp#automatic_workspace_init
            echom "[FSAC] Loading workspace..."
            let bufferDirectory = fnamemodify(resolve(expand('%:p')), ':h')
            call s:findWorkspace(bufferDirectory, function("s:load"))
        endif
    endif
endfunction

function! fsharp#reloadProjects()
    if len(s:workspace) > 0
        function! s:callback_reloadProjects(_)
            call s:prompt("[FSAC] Workspace reloaded.")
        endfunction
        call s:workspaceLoad(s:workspace, function("s:callback_reloadProjects"))
    else
        echom "[FSAC] Workspace is empty"
    endif
endfunction

function! fsharp#OnFSProjSave()
    if &ft == "fsharp_project" && exists('g:fsharp#automatic_reload_workspace') && g:fsharp#automatic_reload_workspace
        call fsharp#reloadProjects()
    endif
endfunction

function! fsharp#showSignature()
    function! s:callback_showSignature(result)
        let result = a:result
        if exists('result.result.content')
            let content = json_decode(result.result.content)
            if exists('content.Data')
                echom substitute(content.Data, '\n\+$', ' ', 'g')
            endif
        endif
    endfunction
    call s:signature(expand('%:p'), line('.') - 1, col('.') - 1, function("s:callback_showSignature"))
endfunction

function! fsharp#OnCursorMove()
    if g:fsharp#show_signature_on_cursor_move
        call fsharp#showSignature()
    endif
endfunction

function! fsharp#showF1Help()
    let result = s:f1Help(expand('%:p'), line('.') - 1, col('.') - 1)
    echo result
endfunction

function! fsharp#showTooltip()
    function! s:callback_showTooltip(result)
        let result = a:result
        if exists('result.result.content')
            let content = json_decode(result.result.content)
            if exists('content.Data')
                call LanguageClient#textDocument_hover()
            endif
        endif
    endfunction
    " show hover only if signature exists for the current position
    call s:signature(expand('%:p'), line('.') - 1, col('.') - 1, function("s:callback_showTooltip"))
endfunction

let s:script_root_dir = expand('<sfile>:p:h') . "/../"
let s:fsac = fnamemodify(s:script_root_dir . "fsac/fsautocomplete.dll", ":p")
let g:fsharp#languageserver_command =
    \ ['dotnet', s:fsac, 
        \ '--background-service-enabled'
    \ ]

function! s:download(branch)
    echom "[FSAC] Downloading FSAC. This may take a while..."
    let zip = s:script_root_dir . "fsac.zip"
    call system(
        \ 'curl -fLo ' . zip .  ' --create-dirs ' .
        \ '"https://github.com/fsharp/FsAutoComplete/releases/latest/download/fsautocomplete.netcore.zip"'
        \ )
    if v:shell_error == 0
        call system('unzip -o -d ' . s:script_root_dir . "/fsac " . zip)
        echom "[FSAC] Updated FsAutoComplete to version " . a:branch . "" 
    else
        echom "[FSAC] Failed to update FsAutoComplete"
    endif
endfunction

function! fsharp#updateFSAC(...)
    if len(a:000) == 0
        let branch = "master"
    else
        let branch = a:000[0]
    endif
    call s:download(branch)
endfunction

let s:fsi_buffer = -1
let s:fsi_job    = -1
let s:fsi_width  = 0
let s:fsi_height = 0

function! s:win_gotoid_safe(winid)
    function! s:vimReturnFocus(window)
        call win_gotoid(a:window)
        redraw!
    endfunction
    if has('nvim')
        call win_gotoid(a:winid)
    else
        call timer_start(1, { -> s:vimReturnFocus(a:winid) })
    endif
endfunction

function! s:get_fsi_command()
    let cmd = g:fsharp#fsi_command
    for prm in g:fsharp#fsi_extra_parameters
        let cmd = cmd . " " . prm
    endfor
    return cmd
endfunction

function! fsharp#openFsi(returnFocus)
    if bufwinid(s:fsi_buffer) <= 0
        let fsi_command = s:get_fsi_command()
        " Neovim
        if exists('*termopen') || exists('*term_start')
            let current_win = win_getid()
            execute g:fsharp#fsi_window_command
            if s:fsi_width  > 0 | execute 'vertical resize' s:fsi_width | endif
            if s:fsi_height > 0 | execute 'resize' s:fsi_height | endif
            " if window is closed but FSI is still alive then reuse it
            if s:fsi_buffer >= 0 && bufexists(str2nr(s:fsi_buffer))
                exec 'b' s:fsi_buffer
                normal G
                if !has('nvim') && mode() == 'n' | execute "normal A" | endif
                if a:returnFocus | call s:win_gotoid_safe(current_win) | endif
            " open FSI: Neovim
            elseif has('nvim')
                let s:fsi_job = termopen(fsi_command)
                if s:fsi_job > 0
                    let s:fsi_buffer = bufnr("%")
                else
                    close
                    echom "[FSAC] Failed to open FSI."
                    return -1
                endif
            " open FSI: Vim
            else
                let options = {
                \ "term_name": "F# Interactive",
                \ "curwin": 1,
                \ "term_finish": "close"
                \ }
                let s:fsi_buffer = term_start(fsi_command, options)
                if s:fsi_buffer != 0
                    if exists('*term_setkill') | call term_setkill(s:fsi_buffer, "term") | endif
                    let s:fsi_job = term_getjob(s:fsi_buffer)
                else
                    close
                    echom "[FSAC] Failed to open FSI."
                    return -1
                endif
            endif
            setlocal bufhidden=hide
            normal G
            if a:returnFocus | call s:win_gotoid_safe(current_win) | endif
            return s:fsi_buffer
        else
            echom "[FSAC] Your Vim does not support terminal".
            return 0
        endif
    endif
    return s:fsi_buffer
endfunction

function! fsharp#toggleFsi()
    let fsiWindowId = bufwinid(s:fsi_buffer)
    if fsiWindowId > 0
        let current_win = win_getid()
        call win_gotoid(fsiWindowId)
        let s:fsi_width = winwidth('%')
        let s:fsi_height = winheight('%')
        close
        call win_gotoid(current_win)
    else
        call fsharp#openFsi(0)
    endif
endfunction

function! fsharp#quitFsi()
    if s:fsi_buffer >= 0 && bufexists(str2nr(s:fsi_buffer))
        if has('nvim')
            let winid = bufwinid(s:fsi_buffer)
            if winid > 0 | execute "close " . winid | endif
            call jobstop(s:fsi_job)
        else
            call job_stop(s:fsi_job, "term")
        endif
        let s:fsi_buffer = -1
        let s:fsi_job = -1
    endif
endfunction

function! fsharp#resetFsi()
    call fsharp#quitFsi()
    return fsharp#openFsi(1)
endfunction

function! fsharp#sendFsi(text)
    if fsharp#openFsi(!g:fsharp#fsi_focus_on_send) > 0
        " Neovim
        if has('nvim')
            call chansend(s:fsi_job, a:text . ";;". "\n")
        " Vim 8
        else
            call term_sendkeys(s:fsi_buffer, a:text . ";;" . "\<cr>")
            call term_wait(s:fsi_buffer)
        endif
    endif
endfunction

" https://stackoverflow.com/a/6271254
function! s:get_visual_selection()
    let [line_start, column_start] = getpos("'<")[1:2]
    let [line_end, column_end] = getpos("'>")[1:2]
    let lines = getline(line_start, line_end)
    if len(lines) == 0
        return ''
    endif
    let lines[-1] = lines[-1][: column_end - (&selection == 'inclusive' ? 1 : 2)]
    let lines[0] = lines[0][column_start - 1:]
    return lines
endfunction

function! s:get_complete_buffer()
    return join(getline(1, '$'), "\n")
endfunction

function! fsharp#sendSelectionToFsi() range
    let lines = s:get_visual_selection()
    exec 'normal' len(lines) . 'j'
    let text = join(lines, "\n")
    return fsharp#sendFsi(text)
endfunction

function! fsharp#sendLineToFsi()
    let text = getline('.')
    exec 'normal j'
    return fsharp#sendFsi(text)
endfunction

function! fsharp#sendAllToFsi()
    let text = s:get_complete_buffer()
    return fsharp#sendFsi(text)
endfunction

let &cpo = s:cpo_save
unlet s:cpo_save

" vim: sw=4 et sts=4

endif